diff --git a/.gitignore b/.gitignore index ac1e67a..6979fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,8 @@ -# QEMU runtime state, downloaded media, and generated captures -.qemu/* -!.qemu/*.ps1 -!.qemu/*.mjs -!.qemu/*.txt -!.qemu/share/ -!.qemu/share/*.c -.qemu/share/fec/ +# Legacy local QEMU state is no longer used, but may contain large user-owned images. +.qemu/ -.qemu/*.qcow2 -.qemu/*.img -.qemu/*.iso -.qemu/*.zip -.qemu/*.png -.qemu/*.ppm -.qemu/*.tmp +# Reproducible DOSBox-X/Open Watcom development cache and ephemeral runs +.dosboxx/ # Windows reserved-device artifact: `> nul` under Git Bash creates a real file. # Committing it breaks checkout on Windows. @@ -36,6 +25,3 @@ __pycache__/ node_modules/ .npm/ .cache/ - -# Temporary DOS build sources -.qemu/share/dosag*.c diff --git a/.qemu/install.ps1 b/.qemu/install.ps1 deleted file mode 100644 index 2d3ade9..0000000 --- a/.qemu/install.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -param() - -$ErrorActionPreference = 'Stop' -$qemu = Get-Command qemu-system-i386.exe -ErrorAction Stop -$disk = Join-Path $PSScriptRoot 'freedos.qcow2' -$iso = Join-Path $PSScriptRoot 'FD14LIVE.iso' - -if (-not (Test-Path -LiteralPath $disk)) { - throw "Missing $disk. Create it first with: qemu-img create -f qcow2 .qemu\\freedos.qcow2 2G" -} -if (-not (Test-Path -LiteralPath $iso)) { - throw "Missing $iso. Run .qemu\\setup.ps1 first." -} -if (Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue) { - throw 'QEMU monitor port 4444 is already in use. Stop the existing VM first.' -} - -& $qemu.Source ` - -machine pc,usb=on ` - -cpu pentium3 ` - -m 64 ` - -drive "file=$disk,format=qcow2,if=ide,index=0,media=disk" ` - -drive "file=$iso,media=cdrom,readonly=on" ` - -nic user,model=ne2k_isa ` - -monitor "tcp:127.0.0.1:4444,server=on,wait=off" ` - -serial null ` - -boot order=d ` - -display default diff --git a/.qemu/readme.txt b/.qemu/readme.txt deleted file mode 100644 index bd4facd..0000000 --- a/.qemu/readme.txt +++ /dev/null @@ -1,144 +0,0 @@ -############################################################################### - FreeDOS 1.4 ("FreeDOS 1.4") -############################################################################### - - -------------------------------------------------------------------------------- - General system requirements: -------------------------------------------------------------------------------- - - * DOS-compatible system (Intel + BIOS, or UEFI with Legacy support) - - * At least 20MB free disk space: - - 20MB Plain DOS system - 30MB Plain DOS system, with sources - - 275MB Full installation including applications and games - 450MB Full installation with sources - - -------------------------------------------------------------------------------- - What's in all those zip files? -------------------------------------------------------------------------------- - -FD14-LiveCD.zip - - * FD14BOOT.IMG - Basic FreeDOS installation boot floppy image. - If your computer has a CD-ROM drive, but you cannot boot from the Live CD - or Legacy CD. Use this diskette image to boot the system. Then insert the - install CD. The FreeDOS installer should do the rest. This diskette - image is for installation purposes only and does not provide a Live - Environment. - - * FD14LIVE.ISO - The FreeDOS 1.4 installer. Most users should - use this image to install FreeDOS. - - Depending on your computer system and hardware configuration, you - can also use the LiveCD to boot and run FreeDOS directly from the - CD-ROM without installation to your hard drive. - -FD14-LegacyCD.zip - - * FD14BOOT.IMG - This zip archive also contains a copy of the basic - CD-ROM installation boot floppy. - - * FD14LGCY.ISO - A bootable CD image designed for older hardware. If - you cannot boot the LiveCD to install FreeDOS, try this disc image. - - This disc image uses the older El Torito boot CD format. Some newer - computers and virtual machines cannot use this older format. Unless - you have a computer that requires this type of bootable CD, we - recommend using the LiveCD instead. - -FD14-BonusCD.zip - - * FD14BNS.ISO - A non-bootable CD image that contains some FreeDOS - packages that are not installed as part of either the LiveCD or - the Legacy CD. - -FD14-LiteUSB.zip - - * FD14LITE.IMG - A minimal FreeDOS installer, as a USB fob drive - image. This does not contain all of the packages from either the - LiveCD or the LegacyCD, and instead only contains a basic set of - FreeDOS packages. - - * FD14LITE.VMDK - A virtual machine disk file, compatible with a - variety of virtual machine software including VirtualBox, VMware, - and other systems. - - Using a VMDK file can simplify installing FreeDOS. Just attach the - VMDK image to your virtual machine software as a hard drive, and - boot it. (Please note that you will still need to create a virtual - hard disk to install FreeDOS) - -FD14-FullUSB.zip - - * FD14FULL.IMG - Plain DOS system and Full install USB stick image. - - * FD14FULL.VMDK - A virtual machine disk file, compatible with a - variety of virtual machine software. Just attach the VMDK image to - your virtual machine as a hard drive, and boot it. - -VERIFY.TXT - - * Contains MD5, SHA256 and SHA512 hashes for all of the different - release files. You can verify your copy of FreeDOS with these. - -README.TXT - - * The "before you choose and install" document. (All of the zip - files listed above also have a copy of the README file.) - - -------------------------------------------------------------------------------- - FreeDOS Floppy-Only Edition (FD14-x86) -------------------------------------------------------------------------------- - -FreeDOS 1.4 includes a Floppy-Only Edition! This edition should run on -any hardware that can run FreeDOS and has EGA or better graphics: - - * Are you running a '286 or another classic system without a CD-ROM - drive? Install from these floppies to install FreeDOS. - - * Do you have just one hard drive and no CD or floppy drive? Just - copy the contents of the floppies to a temporary directory and run - the installer from there. - - * Want to perform a "headless" install to a different DOS directory? - It's easy with the command line options. - -The Floppy-Only Edition uses a completely different installer than -the CD-ROM or USB installers. The Floppy-Only Edition does not use -any of those other media to install. - -The Floppy-Only Edition contains a limited set of FreeDOS programs -that are more useful on classic PC hardware. - -The FreeDOS Floppy-Only Edition is distributed as single zip archive that -contains several pre-made floppy diskette images: - - These zip archives contain image files for several common floppy - diskette media under separate directories: - - * 720k - 3.5" 720k diskette images - - * 144m - 3.5" 1.44mb diskette images - - * 120m - 5.25" 1.2mb diskette images - - Each of those sets contain a number of pre-made disk images: - - * x86BOOT.img - A floppy boot disk image with the x86 installer. - - * x86DSK??.img - Several floppy diskette images that contain the - core FreeDOS operating system files. The number of floppy images - and amount of files on each varies depending on the diskette - capacity. - -To conserve space, the FreeDOS Floppy-Only Edition does not contain -the source code for the FreeDOS packages. You can find the source code -via the FreeDOS website (https://www.freedos.org/download/) or from -the other release media, like the USB or CD-ROM installer. - diff --git a/.qemu/send-keys.ps1 b/.qemu/send-keys.ps1 deleted file mode 100644 index 3431978..0000000 --- a/.qemu/send-keys.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -param( - [Parameter(Mandatory)] - [string] $Text, - - [ValidateRange(0, 1000)] - [int] $DelayMilliseconds = 25, - - [switch] $NoEnter -) - -$ErrorActionPreference = 'Stop' - -$map = @{ - ' ' = 'spc'; ':' = 'shift-semicolon'; ';' = 'semicolon' - '\' = 'backslash'; '|' = 'shift-backslash'; '/' = 'slash'; '?' = 'shift-slash' - '.' = 'dot'; '>' = 'shift-dot'; ',' = 'comma'; '<' = 'shift-comma' - '-' = 'minus'; '_' = 'shift-minus'; '=' = 'equal'; '+' = 'shift-equal' - '[' = 'bracket_left'; '{' = 'shift-bracket_left' - ']' = 'bracket_right'; '}' = 'shift-bracket_right' - "'" = 'apostrophe'; '"' = 'shift-apostrophe'; '`' = 'grave_accent'; '~' = 'shift-grave_accent' - '!' = 'shift-1'; '@' = 'shift-2'; '#' = 'shift-3'; '$' = 'shift-4'; '%' = 'shift-5' - '^' = 'shift-6'; '&' = 'shift-7'; '*' = 'shift-8'; '(' = 'shift-9'; ')' = 'shift-0' -} - -function ConvertTo-QemuKey([char] $Character) { - $text = [string] $Character - if ($map.ContainsKey($text)) { return $map[$text] } - if ([char]::IsLetter($Character)) { - $letter = [char]::ToLowerInvariant($Character) - if ([char]::IsUpper($Character)) { return "shift-$letter" } - return [string] $letter - } - if ([char]::IsDigit($Character)) { return $text } - throw "Unsupported QEMU key character: '$Character'" -} - -$client = [System.Net.Sockets.TcpClient]::new([System.Net.Sockets.AddressFamily]::InterNetwork) -try { - $client.Connect([System.Net.IPAddress]::Parse('127.0.0.1'), 4444) - $stream = $client.GetStream() - $writer = [System.IO.StreamWriter]::new($stream, [System.Text.Encoding]::ASCII, 1024, $true) - try { - $writer.NewLine = "`r`n" - $writer.AutoFlush = $true - Start-Sleep -Milliseconds 100 - - foreach ($char in $Text.ToCharArray()) { - $writer.WriteLine("sendkey $(ConvertTo-QemuKey $char)") - Start-Sleep -Milliseconds $DelayMilliseconds - } - if (-not $NoEnter) { $writer.WriteLine('sendkey ret') } - Start-Sleep -Milliseconds $DelayMilliseconds - } - finally { - $writer.Dispose() - } -} -finally { - $client.Dispose() -} diff --git a/.qemu/setup.ps1 b/.qemu/setup.ps1 deleted file mode 100644 index 58b944e..0000000 --- a/.qemu/setup.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -param() - -$ErrorActionPreference = 'Stop' -$qemuImg = Get-Command qemu-img.exe -ErrorAction Stop -$disk = Join-Path $PSScriptRoot 'freedos.qcow2' -$archive = Join-Path $PSScriptRoot 'FD14-LiveCD.zip' -$iso = Join-Path $PSScriptRoot 'FD14LIVE.iso' -$url = 'https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/distributions/1.4/FD14-LiveCD.zip' - -if (-not (Test-Path -LiteralPath $disk)) { - & $qemuImg.Source create -f qcow2 $disk 2G - if ($LASTEXITCODE -ne 0) { throw "qemu-img failed with exit code $LASTEXITCODE." } -} - -if (-not (Test-Path -LiteralPath $iso)) { - if (-not (Test-Path -LiteralPath $archive)) { - Invoke-WebRequest -Uri $url -OutFile $archive - } - Expand-Archive -LiteralPath $archive -DestinationPath $PSScriptRoot -Force -} - -if (-not (Test-Path -LiteralPath $iso)) { - throw 'FreeDOS ISO extraction failed.' -} - -Get-Item -LiteralPath $disk,$iso | Select-Object Name,Length,LastWriteTime diff --git a/AGENTS.md b/AGENTS.md index 629b919..ad78332 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,48 +10,39 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 | `SPEC.md` | 언어 명세 + 구현 지시서. 유일한 규범 문서 | | `SPEC.AUDIT.md` | 명세 변경의 문제·결정·근거·구현 영향 누적 로그 | | `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | -| `tools/tcpagent/README.md` | DOS 내부 TCP 에이전트 프로토콜과 빌드 | -VM 자동화 **명령 목록과 플래그는 문서가 아니라 CLI가 규범**이다. 문서에 복제하면 -반드시 드리프트하므로 아래로 확인한다. +개발 환경과 테스트 명령 목록·플래그는 CLI로 확인한다. ```powershell -uv run ferro-vm --help -uv run ferro-vm --help +uv run ferro-dos --help +uv run ferro-dos --help +uv run ferro-test --help ``` ## 검증 규칙 -- **실행 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox는 쓰지 않는다. -- 컴파일러 A와 생성 C 모두 VM 안의 Open Watcom으로 컴파일한다. - 컴파일러 A와 bits16은 `WCL`, bits32 생성 C는 `WCL386`. -- **호스트에서 컴파일하지 않는다.** WSL이나 호스트 C 컴파일러 결과는 정식 검증으로 - 인정하지 않는다. 호스트는 편집, diff, Git, 파일 전송에만 쓴다. -- authoritative workspace는 VM의 `C:\FEC`다. -- 마일스톤 완료 기준은 `C:\FEC\BUILD.OK`, `C:\FEC\TEST.OK`, `TEST-DOS.BAT` exit 0 - 세 가지를 모두 확인하는 것이다. 테스트가 증명하지 않는 기능은 완료로 처리하지 - 않는다. -- VGA 데모처럼 멀티모달 수동 검증이 필요한 항목은 완료 게이트에서 제외한다. +- 컴파일러와 생성 C는 DOSBox-X 내부의 고정된 Open Watcom으로 컴파일한다. + 컴파일러와 bits16은 `WCL`, bits32 생성 C는 `WCL386`을 쓴다. +- 호스트 C 컴파일러 결과는 검증으로 인정하지 않는다. 호스트는 편집, Git, 다운로드, + 격리 작업공간 준비에만 쓴다. +- 실행마다 만들어지는 authoritative workspace는 `C:\FEC`다. 호스트에서는 + `.dosboxx/runs//FEC`에 대응한다. +- 완료하려는 기능을 직접 검사하는 pytest case가 통과해야 한다. 테스트가 증명하지 + 않는 기능은 완료로 처리하지 않는다. +- VGA 데모처럼 수동 검증이 필요한 항목은 자동 완료 게이트에서 제외한다. ## 빌드 함정 -VM 안에서 반복해서 물렸던 것들. 어기면 원인 찾기 어려운 실패가 난다. - -- **컴파일러 A는 16비트 large model로 빌드한다.** small model은 메모리 부족으로 - 실패한다. -- **링크는 `*.obj` 와일드카드로 한다.** DOS 명령줄 길이 제한 때문에 오브젝트를 - 나열할 수 없다. 그래서 `fec/build-dos.bat`는 먼저 `C:\FEC\*.obj`를 지워 - stale 32비트 test object가 섞이지 않게 한다. -- **M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다.** 생성 C의 보수적 미사용 helper 때문에 +- 컴파일러는 16비트 large model로 빌드한다. small model은 메모리 부족으로 실패한다. +- 링크는 `*.obj` 와일드카드로 한다. DOS 명령줄 길이 제한 때문에 오브젝트를 나열할 + 수 없다. `fec/build-dos.bat`는 먼저 stale object를 지운다. +- M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다. 생성 C의 보수적 미사용 helper 때문에 W202만 끄고 나머지 경고는 오류로 유지한다. -- **fixture는 짧은 이름으로 전송한다.** DOS 8.3 파일명 때문에 긴 이름은 - `BAD-ARI.FE`, `TRY-FPR.FE`처럼 명시적으로 줄여야 한다. -- **`D:`에서 빌드하지 않는다.** QEMU의 vvfat 뷰는 교환용이며, 과거 `D:`에서 빌드하다 - rename 처리 오류로 QEMU가 종료된 적이 있다. 호스트에서 편집한 파일은 `put`으로 - `C:`에 올린 뒤 컴파일한다. -- 긴 DOS 배치가 멈추면 QEMU를 재시작하기 전에 Ctrl+C 주입을 먼저 시도한다. - `Terminate batch file ... (Yes/No/All)?`가 뜨면 `y`, `ret`을 보내고 `ping` 복구를 - 확인한다. +- fixture는 DOS 8.3 이름으로 실행한다. 긴 이름은 registry에서 명시적으로 줄인다. +- `R:`은 읽기 전용 저장소, `W:`은 읽기 전용 Watcom이다. 빌드 산출물은 반드시 + 임시 `C:\FEC`에 쓴다. +- 실패 분석이 필요하면 `ferro-test --keep-failed` 또는 `ferro-dos --keep`으로 + 임시 작업공간을 보존한다. ## 작업 흐름 @@ -59,12 +50,9 @@ VM 안에서 반복해서 물렸던 것들. 어기면 원인 찾기 어려운 구현이 명세와 다르면 둘 중 하나가 틀린 것이므로 그 자리에서 결론을 낸다. - 검증된 마일스톤마다 커밋하고 항상 `origin`에 푸시한다. - primary 브랜치는 `master`다. -- `.qemu/*.png`, `.qemu/*.ppm`은 진단용이며 커밋하지 않는다. +- `.dosboxx/`의 다운로드, 실행 작업공간, 로그는 커밋하지 않는다. ## 현재 상태 -- M1~M5 완료 및 QEMU/Open Watcom 검증됨. -- 다음은 M6(R1~R8 대여 검사)다. 착수할 때 소유권 로직을 `check.c`/`emit_c.c`에서 - `own.c/h`로 분리한다 (`SPEC.md` §11.3). -- v0.1.6에서 R8(파생 반환), R6(마지막 사용까지 대여), R10(전역 대여 금지)이 - 바뀌었다. 셋 다 own.c의 상태 기계를 건드리므로 분리 이후에 함께 구현한다. +현재 구현 상태와 다음 마일스톤은 `SPEC.md`와 테스트 registry를 기준으로 판단한다. +과거 VM 이미지나 호스트에 남은 바이너리를 근거로 완료 처리하지 않는다. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index d6fcdd1..0000000 --- a/TODO.md +++ /dev/null @@ -1,27 +0,0 @@ -# TODO - -## Done - -- [x] Add a non-reboot abort path for a hung DOS command: inject `Ctrl+C` through - QEMU's monitor and wait for the agent to recover. -- [x] Apply a configurable timeout to `exec` and invoke the non-reboot abort path - on timeout. -- [x] Expose `abort` for an immediate user-requested command interruption. - -The three above were first built for the COM1 serial agent, lost in the rewrite -to the resident TCP agent, and rebuilt on the QEMU monitor in `ferro-vm exec`. -The TCP version supervises with guest disk liveness (`info blockstats`) rather -than a fixed stopwatch, so a slow compile is no longer mistaken for a hang. - -## Open - -- [ ] Consider `BREAK=ON` in `C:\FDCONFIG.SYS`. Ctrl+C only takes effect at a DOS - break check, and with the FreeDOS default of `BREAK=OFF` a compute-bound - child whose output is redirected to a file may never reach one, so `abort` - cannot always stop it. `BREAK=ON` checks on every DOS call and makes the - abort reliable, at a small cost to every DOS call. Needs a VM reboot; back - up `FDCONFIG.SYS` first. -- [ ] `TCPAGENT.EXE` connects from a fixed source port (`LOCAL_PORT 2058`). After - the host end closes, a reconnect reuses the same 4-tuple and can flap until - the old state ages out. Observed as a ~10s connect/disconnect cycle after - the daemon is killed mid-connection. diff --git a/fec/tests/m3/nochk.fe b/fec/tests/m3/nochk.fe new file mode 100644 index 0000000..e8e62f3 --- /dev/null +++ b/fec/tests/m3/nochk.fe @@ -0,0 +1,7 @@ +unit m3_no_checks; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + let x: i32 = a[2]; + return x - x; +} diff --git a/fec/vm-m1.bat b/fec/vm-m1.bat deleted file mode 100644 index e6e76a3..0000000 --- a/fec/vm-m1.bat +++ /dev/null @@ -1,201 +0,0 @@ -@echo off -rem D: is the read-only exchange volume. Stage everything before running DOS tools. -if not exist C:\FEC md C:\FEC -if not exist C:\FEC\SRC md C:\FEC\SRC -if not exist C:\FEC\STD md C:\FEC\STD -if not exist C:\FEC\TESTS md C:\FEC\TESTS -if not exist C:\FEC\TESTS\PASS md C:\FEC\TESTS\PASS -if not exist C:\FEC\TESTS\FAIL md C:\FEC\TESTS\FAIL -if not exist C:\FEC\TESTS\M2 md C:\FEC\TESTS\M2 -if not exist C:\FEC\TESTS\M3 md C:\FEC\TESTS\M3 -if not exist C:\FEC\TESTS\M4 md C:\FEC\TESTS\M4 -if not exist C:\FEC\TESTS\M5 md C:\FEC\TESTS\M5 -if exist C:\FEC\VM.FAIL del C:\FEC\VM.FAIL -if exist C:\FEC\STAGE.FAIL del C:\FEC\STAGE.FAIL - -copy D:\FEC\BUILD-~1.BAT C:\FEC\BUILD.BAT > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TEST-DOS.BAT C:\FEC\TEST-DOS.BAT > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\SRC\ARENA.C C:\FEC\SRC\ARENA.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\ARENA.H C:\FEC\SRC\ARENA.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\DIAG.C C:\FEC\SRC\DIAG.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\DIAG.H C:\FEC\SRC\DIAG.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\LEXER.C C:\FEC\SRC\LEXER.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\LEXER.H C:\FEC\SRC\LEXER.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\AST.C C:\FEC\SRC\AST.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\AST.H C:\FEC\SRC\AST.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\PARSER.C C:\FEC\SRC\PARSER.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\PARSER.H C:\FEC\SRC\PARSER.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\DRIVER.C C:\FEC\SRC\DRIVER.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\TYPES.C C:\FEC\SRC\TYPES.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\TYPES.H C:\FEC\SRC\TYPES.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\CHECK.C C:\FEC\SRC\CHECK.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\CHECK.H C:\FEC\SRC\CHECK.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\EMIT_C.C C:\FEC\SRC\EMIT_C.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\EMIT_C.H C:\FEC\SRC\EMIT_C.H > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\STD\CORE.FE C:\FEC\STD\CORE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\FMT.FE C:\FEC\STD\FMT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\IO.FE C:\FEC\STD\IO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\LIST.FE C:\FEC\STD\LIST.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\MAP.FE C:\FEC\STD\MAP.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\MEM.FE C:\FEC\STD\MEM.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\STR.FE C:\FEC\STD\STR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\SYS.FE C:\FEC\STD\SYS.FE > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\TESTS\PASS\BASIC.FE C:\FEC\TESTS\PASS\BASIC.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\PASS\LITERALS.FE C:\FEC\TESTS\PASS\LITERALS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\PASS\KEYWOR~1.FE C:\FEC\TESTS\PASS\KEYWOR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\PASS\V012-F~1.FE C:\FEC\TESTS\PASS\V012-F.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\FAIL\MISSIN~1.FE C:\FEC\TESTS\FAIL\MISSIN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\FAIL\UNCLOS~1.FE C:\FEC\TESTS\FAIL\UNCLOS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\FAIL\LOGICA~1.FE C:\FEC\TESTS\FAIL\LOGICA.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\HELLO.FE C:\FEC\TESTS\M2\HELLO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\SCOPES.FE C:\FEC\TESTS\M2\SCOPES.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-CO~1.FE C:\FEC\TESTS\M2\BAD-CO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-CAST.FE C:\FEC\TESTS\M2\BAD-CA.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-ASSI~1.FE C:\FEC\TESTS\M2\BAD-AS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-UN~1.FE C:\FEC\TESTS\M2\BAD-UN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-UN~2.FE C:\FEC\TESTS\M2\BAD-UI.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-AR~1.FE C:\FEC\TESTS\M2\BAD-AR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-TY~1.FE C:\FEC\TESTS\M2\BAD-TY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-RE~1.FE C:\FEC\TESTS\M2\BAD-RE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-VOID.FE C:\FEC\TESTS\M2\BAD-VO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\CAST-W~1.FE C:\FEC\TESTS\M2\CAST-W.FE > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\TESTS\M3\STRUCT.FE C:\FEC\TESTS\M3\STRUCT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\ENUM.FE C:\FEC\TESTS\M3\ENUM.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\ARRAY.FE C:\FEC\TESTS\M3\ARRAY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\STR.FE C:\FEC\TESTS\M3\STR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\FOR.FE C:\FEC\TESTS\M3\FOR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\NESTED.FE C:\FEC\TESTS\M3\NESTED.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\CHAR.FE C:\FEC\TESTS\M3\CHAR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\ARRAYCTX.FE C:\FEC\TESTS\M3\ARRAYCTX.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BOUNDS.FE C:\FEC\TESTS\M3\BOUNDS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADFLD.FE C:\FEC\TESTS\M3\BADFLD.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADMAT.FE C:\FEC\TESTS\M3\BADMAT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADARR.FE C:\FEC\TESTS\M3\BADARR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADCYCLE.FE C:\FEC\TESTS\M3\BADCYCLE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADSTR.FE C:\FEC\TESTS\M3\BADSTR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADCHAR.FE C:\FEC\TESTS\M3\BADCHAR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADFIELD.FE C:\FEC\TESTS\M3\BADFIELD.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADINDEX.FE C:\FEC\TESTS\M3\BADINDEX.FE > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\TESTS\M4\FORMAT.FE C:\FEC\TESTS\M4\FORMAT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-ARI~1.FE C:\FEC\TESTS\M4\BAD-ARI.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-VERB.FE C:\FEC\TESTS\M4\BAD-VERB.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-RUN~1.FE C:\FEC\TESTS\M4\BAD-RUN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-TYP~1.FE C:\FEC\TESTS\M4\BAD-TYP.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-TRY.FE C:\FEC\TESTS\M4\BAD-TRY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\TRY-FPR~1.FE C:\FEC\TESTS\M4\TRY-FPR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-WRI~1.FE C:\FEC\TESTS\M4\BAD-WRI.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\PROP.FE C:\FEC\TESTS\M4\PROP.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\PROPTEST.C C:\FEC\TESTS\M4\PROPTEST.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-MANY.FE C:\FEC\TESTS\M4\BAD-MANY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-OPEN.FE C:\FEC\TESTS\M4\BAD-OPEN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-CLS.FE C:\FEC\TESTS\M4\BAD-CLS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\DEFER.FE C:\FEC\TESTS\M5\DEFER.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\OWNED.FE C:\FEC\TESTS\M5\OWNED.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\BAD-MOVE.FE C:\FEC\TESTS\M5\BAD-MOVE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\BAD-DES~1.FE C:\FEC\TESTS\M5\BAD-DES.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\RUNTIME.FE C:\FEC\TESTS\M5\RUNTIME.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\RUNTIME.C C:\FEC\TESTS\M5\RUNTIME.C > nul -if errorlevel 1 goto stage_fail - -call C:\FEC\TEST-DOS.BAT -if exist C:\FEC\TEST.OK goto vm_success -echo FAIL>C:\FEC\VM.FAIL -verify other 2>nul -goto stage_done - -:vm_success -cd C:\FEC -goto stage_done - -:stage_fail -echo FAIL>C:\FEC\STAGE.FAIL -echo FAIL>C:\FEC\VM.FAIL -verify other 2>nul - -:stage_done diff --git a/pyproject.toml b/pyproject.toml index 2660113..635c397 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [project] name = "ferrolang" version = "0.1.0" -description = "Ferro language compiler and QEMU development automation" +description = "Ferro language compiler and reproducible DOS development tools" requires-python = ">=3.12" dependencies = [ - "onnxruntime>=1.28.0", - "rapidocr>=3.9.2", + "pytest>=9.0.0", ] [project.scripts] -ferro-vm = "ferrolang_vm.cli:main" +ferro-dos = "ferrolang_vm.dos_cli:main" +ferro-test = "ferrolang_vm.test_cli:main" [build-system] requires = ["hatchling"] diff --git a/src/ferrolang_vm/__init__.py b/src/ferrolang_vm/__init__.py index 039a606..a63f890 100644 --- a/src/ferrolang_vm/__init__.py +++ b/src/ferrolang_vm/__init__.py @@ -1 +1 @@ -"""Windows-only QEMU and FreeDOS TCP-agent automation.""" +"""Reproducible DOS development tools for the Ferro compiler.""" diff --git a/src/ferrolang_vm/cli.py b/src/ferrolang_vm/cli.py deleted file mode 100644 index ff40ddb..0000000 --- a/src/ferrolang_vm/cli.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Command line client for the Windows-only ferro-vm daemon.""" -from __future__ import annotations - -import argparse -import json -import shutil -import subprocess -import sys -import time -from multiprocessing.connection import Client -from pathlib import Path - -from .daemon import PIPE, ROOT - - -def rpc(payload: dict[str, object], start_daemon: bool = False) -> object: - try: - conn = Client(PIPE, family="AF_PIPE") - except (FileNotFoundError, OSError): - if not start_daemon: - raise RuntimeError("ferro-vm daemon is not running; run `uv run ferro-vm start`") - flags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) | getattr(subprocess, "DETACHED_PROCESS", 0) - subprocess.Popen([sys.executable, "-m", "ferrolang_vm.daemon"], cwd=ROOT, creationflags=flags, close_fds=True) - deadline = time.monotonic() + 5 - while True: - try: - conn = Client(PIPE, family="AF_PIPE") - break - except (FileNotFoundError, OSError): - if time.monotonic() >= deadline: - raise RuntimeError("ferro-vm daemon did not create its control pipe") - time.sleep(.1) - with conn: - conn.send(payload) - response = conn.recv() - if not response["ok"]: - raise RuntimeError(response["error"]) - return response["result"] - - -def wait_ready(timeout: int) -> bool: - """Wait quietly; do not turn an expected boot gap into error-log spam.""" - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - try: - status = rpc({"op": "status"}) - if status["agent_connected"] and str(rpc({"op": "ping"})["response"]).startswith("OK 504F4E47"): - return True - except RuntimeError: - pass - time.sleep(.5) - return False - - -def follow_logs() -> int: - log_path = ROOT / ".qemu" / "ferro-vm.log" - lnav = shutil.which("lnav.exe") or shutil.which("lnav") - if lnav: - return subprocess.run([lnav, str(log_path)]).returncode - print("lnav was not found; following the log with PowerShell.", file=sys.stderr) - return subprocess.run([ - "powershell", "-NoProfile", "-Command", - f"Get-Content -LiteralPath '{log_path}' -Wait", - ]).returncode - - -EPILOG = r"""examples: - uv run ferro-vm start boot the VM and start the daemon - uv run ferro-vm wait-ready block until TCPAGENT answers PING - uv run ferro-vm exec 'dir C:\FEC' run a DOS command, print exit code and output - uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' - uv run ferro-vm get 'C:\FEC\TEST.OK' .qemu/TEST.OK - uv run ferro-vm exec --idle-timeout 180 'C:\FEC\BUILD-DOS.BAT' - uv run ferro-vm abort Ctrl+C the command running right now - uv run ferro-vm logs follow the structured daemon log - -The authoritative workspace is C:\FEC inside the VM. Never build on D: (the vvfat -view is for exchange only). See AGENTS.md for verification rules and build traps. -""" - -SIMPLE_COMMANDS = { - "start": "Start the daemon and boot QEMU. Safe to run when already up.", - "stop": "Quit QEMU cleanly and stop the daemon.", - "status": "Print daemon, QEMU, and TCPAGENT connection state as JSON.", - "ping": "Send PING to TCPAGENT. Expects 'OK 504F4E47' (PONG).", - "screenshot": "Capture the VGA console to a PPM/PNG under .qemu/.", - "ocr": "Capture the console and print recognized text (RapidOCR).", - "logs": "Follow the append-only daemon log. Uses lnav when available.", - "abort": "Interrupt the DOS command currently running (Ctrl+C via QEMU).", -} - - -def main() -> int: - parser = argparse.ArgumentParser( - prog="ferro-vm", - description="Windows-only QEMU/FreeDOS automation for the Ferro compiler.", - epilog=EPILOG, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - commands = parser.add_subparsers(dest="op", required=True, metavar="COMMAND") - for name, blurb in SIMPLE_COMMANDS.items(): - commands.add_parser(name, help=blurb, description=blurb) - - wait_help = "Block until TCPAGENT is connected and answers PING." - wait = commands.add_parser("wait-ready", help=wait_help, description=wait_help) - wait.add_argument("--timeout", type=int, default=45, metavar="SECONDS", - help="give up after this many seconds (default: %(default)s)") - - reset_help = "Quit QEMU cleanly, reboot it, and wait for TCPAGENT." - reset = commands.add_parser("reset", help=reset_help, description=reset_help) - reset.add_argument("--timeout", type=int, default=45, metavar="SECONDS", - help="give up after this many seconds (default: %(default)s)") - - exec_help = "Run a DOS command inside the VM and print its exit code and output." - execute = commands.add_parser( - "exec", help=exec_help, - description=exec_help + " Quote the command so the host shell does not eat" - r" backslashes: exec 'wcl386 -q HELLO.C'." - " A slow command is not a failed one: the wait ends" - " when the guest stops touching its disk, not when a" - " stopwatch expires.") - execute.add_argument("command", metavar="DOS_COMMAND", - help=r"command line to hand to COMMAND.COM, e.g. 'dir C:\FEC'") - execute.add_argument("--idle-timeout", type=float, default=60, metavar="SECONDS", - help="interrupt once the guest has made no disk access for" - " this long (default: %(default)s)") - execute.add_argument("--hard-timeout", type=float, default=900, metavar="SECONDS", - help="interrupt after this much total time regardless of" - " activity; the backstop for a CPU-bound hang" - " (default: %(default)s)") - - put_help = "Copy a host file into the VM." - put = commands.add_parser("put", help=put_help, description=put_help) - put.add_argument("source", type=Path, metavar="HOST_PATH", - help="file on this machine") - put.add_argument("destination", metavar="DOS_PATH", - help=r"target inside the VM, e.g. 'C:\FEC\SRC\CHECK.C'." - " DOS uses 8.3 names, so long fixtures must be shortened" - " explicitly (BAD-ARI.FE, TRY-FPR.FE)") - - get_help = "Copy a file out of the VM onto the host." - get = commands.add_parser("get", help=get_help, description=get_help) - get.add_argument("source", metavar="DOS_PATH", - help=r"file inside the VM, e.g. 'C:\FEC\TEST.OK'") - get.add_argument("destination", type=Path, metavar="HOST_PATH", - help="target on this machine") - - args = parser.parse_args() - - try: - if args.op == "logs": - return follow_logs() - if args.op == "wait-ready": - if wait_ready(args.timeout): - print(json.dumps({"agent": "PONG"})) - return 0 - raise RuntimeError("TCPAGENT did not become ready") - if args.op == "ocr": - result = rpc({"op": "screenshot"}) - import logging - logging.disable(logging.INFO) - from rapidocr import RapidOCR - recognized = RapidOCR()(result["path"]) - print("\n".join(recognized.txts or ())) - return 0 - if args.op == "reset": - rpc({"op": "stop"}, start_daemon=True) - time.sleep(.5) - rpc({"op": "start"}, start_daemon=True) - time.sleep(2) - rpc({"op": "monitor", "command": "sendkey ret"}) - if wait_ready(args.timeout): - print(json.dumps({"reset": "complete", "agent": "PONG"})) - return 0 - raise RuntimeError("TCPAGENT did not become ready") - payload: dict[str, object] = {"op": args.op} - if args.op == "exec": - payload["command"] = args.command - payload["idle_timeout"] = args.idle_timeout - payload["hard_timeout"] = args.hard_timeout - if args.op == "put": - payload["source"] = str(args.source.resolve()) - payload["destination"] = args.destination - if args.op == "get": - payload["source"] = args.source - payload["destination"] = str(args.destination.resolve()) - print(json.dumps(rpc(payload, start_daemon=args.op == "start"), ensure_ascii=False, indent=2)) - return 0 - except RuntimeError as exc: - print(f"ferro-vm: {exc}", file=sys.stderr) - return 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/ferrolang_vm/daemon.py b/src/ferrolang_vm/daemon.py deleted file mode 100644 index 20b3e9d..0000000 --- a/src/ferrolang_vm/daemon.py +++ /dev/null @@ -1,467 +0,0 @@ -"""Long-lived Windows host for the FreeDOS TCP agent. - -The only automation TCP listener is 127.0.0.1:5558, used exclusively by -TCPAGENT.EXE. Local commands use a Windows named pipe. -""" -from __future__ import annotations - -import json -import logging -import os -import re -import select -import shutil -import socket -import subprocess -import sys -import threading -import time -from datetime import datetime, timezone -from multiprocessing.connection import Listener -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -QEMU = ROOT / ".qemu" -PIPE = r"\\.\pipe\ferrolang-vm" -AGENT_ADDRESS = ("127.0.0.1", 5558) -MONITOR_ADDRESS = ("127.0.0.1", 4444) -LOG_PATH = QEMU / "ferro-vm.log" - -# Short commands answer promptly, so a plain socket timeout is the right guard. -REQUEST_TIMEOUT = 30 -# EXEC is different: TCPAGENT is frozen inside system() for the whole command -# and cannot answer, so silence proves nothing. Wake up often, decide with -# guest liveness instead of a stopwatch, and never discard the connection just -# because a compile is slow. -EXEC_POLL_SECONDS = 2 -DEFAULT_IDLE_TIMEOUT = 60 -DEFAULT_HARD_TIMEOUT = 900 -# Budget for collecting the result after Ctrl+C, before giving up on the stream. -INTERRUPT_GRACE_SECONDS = 15 - - -class ExecInterrupted(RuntimeError): - """The supervisor decided to stop the running DOS command.""" - - -def configure_logging() -> None: - QEMU.mkdir(exist_ok=True) - handler = logging.FileHandler(LOG_PATH, encoding="utf-8") - handler.setFormatter(logging.Formatter("%(asctime)s.%(msecs)03dZ %(levelname)-7s %(message)s", "%Y-%m-%dT%H:%M:%S")) - logging.basicConfig(level=logging.INFO, handlers=[handler]) - logging.Formatter.converter = time.gmtime - - -def log_event(level: int, event: str, **fields: object) -> None: - suffix = " ".join(f"{key}={json.dumps(value, ensure_ascii=False)}" for key, value in fields.items()) - logging.log(level, "%s%s", event, f" {suffix}" if suffix else "") - - -class Host: - def __init__(self) -> None: - self.agent: socket.socket | None = None - self.agent_lock = threading.Lock() - self.agent_ready = threading.Event() - self.qemu: subprocess.Popen[bytes] | None = None - # QEMU accepts one monitor connection at a time, and the EXEC - # supervisor polls it while other control requests run concurrently. - self.monitor_lock = threading.Lock() - self.abort_requested = threading.Event() - self.exec_active = threading.Event() - - @staticmethod - def bind_agent_listener() -> socket.socket: - """Bind 5558 exclusively so a second daemon fails loudly. - - SO_REUSEADDR means something different on Windows than on Unix: it lets - another process bind an already-bound port and quietly take over new - connections, so a duplicate daemon would be silently half-working - instead of refusing to start. SO_EXCLUSIVEADDRUSE is the Windows way to - say "only me". - """ - server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - exclusive = getattr(socket, "SO_EXCLUSIVEADDRUSE", None) - if exclusive is not None: - server.setsockopt(socket.SOL_SOCKET, exclusive, 1) - else: - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(AGENT_ADDRESS) - server.listen(1) - return server - - def accept_agents(self, server: socket.socket) -> None: - log_event(logging.INFO, "agent listener ready", address="127.0.0.1:5558") - while True: - sock, peer = server.accept() - sock.settimeout(REQUEST_TIMEOUT) - with self.agent_lock: - if self.agent is not None: - sock.close() - log_event(logging.WARNING, "agent rejected", peer=str(peer), reason="already connected") - continue - self.agent = sock - try: - banner = self._read_line(sock).decode("ascii", "replace") - if banner != "TCPAGENT READY": - raise ConnectionError(f"invalid TCPAGENT banner: {banner!r}") - self.agent_ready.set() - log_event(logging.INFO, "agent connected", peer=f"{peer[0]}:{peer[1]}") - # Request() owns protocol reads. Between requests, peek only - # for EOF so TCPAGENT can reconnect without being rejected. - while self.agent is sock: - if self.agent_lock.acquire(blocking=False): - try: - readable, _, _ = select.select([sock], [], [], .25) - if readable and not sock.recv(1, socket.MSG_PEEK): - break - finally: - self.agent_lock.release() - else: - time.sleep(.05) - except (ConnectionError, OSError) as exc: - # Reset can close a connection during its banner. This is a - # per-connection event, never a reason to kill the listener. - log_event(logging.WARNING, "agent handshake/connection failed", - peer=f"{peer[0]}:{peer[1]}", error=str(exc)) - finally: - with self.agent_lock: - if self.agent is sock: - self.agent = None - self.agent_ready.clear() - sock.close() - log_event(logging.INFO, "agent disconnected", peer=f"{peer[0]}:{peer[1]}") - - @staticmethod - def _read_line(sock: socket.socket, supervise=None) -> bytes: - # Partial input is kept across timeouts, so supervise() may fire in the - # middle of a line without losing what has already arrived. - out = bytearray() - while not out.endswith(b"\n"): - try: - part = sock.recv(1) - except TimeoutError: - if supervise is None: - raise - supervise() - continue - if not part: - raise ConnectionError("TCP agent closed connection") - out.extend(part) - return bytes(out).rstrip(b"\r\n") - - @staticmethod - def _read_exactly(sock: socket.socket, count: int, supervise=None) -> bytes: - chunks: list[bytes] = [] - while count: - try: - chunk = sock.recv(min(65536, count)) - except TimeoutError: - if supervise is None: - raise - supervise() - continue - if not chunk: - raise ConnectionError("TCP agent closed connection") - chunks.append(chunk) - count -= len(chunk) - return b"".join(chunks) - - def guest_idle_seconds(self) -> float | None: - """Seconds since the guest last touched a disk, or None if unknown. - - QEMU keeps counting while TCPAGENT is frozen inside system(), so this - is the one progress signal available during a long DOS command. A - purely CPU-bound command looks idle here, which is what the hard - timeout is for. - """ - try: - text = self.monitor("info blockstats") - except OSError: - return None - idle: float | None = None - for line in text.splitlines(): - operations = re.search(r"rd_operations=(\d+)", line) - elapsed = re.search(r"idle_time_ns=(\d+)", line) - if not operations or not elapsed or int(operations.group(1)) == 0: - continue - seconds = int(elapsed.group(1)) / 1e9 - idle = seconds if idle is None else min(idle, seconds) - return idle - - def request(self, command: str, payload: bytes = b"") -> str: - with self.agent_lock: - if self.agent is None: - raise RuntimeError("TCPAGENT is not connected") - sock = self.agent - started = time.monotonic() - try: - sock.sendall(command.encode("ascii") + b"\n" + payload) - response = self._read_line(sock).decode("ascii", "replace") - except OSError as exc: - if self.agent is sock: - self.agent = None - self.agent_ready.clear() - raise RuntimeError(f"TCPAGENT request failed: {exc}") from exc - log_event(logging.INFO, "agent request", command=command.split(" ", 1)[0], response=response[:200], elapsed_ms=round((time.monotonic()-started)*1000)) - if response.startswith("ERR "): - raise RuntimeError(bytes.fromhex(response[4:]).decode("utf-8", "replace")) - return response - - def ping(self) -> dict[str, object]: - return {"response": self.request("PING")} - - def _read_exec_result(self, sock: socket.socket, supervise=None) -> tuple[int, int, bytes]: - header = self._read_line(sock, supervise).decode("ascii", "replace") - fields = header.split() - if len(fields) == 4 and fields[0] == "RESULT": - code, length, flags = int(fields[1]), int(fields[2]), int(fields[3]) - return code, flags, self._read_exactly(sock, length, supervise) - if fields and fields[0] == "OK": - # Compatibility with an installed pre-RESULT agent. - return int(fields[1]), 1, bytes.fromhex(fields[2]) if len(fields) > 2 else b"" - raise RuntimeError("malformed EXEC response: " + header) - - def exec(self, command: str, idle_timeout: float = DEFAULT_IDLE_TIMEOUT, - hard_timeout: float = DEFAULT_HARD_TIMEOUT) -> dict[str, object]: - log_event(logging.INFO, "exec start", command=command, - idle_timeout=idle_timeout, hard_timeout=hard_timeout) - started = time.monotonic() - self.abort_requested.clear() - with self.agent_lock: - if self.agent is None: - raise RuntimeError("TCPAGENT is not connected") - sock = self.agent - self.exec_active.set() - reported = started - interrupt_at: float | None = None - interrupted = "" - - def supervise() -> None: - """Called every EXEC_POLL_SECONDS while the agent stays silent.""" - nonlocal reported, interrupt_at, interrupted - now = time.monotonic() - idle = self.guest_idle_seconds() - if now - reported >= 15: - reported = now - log_event(logging.INFO, "exec running", elapsed_s=round(now-started, 1), - guest_idle_s=None if idle is None else round(idle, 1)) - if interrupt_at is None: - if self.abort_requested.is_set(): - interrupted = "aborted by request" - elif now - started > hard_timeout: - interrupted = f"hard timeout after {hard_timeout:.0f}s" - elif idle is not None and idle > idle_timeout: - interrupted = f"guest idle {idle:.0f}s exceeds {idle_timeout:.0f}s" - if interrupted: - interrupt_at = now - log_event(logging.WARNING, "exec interrupting", reason=interrupted) - self.monitor("sendkey ctrl-c") - return - waited = now - interrupt_at - # DOS answers Ctrl+C with "Terminate batch file (Y/N/A)?" and - # waits there. The prompt only appears once COMMAND.COM reaches - # the next batch line, which can be many seconds into a slow - # command, so answer on every poll rather than once: a single - # early 'y' is swallowed by whatever is still running. Send only - # 'y' -- the prompt takes one keystroke, and a trailing Enter - # gets read as "keep going". - self.monitor("sendkey y") - # Even answered, Ctrl+C is a request. It lands only at a DOS - # break check, and a DOS/4GW child (wcc386, wmake) runs in - # protected mode where it may never reach one, so the command - # can still run to completion. Keep collecting its result rather - # than abandoning a stream that still owes us one -- give up - # only once the guest has gone quiet too. - if waited > INTERRUPT_GRACE_SECONDS and (idle is None or idle > 5): - raise ExecInterrupted(interrupted + "; command did not stop") - - try: - encoded = command.encode("ascii", "replace").hex().upper() - sock.settimeout(EXEC_POLL_SECONDS) - sock.sendall(f"EXEC {encoded}\n".encode("ascii")) - code, flags, raw = self._read_exec_result(sock, supervise) - except (OSError, ExecInterrupted) as exc: - # Only now is the stream beyond repair; drop it so the agent - # reconnects with a clean protocol state. - if self.agent is sock: - self.agent = None - self.agent_ready.clear() - sock.close() - raise RuntimeError(f"TCPAGENT EXEC failed: {exc}") from exc - finally: - self.exec_active.clear() - self.abort_requested.clear() - try: - sock.settimeout(REQUEST_TIMEOUT) - except OSError: - pass - output = raw.decode("cp437", "replace") - for line in output.splitlines(): - log_event(logging.INFO, "dos output", line=line) - log_event(logging.INFO, "exec finish", exit=code, bytes=len(raw), flags=flags, - interrupted=interrupted or None, - elapsed_ms=round((time.monotonic()-started)*1000)) - result = {"exit": code, "output": output, "bytes": len(raw), "flags": flags} - if interrupted: - result["interrupted"] = interrupted - return result - - def abort(self) -> dict[str, object]: - if not self.exec_active.is_set(): - return {"aborted": False, "reason": "no command is running"} - self.abort_requested.set() - log_event(logging.INFO, "abort requested") - return {"aborted": True} - - def put(self, source: str, destination: str) -> dict[str, object]: - data = Path(source).read_bytes() - encoded = destination.encode("ascii").hex().upper() - self.request(f"PUT {encoded} {len(data)}", data) - stat = self.request(f"HASH {encoded}") - log_event(logging.INFO, "put", path=destination, bytes=len(data), stat=stat) - return {"path": destination, "bytes": len(data), "stat": stat} - - def get(self, source: str, destination: str) -> dict[str, object]: - encoded = source.encode("ascii").hex().upper() - with self.agent_lock: - if self.agent is None: - raise RuntimeError("TCPAGENT is not connected") - sock = self.agent - sock.sendall(f"GET {encoded}\n".encode("ascii")) - header = self._read_line(sock).decode("ascii", "strict").split() - if len(header) != 2 or header[0] != "DATA": - raise RuntimeError("GET failed: " + " ".join(header)) - remaining = int(header[1]) - chunks: list[bytes] = [] - while remaining: - chunk = sock.recv(min(65536, remaining)) - if not chunk: - raise RuntimeError("TCPAGENT closed during GET") - chunks.append(chunk) - remaining -= len(chunk) - target = Path(destination) - target.parent.mkdir(parents=True, exist_ok=True) - data = b"".join(chunks) - target.write_bytes(data) - log_event(logging.INFO, "get", path=source, bytes=len(data), destination=str(target)) - return {"path": source, "destination": str(target), "bytes": len(data)} - - def monitor(self, command: str) -> str: - with self.monitor_lock, socket.create_connection(MONITOR_ADDRESS, timeout=3) as sock: - sock.settimeout(1) - time.sleep(.1) - try: - sock.recv(4096) - except TimeoutError: - pass - sock.sendall(command.encode("ascii") + b"\n") - time.sleep(.2) - chunks: list[bytes] = [] - while True: - try: - chunk = sock.recv(4096) - except TimeoutError: - break - if not chunk: - break - chunks.append(chunk) - return b"".join(chunks).decode("ascii", "replace").strip() - - def start(self) -> dict[str, object]: - if self.qemu is not None and self.qemu.poll() is None: - return {"started": False, "reason": "already running"} - try: - self.monitor("info status") - return {"started": False, "reason": "already running (external)"} - except OSError: - pass - executable = shutil.which("qemu-system-i386.exe") - disk = QEMU / "freedos.qcow2" - if not executable: - raise RuntimeError("qemu-system-i386.exe is not on PATH") - if not disk.exists(): - raise RuntimeError(f"missing {disk}; run .qemu/setup.ps1 and .qemu/install.ps1") - self.qemu = subprocess.Popen([executable, "-machine", "pc,accel=whpx,kernel-irqchip=off,usb=on", "-smp", "1", "-m", "64", "-drive", f"file={disk},format=qcow2,if=ide,index=0,media=disk", "-nic", "user,model=ne2k_isa", "-monitor", "tcp:127.0.0.1:4444,server=on,wait=off", "-boot", "order=c", "-display", "default"], cwd=QEMU) - log_event(logging.INFO, "qemu started", pid=self.qemu.pid) - return {"started": True, "pid": self.qemu.pid} - - def stop(self) -> dict[str, object]: - try: - self.monitor("quit") - log_event(logging.INFO, "qemu stop requested") - except OSError: - pass - return {"stopped": True} - - def screenshot(self) -> dict[str, object]: - ppm, png = QEMU / "qemu-screen.ppm", QEMU / "qemu-screen.png" - self.monitor("screendump " + str(ppm).replace("\\", "/")) - ffmpeg = shutil.which("ffmpeg.exe") - if not ppm.exists() or not ffmpeg: - raise RuntimeError("screenshot failed or ffmpeg.exe is not on PATH") - subprocess.run([ffmpeg, "-y", "-loglevel", "error", "-i", str(ppm), str(png)], check=True) - ppm.unlink(missing_ok=True) - log_event(logging.INFO, "screenshot", path=str(png)) - return {"path": str(png)} - - def dispatch(self, request: dict[str, object]) -> object: - op = request["op"] - if op == "status": - try: - self.monitor("info status") - running = True - except OSError: - running = False - return {"agent_connected": self.agent_ready.is_set(), "qemu_running": running, "log": str(LOG_PATH)} - if op == "start": return self.start() - if op == "stop": return self.stop() - if op == "ping": return self.ping() - if op == "abort": return self.abort() - if op == "exec": - return self.exec(str(request["command"]), - float(request.get("idle_timeout", DEFAULT_IDLE_TIMEOUT)), - float(request.get("hard_timeout", DEFAULT_HARD_TIMEOUT))) - if op == "put": return self.put(str(request["source"]), str(request["destination"])) - if op == "get": return self.get(str(request["source"]), str(request["destination"])) - if op == "screenshot": return self.screenshot() - if op == "monitor": return {"output": self.monitor(str(request["command"]))} - raise ValueError(f"unknown operation: {op}") - - -def serve_pipe(host: Host) -> None: - listener = Listener(PIPE, family="AF_PIPE") - log_event(logging.INFO, "control pipe ready", pipe=PIPE) - - def handle(conn) -> None: - try: - request = conn.recv() - try: - conn.send({"ok": True, "result": host.dispatch(request)}) - except Exception as exc: - log_event(logging.ERROR, "control failed", error=str(exc)) - conn.send({"ok": False, "error": str(exc)}) - finally: - conn.close() - - while True: - # One thread per request: `abort` has to be answerable while a long - # `exec` is still holding the agent. - threading.Thread(target=handle, args=(listener.accept(),), daemon=True).start() - - -def main() -> None: - if os.name != "nt": - raise SystemExit("ferro-vm currently supports Windows only") - configure_logging() - host = Host() - try: - server = host.bind_agent_listener() - except OSError as exc: - log_event(logging.ERROR, "agent listener bind failed", address="127.0.0.1:5558", error=str(exc)) - raise SystemExit(f"another ferro-vm daemon already owns 127.0.0.1:5558 ({exc})") - threading.Thread(target=host.accept_agents, args=(server,), daemon=True).start() - serve_pipe(host) - - -if __name__ == "__main__": - main() diff --git a/src/ferrolang_vm/dos_cli.py b/src/ferrolang_vm/dos_cli.py new file mode 100644 index 0000000..02a0567 --- /dev/null +++ b/src/ferrolang_vm/dos_cli.py @@ -0,0 +1,80 @@ +"""General-purpose disposable DOSBox-X/Open Watcom environment CLI.""" +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +from .dosboxx import DosboxError, run_suite, setup +from .paths import ROOT +from .suite import Case + + +def _run(command: str | None, *, keep: bool, show_dos: bool) -> int: + cases = [] if command is None else [Case("command", 0, command, True)] + run = run_suite(cases, keep=keep, show_dos=show_dos, trace_dos=show_dos) + try: + if run.result() != "PASS": + print(run.log(), file=sys.stderr) + return 1 + if cases and run.result(cases[0]) != "PASS": + print(run.log(cases[0]), file=sys.stderr) + return 1 + if cases: + output = run.log(cases[0]) + if output: + print(output, end="" if output.endswith("\n") else "\n") + if keep: + print(f"DOS workspace: {run.root}") + return 0 + finally: + run.cleanup() + + +def main() -> int: + parser = argparse.ArgumentParser( + prog="ferro-dos", + description="Disposable directory-backed DOSBox-X/Open Watcom environment.", + ) + commands = parser.add_subparsers(dest="action", required=True) + prepare = commands.add_parser("setup", help="install the pinned DOSBox-X and Open Watcom tools") + prepare.add_argument("--accept-watcom-license", action="store_true") + for name, help_text in ( + ("build", "build the current FEC source inside DOS"), + ("exec", "build FEC and execute one DOS command"), + ("batch", "build FEC and call a repository DOS batch"), + ("shell", "build FEC and open an interactive DOS shell"), + ): + command = commands.add_parser(name, help=help_text) + command.add_argument("--keep", action="store_true", help="preserve the temporary DOS workspace") + command.add_argument("--show-dos", action="store_true", help="show and pause the DOS window") + if name == "exec": + command.add_argument("dos_command") + elif name == "batch": + command.add_argument("path", type=Path) + args = parser.parse_args() + try: + if args.action == "setup": + dosbox, watcom = setup(accept_watcom_license=args.accept_watcom_license) + print(f"DOSBox-X: {dosbox}") + print(f"Open Watcom: {watcom}") + return 0 + if args.action == "build": + return _run(None, keep=args.keep, show_dos=args.show_dos) + if args.action == "exec": + return _run(args.dos_command, keep=args.keep, show_dos=args.show_dos) + if args.action == "shell": + return _run("COMMAND.COM", keep=args.keep, show_dos=True) + path = (ROOT / args.path).resolve() + if (path != ROOT and ROOT not in path.parents) or not path.is_file(): + raise DosboxError("batch path must be an existing file inside the repository") + relative = path.relative_to(ROOT).as_posix().replace("/", "\\").upper() + return _run(f"CALL R:\\{relative}", keep=args.keep, show_dos=args.show_dos) + except (DosboxError, subprocess.SubprocessError) as exc: + print(f"ferro-dos: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py new file mode 100644 index 0000000..58b5ca8 --- /dev/null +++ b/src/ferrolang_vm/dosboxx.py @@ -0,0 +1,249 @@ +"""Reproducible DOSBox-X/Open Watcom development test backend.""" +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import subprocess +import tempfile +import urllib.request +import zipfile +from dataclasses import dataclass +from pathlib import Path + +from .paths import ROOT +from .suite import Case + + +CACHE = ROOT / ".dosboxx" +LOCK_PATH = ROOT / "tools" / "toolchains" / "dosboxx.lock.json" +RUNS = CACHE / "runs" + + +class DosboxError(RuntimeError): + pass + + +def _lock() -> dict[str, object]: + return json.loads(LOCK_PATH.read_text(encoding="utf-8")) + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + while chunk := source.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _download(name: str, spec: dict[str, object]) -> Path: + downloads = CACHE / "downloads" + downloads.mkdir(parents=True, exist_ok=True) + target = downloads / Path(str(spec["url"])).name + expected = str(spec["sha256"]).lower() + if target.is_file() and _sha256(target) == expected: + return target + target.unlink(missing_ok=True) + partial = target.with_suffix(target.suffix + ".part") + partial.unlink(missing_ok=True) + print(f"ferro-test: downloading {name} {spec['version']}...") + try: + with urllib.request.urlopen(str(spec["url"])) as response, partial.open("wb") as output: + shutil.copyfileobj(response, output, length=1024 * 1024) + except Exception: + partial.unlink(missing_ok=True) + raise + actual = _sha256(partial) + if actual != expected: + partial.unlink(missing_ok=True) + raise DosboxError(f"{name} SHA-256 mismatch: expected {expected}, got {actual}") + partial.replace(target) + return target + + +def _safe_extract(archive: Path, destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + temporary = Path(tempfile.mkdtemp(prefix=f".{destination.name}-", dir=destination.parent)) + try: + with zipfile.ZipFile(archive) as bundle: + root = temporary.resolve() + for member in bundle.infolist(): + target = (temporary / member.filename).resolve() + if target != root and root not in target.parents: + raise DosboxError(f"unsafe archive member: {member.filename}") + bundle.extractall(temporary) + if destination.exists(): + shutil.rmtree(destination) + temporary.replace(destination) + except Exception: + shutil.rmtree(temporary, ignore_errors=True) + raise + + +def _required_paths(lock: dict[str, object]) -> tuple[Path, Path, list[Path]]: + dosbox_spec = lock["dosboxx"] + watcom_spec = lock["open_watcom"] + assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) + dosbox = CACHE / "dosbox-x" / str(dosbox_spec["executable"]) + watcom = CACHE / "watcom" + required = [watcom / str(item) for item in watcom_spec["required"]] + return dosbox, watcom, required + + +def setup(*, accept_watcom_license: bool = False) -> tuple[Path, Path]: + if os.name != "nt": + raise DosboxError("the DOSBox-X development backend currently supports Windows only") + lock = _lock() + dosbox, watcom, watcom_required = _required_paths(lock) + tools_ready = dosbox.is_file() and all(path.is_file() for path in watcom_required) + if not tools_ready and not accept_watcom_license: + raise DosboxError( + "Open Watcom is distributed under the Sybase Open Watcom Public License. " + "Review tools/toolchains/dosboxx.lock.json and rerun " + "`uv run ferro-dos setup --accept-watcom-license`." + ) + if not tools_ready: + dosbox_spec = lock["dosboxx"] + watcom_spec = lock["open_watcom"] + assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) + _safe_extract(_download("DOSBox-X", dosbox_spec), CACHE / "dosbox-x") + _safe_extract(_download("Open Watcom", watcom_spec), watcom) + dosbox, watcom, watcom_required = _required_paths(lock) + missing = [str(path.relative_to(CACHE)) for path in [dosbox, *watcom_required] + if not path.is_file()] + if missing: + raise DosboxError("toolchain archive is missing: " + ", ".join(missing)) + (CACHE / "SETUP.OK").write_text(_sha256(LOCK_PATH) + "\n", encoding="ascii") + return dosbox, watcom + + +def resolve_tools() -> tuple[Path, Path]: + lock = _lock() + dosbox, watcom, required = _required_paths(lock) + if not dosbox.is_file() or not all(path.is_file() for path in required): + raise DosboxError("toolchain is not installed; run `uv run ferro-dos setup`") + return dosbox, watcom + + +def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: + lines = [ + "@echo off", "if not exist RESULTS md RESULTS", "if not exist OUT md OUT", + "set WATCOM=W:", "set INCLUDE=W:\\H", + "set LIB=W:\\LIB286\\DOS;W:\\LIB286;W:\\LIB386\\DOS;W:\\LIB386", + "call BUILD.BAT", "if not exist BUILD.OK goto BUILDFAIL", + "echo PASS>RESULTS\\BUILD.RES", + ] + for index, case in enumerate(cases): + key = f"C{index:03d}" + command = case.command + # Watcom writes diagnostics into the current directory. Isolate each + # case so a later pytest item never sees stale diagnostics. + lines.extend([ + "if exist *.ERR del *.ERR > NUL", + f"if exist RESULTS\\{key}.ERR del RESULTS\\{key}.ERR > NUL", + ]) + if not trace_dos: + command += f" > RESULTS\\{key}.LOG" + lines.append(command) + if case.expect_success: + lines.extend([ + f"if errorlevel 1 goto {key}E", f"echo PASS>RESULTS\\{key}.RES", + f"goto {key}C", f":{key}E", f"echo FAIL>RESULTS\\{key}.RES", + ]) + else: + lines.extend([ + f"if errorlevel 1 goto {key}E", f"echo FAIL>RESULTS\\{key}.RES", + f"goto {key}C", f":{key}E", f"echo PASS>RESULTS\\{key}.RES", + ]) + lines.extend([ + f":{key}C", f"if exist *.ERR type *.ERR > RESULTS\\{key}.ERR", + f"if not exist RESULTS\\{key}.ERR type NUL > RESULTS\\{key}.ERR", + ]) + lines.extend([ + "goto FINISH", ":BUILDFAIL", "echo FAIL>RESULTS\\BUILD.RES", ":FINISH", + "echo DONE>RUN.OK", + *(["pause"] if show_dos else []), "exit", "", + ]) + return "\r\n".join(lines) + + +@dataclass +class SuiteRun: + root: Path + cases: list[Case] + keep: bool = False + + @property + def fec(self) -> Path: + return self.root / "FEC" + + def _key(self, case: Case) -> str: + return f"C{self.cases.index(case):03d}" + + def result(self, case: Case | None = None) -> str: + name = "BUILD" if case is None else self._key(case) + path = self.fec / "RESULTS" / f"{name}.RES" + return path.read_text(encoding="ascii").strip() if path.is_file() else "MISSING" + + def log(self, case: Case | None = None) -> str: + name = "BUILD" if case is None else self._key(case) + path = self.fec / "RESULTS" / f"{name}.LOG" + content = path.read_text(encoding="utf-8", errors="replace") if path.is_file() else "" + if content: + return content + errors = sorted(self.fec.glob("*.ERR")) + if errors: + return "\n".join(p.read_text(encoding="utf-8", errors="replace") for p in errors) + console = self.root / "CONSOLE.LOG" + return console.read_text(encoding="utf-8", errors="replace") if console.is_file() else "" + + def err(self, case: Case) -> str: + path = self.fec / "RESULTS" / f"{self._key(case)}.ERR" + return path.read_text(encoding="utf-8", errors="replace") if path.is_file() else "" + + def cleanup(self) -> None: + if not self.keep: + shutil.rmtree(self.root, ignore_errors=True) + + +def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, + trace_dos: bool = False) -> SuiteRun: + dosbox, watcom = resolve_tools() + RUNS.mkdir(parents=True, exist_ok=True) + run_root = Path(tempfile.mkdtemp(prefix="suite-", dir=RUNS)) + result = SuiteRun(run_root, cases, keep) + fec = result.fec + try: + shutil.copytree(ROOT / "fec" / "src", fec / "SRC") + shutil.copytree(ROOT / "fec" / "std", fec / "STD") + shutil.copytree(ROOT / "fec" / "tests", fec / "TESTS") + shutil.copy2(ROOT / "fec" / "build-dos.bat", fec / "BUILD.BAT") + console = run_root / "CONSOLE.LOG" + config = run_root / "DOSBOX.CON" + config.write_text( + f"[log]\nlogfile={console}\n[dosbox]\nlog console=quiet\n", + encoding="ascii", + ) + (fec / "RUN.BAT").write_text( + _batch(cases, show_dos=show_dos, trace_dos=trace_dos), + encoding="ascii", newline="", + ) + command = [str(dosbox)] + if not show_dos: + command.append("-silent") + command.extend([ + "-fastlaunch", "-conf", str(config), + "-c", f'mount C "{run_root}"', "-c", f'mount R "{ROOT}" -ro', + "-c", f'mount W "{watcom}" -ro', + "-c", "C:", "-c", "cd \\FEC", "-c", "RUN.BAT", + ]) + completed = subprocess.run(command, check=False, timeout=300) + if completed.returncode != 0: + raise DosboxError(f"DOSBox-X exited with status {completed.returncode}") + if not (fec / "RUN.OK").is_file(): + raise DosboxError("DOSBox-X did not complete the test batch") + return result + except Exception: + result.keep = True + raise diff --git a/src/ferrolang_vm/paths.py b/src/ferrolang_vm/paths.py new file mode 100644 index 0000000..87ffbbd --- /dev/null +++ b/src/ferrolang_vm/paths.py @@ -0,0 +1,5 @@ +"""Repository and local cache paths shared by Ferro developer tools.""" +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py new file mode 100644 index 0000000..74d129a --- /dev/null +++ b/src/ferrolang_vm/registry.py @@ -0,0 +1,216 @@ +"""Milestone case registry: DOS commands and their expected exit status. + +Only commands live here; the ``.fe`` fixtures stay under ``fec/tests`` and are +copied into the disposable DOS filesystem by the runner. Case order is load +bearing -- ``emit`` must precede ``build`` must precede ``run`` for the same +fixture, because each step consumes the previous step's output. +""" +from __future__ import annotations + +from .suite import Case + +PASS = "TESTS\\PASS" +FAIL = "TESTS\\FAIL" +STD = "STD" +M2 = "TESTS\\M2" +M3 = "TESTS\\M3" +M4 = "TESTS\\M4" +M5 = "TESTS\\M5" +M6 = "TESTS\\M6" +OUT = "OUT" + +# Emitted-C basenames that were hand-shortened for DOS 8.3. Keyed by milestone +# because the same fixture name maps to different outputs across milestones +# (``bad-type`` is BAD-TY in M2 but BAD-TYP in M4). The shortenings are not +# consistent -- M2 cut to six characters, M4 to seven, and several were never +# required at all since BAD-COND is already a legal 8.3 name. Preserved verbatim; +# changing one renames a file inside the DOS run, so re-verify if you touch it. +_OUT83 = { + (2, "bad-cond"): "BAD-CO", + (2, "bad-cast"): "BAD-CA", + (2, "bad-asgn"): "BAD-AS", + (2, "bad-unk"): "BAD-UN", + (2, "bad-ari"): "BAD-AR", + (2, "bad-type"): "BAD-TY", + (2, "bad-ret"): "BAD-RE", + (2, "bad-unit"): "BAD-UI", + (2, "bad-void"): "BAD-VO", + (4, "bad-type"): "BAD-TYP", + (4, "bad-writ"): "BAD-WRI", + (5, "bad-dest"): "BAD-DES", +} + + +def _case(milestone: int, name: str, command: str, ok: bool = True) -> Case: + return Case(f"m{milestone}-{name}", milestone, command, ok) + + +def _fe(directory: str, name: str) -> str: + return f"{directory}\\{name.upper()}.FE" + + +def _emit(source: str, output: str, *, target: str = "bits32", + flags: tuple[str, ...] = (), output_first: bool = False) -> str: + """``fec`` invocation that translates ``source`` to C at ``output``. + + ``output_first`` reproduces the M6 cases, which pass ``-o`` before the input + file while every other milestone passes it after. + """ + parts = ["FEC.EXE", f"--target={target}", *flags, "--emit-c"] + parts += ["-o", output, source] if output_first else [source, "-o", output] + return " ".join(parts) + + +def _wcl(exe: str, *sources: str, bits: int = 32, strict: bool = False, + defines: tuple[str, ...] = ()) -> str: + """Open Watcom invocation. ``strict`` is the M4 ``-wx -wcd=202`` pairing: + warnings are errors except W202, which the generated C trips on unused + helpers (see AGENTS.md).""" + parts = ["WCL386" if bits == 32 else "WCL", "-q", "-za"] + if strict: + parts += ["-wx", "-wcd=202"] + parts += ["-bt=dos", *defines, f"-fe={exe}", *sources] + return " ".join(parts) + + +def _dump_ast(milestone: int, directory: str, names: tuple[str, ...], *, + suffix: str, ok: bool = True, prefix: str = "") -> list[Case]: + return [ + _case(milestone, f"{prefix}{name}-{suffix}", + f"FEC.EXE --dump-ast {_fe(directory, name)}", ok) + for name in names + ] + + +def _rejects(milestone: int, directory: str, names: tuple[str, ...], *, + suffix: str = "") -> list[Case]: + """Fixtures that must fail to compile. The emitted-C path is still spelled + out because ``fec`` needs an ``-o`` even when it is expected to bail.""" + return [ + _case(milestone, f"{name}-{suffix}" if suffix else name, + _emit(_fe(directory, name), + f"{directory}\\{_OUT83.get((milestone, name), name.upper())}.C"), + False) + for name in names + ] + + +def _triple(milestone: int, name: str, directory: str, *, stem: str | None = None, + target: str = "bits32", bits: int = 32, strict: bool = False, + build_source: str | None = None, emit_suffix: str | None = "emit", + run_suffix: str = "run", run_ok: bool = True) -> list[Case]: + """emit -> build -> run for one fixture. + + ``stem`` renames the C/EXE pair when the fixture name does not fit 8.3 or + collides (M2 castwhil emits CAST16). ``build_source`` compiles a different + file than the one emitted (M4 prop emits PROP.C but builds PROPTEST.C, which + ``#include``s it). + """ + stem = stem or name.upper() + cfile = f"{directory}\\{stem}.C" + exe = f"{directory}\\{stem}.EXE" + emit_id = f"{name}-{emit_suffix}" if emit_suffix else name + return [ + _case(milestone, emit_id, _emit(_fe(directory, name), cfile, target=target)), + _case(milestone, f"{name}-build", + _wcl(exe, build_source or cfile, bits=bits, strict=strict)), + _case(milestone, f"{name}-{run_suffix}", exe, run_ok), + ] + + +CASES: list[Case] = [ + # -- M1: parse only ------------------------------------------------------- + *_dump_ast(1, PASS, ("basic", "literals", "keybuilt", "v012form"), suffix="parse"), + *_dump_ast(1, STD, ("core", "fmt", "io", "list", "map", "mem", "str", "sys"), + suffix="parse", prefix="std-"), + *_dump_ast(1, FAIL, ("misssemi", "unclcomm", "logical"), suffix="reject", ok=False), + + # -- M2: first generated C ------------------------------------------------ + *_triple(2, "hello", M2), + *_triple(2, "scopes", M2), + *_triple(2, "castwhil", M2, stem="CAST16", target="bits16", bits=16), + *_rejects(2, M2, ("bad-cond", "bad-cast", "bad-asgn", "bad-unk", "bad-ari", + "bad-type", "bad-ret", "bad-unit", "bad-void"), suffix="reject"), + + # -- M3: aggregates, strings, bounds checks ------------------------------- + *_triple(3, "struct", M3), + *_triple(3, "enum", M3), + *_triple(3, "array", M3), + *_triple(3, "mutable", M3), + *_rejects(3, M3, ("bad-mlet", "bad-shwr"), suffix="reject"), + *_triple(3, "str", M3), + *_triple(3, "for", M3), + *_triple(3, "nested", M3), + *_triple(3, "char", M3), + *_triple(3, "arrayctx", M3), + # These two must trap at runtime: the bounds check is the feature under test. + *_triple(3, "bounds", M3, run_suffix="trap", run_ok=False), + *_triple(3, "slcbound", M3, run_suffix="trap", run_ok=False), + # --no-checks is proved by a differential on one source. NOCHK.FE reads one + # element past a [2]i32 and returns x - x, which is 0 whatever garbage the + # unchecked read produced: compiled with checks it must trap, compiled with + # --no-checks it must run to completion. BOUNDS.FE cannot serve as the + # unchecked half because it returns the out-of-bounds value directly, so its + # exit code would be whatever happens to sit past the array on the stack. + *_triple(3, "nochk", M3, run_suffix="trap", run_ok=False), + _case(3, "nochk-off-emit", + _emit(_fe(M3, "nochk"), f"{M3}\\NOCHK-N.C", flags=("--no-checks",))), + _case(3, "nochk-off-build", + _wcl(f"{M3}\\NOCHK-N.EXE", f"{M3}\\NOCHK-N.C")), + _case(3, "nochk-off-run", f"{M3}\\NOCHK-N.EXE"), + *_rejects(3, M3, ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", + "badfield", "badindex"), suffix="reject"), + + # -- M4: formatting and error propagation --------------------------------- + *_triple(4, "format", M4, strict=True, emit_suffix=None), + *_triple(4, "try-fpr", M4, strict=True, emit_suffix=None), + *_triple(4, "prop", M4, strict=True, emit_suffix=None, + build_source=f"{M4}\\PROPTEST.C"), + *_rejects(4, M4, ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", + "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls")), + + # -- M5: defer and ownership ---------------------------------------------- + _case(5, "defer", _emit(_fe(M5, "defer"), f"{M5}\\DEFER.C")), + _case(5, "owned", _emit(_fe(M5, "owned"), f"{M5}\\OWNED.C")), + *_rejects(5, M5, ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", + "bad-proj", "bad-clos", "bad-loop")), + # The runtime case links the generated C against a hand-written allocator + # shim, so malloc/free are redirected at compile time. + _case(5, "runtime", _emit(_fe(M5, "runtime"), f"{M5}\\RUNT-G.C")), + _case(5, "runtime-build", + _wcl(f"{M5}\\RUNTIME.EXE", f"{M5}\\RUNT-G.C", f"{M5}\\RUNTIME.C", + defines=("-dmalloc=m5_malloc", "-dfree=m5_free"))), + _case(5, "runtime-run", f"{M5}\\RUNTIME.EXE"), + + # -- M6: borrow checking (R1--R8) ----------------------------------------- + *[_case(6, name, f"FEC.EXE --check {_fe(M6, name)}", False) for name in ( + "badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", + "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", + "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", + "badtwo", "badup", "badweak")], + *[_case(6, name, _emit(_fe(M6, name), f"{OUT}\\{name.upper()}.C", + output_first=True)) for name in ( + "okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", + "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", + "okstatic", "oktemp", "oktrim", "okwcall")], +] + +MAX_MILESTONE: int = max(case.milestone for case in CASES) +MILESTONES: tuple[str, ...] = tuple(f"m{number}" + for number in range(1, MAX_MILESTONE + 1)) + + +def milestone_number(name: str) -> int: + """Parse an ``mN`` selector against the milestones the registry knows about.""" + if not name.startswith("m") or not name[1:].isdigit(): + raise ValueError(f"invalid milestone: {name}") + value = int(name[1:]) + if value not in range(1, MAX_MILESTONE + 1): + raise ValueError(f"unsupported milestone: {name}") + return value + + +def all_cases(*, through: int = MAX_MILESTONE, only: int | None = None) -> list[Case]: + if only is not None: + return [case for case in CASES if case.milestone == only] + return [case for case in CASES if case.milestone <= through] diff --git a/src/ferrolang_vm/suite.py b/src/ferrolang_vm/suite.py new file mode 100644 index 0000000..b412298 --- /dev/null +++ b/src/ferrolang_vm/suite.py @@ -0,0 +1,16 @@ +"""The one type shared by the case registry and the DOSBox-X runner. + +Kept separate from ``registry`` so that ``dosboxx`` can depend on the type +without importing the case data. +""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Case: + id: str + milestone: int + command: str + expect_success: bool diff --git a/src/ferrolang_vm/test_cli.py b/src/ferrolang_vm/test_cli.py new file mode 100644 index 0000000..e20f744 --- /dev/null +++ b/src/ferrolang_vm/test_cli.py @@ -0,0 +1,71 @@ +"""Developer test entry point backed by a disposable DOSBox-X run.""" +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +from .dosboxx import DosboxError, setup +from .registry import MAX_MILESTONE, MILESTONES + + +def main() -> int: + parser = argparse.ArgumentParser( + prog="ferro-test", + description="Fast Ferro development tests in DOSBox-X/Open Watcom.", + ) + commands = parser.add_subparsers(dest="command", required=True) + prepare = commands.add_parser("setup", help="download and verify pinned development tools") + prepare.add_argument("--accept-watcom-license", action="store_true", + help="confirm acceptance of the Sybase Open Watcom Public License") + run = commands.add_parser("run", help="build once and run milestone pytest cases") + selection = run.add_mutually_exclusive_group() + selection.add_argument("--through", choices=MILESTONES, default=f"m{MAX_MILESTONE}", + help="run cumulatively through this milestone " + f"(default: m{MAX_MILESTONE})") + selection.add_argument("--only", choices=MILESTONES, + help="run only this milestone's cases") + run.add_argument("-v", "--verbose", action="store_true", help="show every pytest case") + run.add_argument("--keep-failed", action="store_true", + help="keep the disposable DOS filesystem after failures") + run.add_argument("--show-dos", action="store_true", + help="show DOSBox-X and wait for a key before closing") + run.add_argument("--dos-log", action="store_true", + help="print the captured DOS console after the run") + run.add_argument("--trace-dos", action="store_true", + help="do not redirect case command output") + args = parser.parse_args() + try: + if args.command == "setup": + dosbox, watcom = setup(accept_watcom_license=args.accept_watcom_license) + print(f"DOSBox-X: {dosbox}") + print(f"Open Watcom: {watcom}") + return 0 + if args.only: + os.environ["FERRO_TEST_ONLY"] = args.only + else: + os.environ["FERRO_TEST_THROUGH"] = args.through + for enabled, name in ( + (args.keep_failed, "FERRO_TEST_KEEP_FAILED"), + (args.show_dos, "FERRO_TEST_SHOW_DOS"), + (args.trace_dos, "FERRO_TEST_TRACE_DOS"), + (args.dos_log, "FERRO_TEST_DOS_LOG"), + ): + if enabled: + os.environ[name] = "1" + import pytest + test_file = os.fspath( + Path(__file__).resolve().parents[2] / "tools" / "tests" / "test_milestones_dosboxx.py" + ) + pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"] + if args.dos_log: + pytest_args.append("-s") + return int(pytest.main(pytest_args)) + except (DosboxError, ValueError) as exc: + print(f"ferro-test: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/README.md b/tools/README.md index 3d909a5..97b141e 100644 --- a/tools/README.md +++ b/tools/README.md @@ -2,49 +2,54 @@ ## Host support -Automation currently supports **Windows 10/11 only**. It requires `uv`, QEMU -with WHPX support, and `ffmpeg.exe` on `PATH`. The Python implementation uses -portable APIs where possible, but other hosts are not supported yet. - -## Getting started +The automated DOS development environment currently supports Windows 10/11. +The host only needs `uv`. The setup command downloads the pinned DOSBox-X and +Open Watcom DOS archives, verifies their SHA-256 hashes, and installs them in the +ignored `.dosboxx/` cache. ```powershell -uv run ferro-vm start -uv run ferro-vm status +uv run ferro-dos setup --accept-watcom-license ``` -The command list lives in the CLI itself, not in this file: +Review the Open Watcom license referenced by +`tools/toolchains/dosboxx.lock.json` before accepting it. Neither downloaded +archives nor installed tools are committed. + +## General DOS environment + +`ferro-dos` provides the development entry points: ```powershell -uv run ferro-vm --help -uv run ferro-vm --help +uv run ferro-dos build +uv run ferro-dos exec "FEC.EXE --check TESTS\M6\OKLAST.FE" +uv run ferro-dos batch fec\test-dos.bat +uv run ferro-dos shell +uv run ferro-dos --help ``` -Working rules, verification gates, and DOS build traps are in `AGENTS.md`. +Every invocation creates an isolated host directory under `.dosboxx/runs/` and +mounts it as writable `C:`. The repository is mounted read-only as `R:` and the +pinned Open Watcom installation as read-only `W:`. Current compiler sources, +the standard library, and fixtures are copied to `C:\FEC`; all compilation and +execution happen there inside DOSBox-X. Successful runs are removed by default. +Use `--keep` to retain a workspace and `--show-dos` to display the DOS window. -## How it fits together +This directory-backed layout deliberately has no QEMU, disk-image, TCP-agent, +or OCR dependency. A future disk-image backend can be added without changing +the command interface. -`TCPAGENT.EXE` runs inside FreeDOS and dials out to `127.0.0.1:5558`; its wire -protocol is documented in `tcpagent/README.md`. The `ferro-vm` daemon owns that -connection and the QEMU monitor. Local commands reach the daemon over the -Windows named pipe `\\.\pipe\ferrolang-vm` — there is no controller or observer -TCP port. +## Pytest regression suite -The daemon writes an append-only structured log (`uv run ferro-vm logs`, which -uses `lnav` when installed and otherwise falls back to PowerShell `Get-Content --Wait`). It records command metadata, DOS output, exit status, transfers, and -agent lifecycle events as UTF-8 lines, and deliberately never logs raw binary -payloads or protocol hex. - -`reset` quits QEMU cleanly, restarts it, waits for FreeDOS to boot, submits the -default boot-menu Enter, and requires a TCPAGENT `PING`/`PONG` before returning. -QEMU `system_reset` is intentionally unsupported: repeated soft resets leave the -FreeDOS NE2000 packet driver stuck during initialization. - -## Standalone OCR - -`tools/qemu_ocr.py` remains available for OCRing an existing image: +`ferro-test` uses the same isolated DOSBox-X/Open Watcom environment, builds +`FEC.EXE` once, and executes all selected cases sequentially in that one DOS +instance. Pytest still reports each registered case separately. ```powershell -uv run python tools/qemu_ocr.py --image .qemu/qemu-screen.png +uv run ferro-test run --through m6 -v +uv run ferro-test run --only m6 --dos-log +uv run ferro-test --help ``` + +`--keep-failed` preserves a failed workspace, `--dos-log` prints captured DOS +output, `--trace-dos` disables per-command redirection, and `--show-dos` displays +the GUI. Working rules and DOS/Open Watcom build traps are in `AGENTS.md`. diff --git a/tools/qemu_ocr.py b/tools/qemu_ocr.py deleted file mode 100644 index 949380f..0000000 --- a/tools/qemu_ocr.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -"""Capture the QEMU VGA console and print its text with RapidOCR.""" - -from __future__ import annotations - -import argparse -import json -import logging -import subprocess -import sys -from pathlib import Path - -from rapidocr import RapidOCR - -ROOT = Path(__file__).resolve().parent.parent -DEFAULT_IMAGE = ROOT / ".qemu" / "qemu-screen.png" - - -def capture() -> Path: - subprocess.run( - ["ferro-vm", "screenshot"], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - text=True, - ) - if not DEFAULT_IMAGE.is_file(): - raise RuntimeError(f"QEMU screenshot was not created: {DEFAULT_IMAGE}") - return DEFAULT_IMAGE - - -def recognize(image: Path, min_score: float) -> list[dict[str, object]]: - logging.disable(logging.INFO) - result = RapidOCR()(image) - rows: list[dict[str, object]] = [] - if result.txts is None or result.boxes is None or result.scores is None: - return rows - for box, text, score in zip(result.boxes, result.txts, result.scores): - if float(score) < min_score: - continue - rows.append( - { - "x": int(min(point[0] for point in box)), - "y": int(min(point[1] for point in box)), - "text": text, - "score": round(float(score), 5), - } - ) - rows.sort(key=lambda row: (int(row["y"]), int(row["x"]))) - return rows - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--image", type=Path, help="OCR an existing image instead of capturing QEMU") - parser.add_argument("--min-score", type=float, default=0.5) - parser.add_argument("--json", action="store_true", help="emit locations and scores as JSON") - parser.add_argument("-o", "--output", type=Path, help="also save output as UTF-8 text") - args = parser.parse_args() - - image = args.image.resolve() if args.image else capture() - rows = recognize(image, args.min_score) - if args.json: - rendered = json.dumps({"image": str(image), "lines": rows}, ensure_ascii=False, indent=2) - else: - rendered = "\n".join(str(row["text"]) for row in rows) - if args.output: - args.output.write_text(rendered + ("\n" if rendered else ""), encoding="utf-8") - if rendered: - print(rendered) - return 0 if rows else 1 - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except (OSError, RuntimeError, subprocess.CalledProcessError) as exc: - print(f"qemu-ocr: {exc}", file=sys.stderr) - raise SystemExit(2) diff --git a/tools/tcpagent/BUILD.BAT b/tools/tcpagent/BUILD.BAT deleted file mode 100644 index fa13ec9..0000000 --- a/tools/tcpagent/BUILD.BAT +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -set WATCOM=C:\DEVEL\WATCOMC -set PATH=C:\DEVEL\WATCOMC\BINW;C:\DEVEL\WATCOMC\BINP;C:\FREEDOS\BIN -set INCLUDE=C:\DEVEL\WATCOMC\H -set EDPATH= -wmake diff --git a/tools/tcpagent/INSTALL.BAT b/tools/tcpagent/INSTALL.BAT deleted file mode 100644 index 5532775..0000000 --- a/tools/tcpagent/INSTALL.BAT +++ /dev/null @@ -1,15 +0,0 @@ -@echo off -if not exist TCPAGENT.EXE goto fail -copy TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE > nul -if errorlevel 1 goto fail -find "TCPAGENT.EXE" C:\FDAUTO.BAT > nul -if not errorlevel 1 goto done -echo SET MTCPCFG=C:\FREEDOS\MTCP.CFG>>C:\FDAUTO.BAT -echo IF EXIST C:\FREEDOS\BIN\TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE>>C:\FDAUTO.BAT -:done -echo TCPAGENT installed. -goto end -:fail -echo TCPAGENT install failed. -verify other 2>nul -:end diff --git a/tools/tcpagent/Makefile b/tools/tcpagent/Makefile deleted file mode 100644 index ceb4f0f..0000000 --- a/tools/tcpagent/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -# Place this directory at MTCP\APPS\TCPAGENT in an mTCP checkout. -tcp_c_dir = ..\..\TCPLIB -memory_model = -mc -compile_options = @WPP.RSP -tcpobjs = packet.obj arp.obj eth.obj ip.obj tcp.obj tcpsockm.obj udp.obj utils.obj timer.obj ipasm.obj trace.obj -all : tcpagent.exe -.cpp.obj : - wpp $[* $(compile_options) -tcpagent.obj: tcpagent.cpp - wpp $(compile_options) tcpagent.cpp -packet.obj: - wpp $(tcp_c_dir)\packet.cpp $(compile_options) -arp.obj: - wpp $(tcp_c_dir)\arp.cpp $(compile_options) -eth.obj: - wpp $(tcp_c_dir)\eth.cpp $(compile_options) -ip.obj: - wpp $(tcp_c_dir)\ip.cpp $(compile_options) -tcp.obj: - wpp $(tcp_c_dir)\tcp.cpp $(compile_options) -tcpsockm.obj: - wpp $(tcp_c_dir)\tcpsockm.cpp $(compile_options) -udp.obj: - wpp $(tcp_c_dir)\udp.cpp $(compile_options) -utils.obj: - wpp $(tcp_c_dir)\utils.cpp $(compile_options) -timer.obj: - wpp $(tcp_c_dir)\timer.cpp $(compile_options) -trace.obj: - wpp $(tcp_c_dir)\trace.cpp $(compile_options) -ipasm.obj: - wasm -0 -mc $(tcp_c_dir)\ipasm.asm -tcpagent.exe: tcpagent.obj $(tcpobjs) - wlink system dos option map option eliminate option stack=16384 name $@ file *.obj -clean : .symbolic - del *.obj - del *.map - del tcpagent.exe diff --git a/tools/tcpagent/README.md b/tools/tcpagent/README.md deleted file mode 100644 index 249b8c8..0000000 --- a/tools/tcpagent/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# FreeDOS resident TCP agent - -`TCPAGENT.EXE` is a foreground resident automation process. It uses the mTCP -packet-driver stack and maintains an outbound connection to the QEMU host at -`10.0.2.2:5558`. - -The Windows-only Python `ferro-vm` daemon owns that listener. It logs metadata -and decoded command output to `.qemu/ferro-vm.log`; it does not expose an -observer/controller TCP port or emit binary payloads to the log. Local host -control uses a Windows named pipe. - -## Build in FreeDOS - -1. Obtain the GPLv3 mTCP source tree (tested with the jhpyle/mTCP 2022 fork). -2. Copy this directory to `MTCP\APPS\TCPAGENT` inside that tree. -3. Set `WATCOM` for Open Watcom and run `BUILD.BAT`. -4. Run `INSTALL.BAT`; it installs the executable and adds startup lines after - the existing packet-driver setup in `C:\FDAUTO.BAT`. - -The build uses mTCP's compact memory model and Open Watcom C++16. The agent is -therefore distributed under GPLv3 when linked with mTCP. - -## Protocol - -`PING`, `READ`, `WRITE`, and `LIST` use text commands. `EXEC` captures both -stdout and stderr at the DOS handle level and returns an untruncated raw body: - -- `EXEC \n` -> `RESULT \r\n` - -Fast transfer commands are: - -- `PUT \n` -> `OK\r\n` -- `GET \n` -> `DATA \r\n` -- `HASH \n` -> `STAT \r\n` - -The host invokes them through: - -```powershell -uv run ferro-vm put host-file 'C:\DOS\FILE' -uv run ferro-vm get 'C:\DOS\FILE' host-file -``` - -## Agent-side logging - -The foreground agent prints one timestamped line per event on the VGA console -and keeps the same text in `C:\TCPAGENT.LOG`, rotating files larger than 256 KiB -to `C:\TCPAGENT.OLD`. Payloads and command output are never written to that -metadata log. - -Every command is logged with a request line and a result line carrying byte -counts and elapsed time — `EXEC`, `PUT`, `GET`, `HASH`, `LIST`, `READ`, and -`WRITE`. `PING` is deliberately excluded because `wait-ready` polls it twice a -second. Connection events (`connecting`, `connected`, `connect failed; retry N`, -`link lost`) are logged too; those are invisible to the host by definition, -since they happen when the socket is down. - -Lines are colored by writing VGA attribute bytes after `cprintf` lays out the -line: gray timestamps, cyan requests, yellow `EXEC` command text, green success, -red failure. Open Watcom's DOS `conio.h` has no `textattr()`, and ANSI escapes -are not interpreted on this FreeDOS console, so neither of the usual routes -works. Elapsed times come from the BIOS tick counter at 18.2065 Hz (~55 ms -resolution). - -## Long commands - -mTCP is only driven when the agent calls it, and `system()` freezes the agent -for the entire child command. So during a long `EXEC` the DOS side is mute: it -cannot answer, cannot acknowledge, cannot report progress. Silence therefore -proves nothing about whether the command is healthy. - -The host must not read that silence as failure. `ferro-vm exec` waits on -QEMU's own view of the guest instead: `info blockstats` keeps counting while -the agent is frozen, and `idle_time_ns` distinguishes a slow command from a -stuck one. See `--idle-timeout` and `--hard-timeout` in `ferro-vm exec --help`. - -When the host does decide to stop a command it injects Ctrl+C through the QEMU -monitor, then answers COMMAND.COM's `Terminate batch file (Y/N/A)?` prompt. -That is a request, not a guarantee: Ctrl+C only lands at a DOS break check, and -with `BREAK=OFF` (the FreeDOS default in `C:\FDCONFIG.SYS`) a compute-bound -child whose output we redirected to a file may never reach one. The host keeps -collecting the result either way rather than abandoning a stream that still -owes it a `RESULT`. - -Adding `BREAK=ON` to `C:\FDCONFIG.SYS` would make DOS check on every system -call and so make Ctrl+C reliable, at a small cost to every DOS call. - -## Rebuilding inside the VM - -`REBUILD.BAT` compiles and installs the agent in a single `exec`. `BUILD.BAT` -only runs `wmake` in the current directory, which is not where a host-driven -`ferro-vm exec` starts. - -```powershell -uv run ferro-vm put tools/tcpagent/tcpagent.cpp 'C:\MTSRC\MTCP\APPS\TCPAGENT\TCPAGENT.CPP' -uv run ferro-vm put tools/tcpagent/REBUILD.BAT 'C:\REBUILD.BAT' -uv run ferro-vm exec 'C:\REBUILD.BAT' -uv run ferro-vm reset -``` - -The reset is required: the running agent holds the old image in memory, and -`C:\FDAUTO.BAT` starts it at boot. diff --git a/tools/tcpagent/REBUILD.BAT b/tools/tcpagent/REBUILD.BAT deleted file mode 100644 index 989d172..0000000 --- a/tools/tcpagent/REBUILD.BAT +++ /dev/null @@ -1,22 +0,0 @@ -@echo off -rem Rebuild TCPAGENT.EXE from C:\MTSRC and install it, in one EXEC. -rem BUILD.BAT only runs wmake in the current directory, which is not where a -rem host-driven `ferro-vm exec` starts. Copy this to C:\ and run it by path. -rem The running agent keeps the old image in memory, so reset the VM afterwards: -rem uv run ferro-vm reset -C: -cd C:\MTSRC\MTCP\APPS\TCPAGENT -set WATCOM=C:\DEVEL\WATCOMC -set PATH=C:\DEVEL\WATCOMC\BINW;C:\DEVEL\WATCOMC\BINP;C:\FREEDOS\BIN -set INCLUDE=C:\DEVEL\WATCOMC\H -set EDPATH= -if exist TCPAGENT.OBJ del TCPAGENT.OBJ -if exist TCPAGENT.EXE del TCPAGENT.EXE -wmake -if not exist TCPAGENT.EXE goto fail -copy /Y TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE -echo BUILD-OK -goto end -:fail -echo BUILD-FAILED -:end diff --git a/tools/tcpagent/WPP.RSP b/tools/tcpagent/WPP.RSP deleted file mode 100644 index 08000f4..0000000 --- a/tools/tcpagent/WPP.RSP +++ /dev/null @@ -1,11 +0,0 @@ --0 --mc --DCFG_H="tcpagent.cfg" --oh --os --s --zp2 --zpw --we --i=..\..\TCPINC --i=..\..\INCLUDE diff --git a/tools/tcpagent/tcpagent.cfg b/tools/tcpagent/tcpagent.cfg deleted file mode 100644 index b962291..0000000 --- a/tools/tcpagent/tcpagent.cfg +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef CONFIG_H -#define CONFIG_H -#define MTCP_PROGRAM_NAME "tcpagent" -#include "Global.Cfg" -#define COMPILE_ARP -#define IP_FRAGMENTS_ON -#define COMPILE_UDP -#define COMPILE_TCP -#define COMPILE_ICMP -#undef TCP_MAX_SOCKETS -#define TCP_MAX_SOCKETS (1) -#endif diff --git a/tools/tcpagent/tcpagent.cpp b/tools/tcpagent/tcpagent.cpp deleted file mode 100644 index 574316d..0000000 --- a/tools/tcpagent/tcpagent.cpp +++ /dev/null @@ -1,303 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "types.h" -#include "trace.h" -#include "utils.h" -#include "packet.h" -#include "arp.h" -#include "udp.h" -#include "tcp.h" -#include "tcpsockm.h" - -#define LINE_SIZE 9000 -#define CHUNK_SIZE 4096 -#define SERVER_PORT 5558 -#define LOCAL_PORT 2058 -#define RECV_SIZE 12288 - -static char linebuf[LINE_SIZE]; -static unsigned char data[CHUNK_SIZE+1]; -static TcpSocket *socketp; -static volatile uint8_t stop_requested; -static FILE *put_file; -static unsigned long put_remaining; -static unsigned long put_started; -static char put_path[260]; - -static unsigned long ticks(void) { - long value=0; - _bios_timeofday(_TIME_GETCLOCK,&value); - return (unsigned long)value; -} -/* The BIOS tick is 18.2065 Hz, so one tick is 5.49254 hundredths of a second. - 549/100 keeps the error under 0.05% and cannot overflow 32 bits for a delta - up to a full day (1573040 ticks * 549 fits). Plain delta/18 ran 1.1% fast. */ -static void elapsed_text(unsigned long started,char *out) { - unsigned long now=ticks(),delta=now>=started?now-started:now+(1573040UL-started); - unsigned long hundredths=delta*549UL/100UL; - sprintf(out,"%lu.%02lus",hundredths/100UL,hundredths%100UL); -} - -/* Open Watcom's DOS conio has no textattr()/textcolor() -- it offers only - cprintf/cputs/getch and friends -- and ANSI escapes are not interpreted on - the installed FreeDOS console. So let cprintf lay the line out (it scrolls - correctly) and then repaint the attribute bytes of the cells it just wrote. - The cursor sits at column 0 of the row after the line, which is what lets us - find those cells without tracking scrolling ourselves. */ -#define TIMESTAMP_WIDTH 9 -static void colorize(unsigned total,int attr) { - unsigned char __far *vram; unsigned cols,used,row,col,start,k; - if(*(unsigned char __far *)MK_FP(0x0040,0x0049)==7) return; /* MDA: no color */ - cols=*(unsigned __far *)MK_FP(0x0040,0x004A); - if(cols<40||cols>132) cols=80; - used=(total+cols-1)/cols; if(!used) used=1; - row=*(unsigned char __far *)MK_FP(0x0040,0x0051); - if(row262144L){remove("C:\\TCPAGENT.OLD");rename("C:\\TCPAGENT.LOG","C:\\TCPAGENT.OLD");} - f=fopen("C:\\TCPAGENT.LOG","a"); - if(f){fputs("--- TCPAGENT start ---\n",f);fclose(f);} -} -/* PUT completes either here in command_put (zero length) or in the receive loop - once the body arrives, so keep the one line both paths emit in one place. */ -static void put_finished(void) { - char elapsed[24]; elapsed_text(put_started,elapsed); - log_line(0x0A,"< PUT %s OK %s",put_path,elapsed); -} - -void __interrupt __far ctrl_break(void) { stop_requested=1; } -void __interrupt __far ctrl_c(void) { stop_requested=1; } - -static void drive(void) { - PACKET_PROCESS_MULT(5); - Arp::driveArp(); - Tcp::drivePackets(); -} - -static int send_all(const void *buffer, unsigned length) { - const uint8_t *p=(const uint8_t *)buffer; - unsigned sent=0; - int rc; - while(sentsend((uint8_t *)(p+sent),length-sent); - if(rc>0) sent+=(unsigned)rc; - else if(rc<0 || socketp->isRemoteClosed()) return -1; - } - return sent==length ? 0 : -1; -} - -static void write_text(const char *s) { send_all(s,strlen(s)); } -static int hexval(int c) { - if(c>='0'&&c<='9') return c-'0'; - if(c>='A'&&c<='F') return c-'A'+10; - if(c>='a'&&c<='f') return c-'a'+10; - return -1; -} -static int decode_hex(const char *src,unsigned char *dst,int cap) { - int n=0,hi,lo; - while(*src&&src[1]) { - if(n>=cap) return -1; - hi=hexval((unsigned char)src[0]); lo=hexval((unsigned char)src[1]); - if(hi<0||lo<0) return -1; - dst[n++]=(unsigned char)((hi<<4)|lo); src+=2; - } - return *src ? -1 : n; -} -static void write_hex(const unsigned char *src,unsigned count) { - static const char digits[]="0123456789ABCDEF"; - char out[256]; unsigned i,n; - while(count) { - n=count>sizeof(out)/2 ? sizeof(out)/2 : count; - for(i=0;i>4]; out[i*2+1]=digits[src[i]&15]; } - send_all(out,n*2); src+=n; count-=n; - } -} -static void ok_data(const unsigned char *src,unsigned count) { - write_text("OK "); write_hex(src,count); write_text("\r\n"); -} -static void error_text(const char *s) { - write_text("ERR "); write_hex((const unsigned char *)s,strlen(s)); write_text("\r\n"); -} -static int decode_path(const char *hex,char *path,int cap) { - int n=decode_hex(hex,(unsigned char *)path,cap-1); - if(n<0) return 0; path[n]='\0'; return 1; -} -static void command_read(char *args) { - char path[260],*off=strchr(args,' '); FILE *f; long pos; size_t count; int eof; - if(!off){log_line(0x0C,"< READ ERR missing offset");error_text("READ requires path and offset");return;} *off++='\0'; - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< READ ERR bad path encoding");error_text("Invalid path encoding");return;} - pos=atol(off); log_line(0x0B,"> READ %s @%ld",path,pos); - f=fopen(path,"rb"); if(!f){log_line(0x0C,"< READ ERR cannot open");error_text("Cannot open file");return;} - if(fseek(f,pos,SEEK_SET)){fclose(f);log_line(0x0C,"< READ ERR cannot seek");error_text("Cannot seek file");return;} - count=fread(data,1,CHUNK_SIZE,f); eof=count WRITE %s %c %dB",path,mode[0]=='A'?'A':'T',count); - f=fopen(path,mode[0]=='A'?"ab":"wb"); if(!f){log_line(0x0C,"< WRITE ERR cannot open");error_text("Cannot write file");return;} - if(count&&fwrite(data,1,count,f)!=(size_t)count){fclose(f);log_line(0x0C,"< WRITE ERR short write");error_text("Short write");return;} - fclose(f); log_line(0x0A,"< WRITE OK"); ok_data((const unsigned char *)"",0); -} -static void command_put(char *args) { - char path[260],*length_text=strchr(args,' '); - if(!length_text){log_line(0x0C,"< PUT ERR missing length");error_text("PUT requires path and length");return;} - *length_text++='\0'; - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< PUT ERR bad path encoding");error_text("Invalid path encoding");return;} - put_remaining=strtoul(length_text,0,10); strcpy(put_path,path); put_started=ticks(); - log_line(0x0B,"> PUT %s %luB",put_path,put_remaining); - put_file=fopen(path,"wb"); - if(!put_file){put_remaining=0;log_line(0x0C,"< PUT %s ERR cannot open",put_path);error_text("Cannot write file");return;} - if(!put_remaining){fclose(put_file);put_file=0;put_finished();ok_data((const unsigned char *)"",0);} -} -static void command_get(char *args) { - char path[260],elapsed[24]; FILE *f; long length; size_t count; unsigned long started=ticks(); - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< GET ERR bad path encoding");error_text("Invalid path encoding");return;} - log_line(0x0B,"> GET %s",path); - f=fopen(path,"rb"); if(!f){log_line(0x0C,"< GET %s ERR cannot open",path);error_text("Cannot open file");return;} - fseek(f,0,SEEK_END); length=ftell(f); fseek(f,0,SEEK_SET); - sprintf(linebuf,"DATA %ld\r\n",length); write_text(linebuf); - while((count=fread(data,1,CHUNK_SIZE,f))>0) if(send_all(data,count)<0)break; - fclose(f); elapsed_text(started,elapsed); - log_line(0x0A,"< GET OK %ldB %s",length,elapsed); -} -static void command_hash(char *args) { - char path[260],elapsed[24]; FILE *f; size_t count; unsigned i; - unsigned long length=0,hash=2166136261UL,started=ticks(); - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< HASH ERR bad path encoding");error_text("Invalid path encoding");return;} - log_line(0x0B,"> HASH %s",path); - f=fopen(path,"rb"); if(!f){log_line(0x0C,"< HASH %s ERR cannot open",path);error_text("Cannot open file");return;} - while((count=fread(data,1,CHUNK_SIZE,f))>0){length+=(unsigned long)count;for(i=0;i LIST %s",path); - strcpy(pattern,path); if(pattern[0]&&pattern[strlen(pattern)-1]!='\\') strcat(pattern,"\\"); strcat(pattern,"*.*"); - rc=_dos_findfirst(pattern,_A_NORMAL|_A_RDONLY|_A_HIDDEN|_A_SYSTEM|_A_SUBDIR|_A_ARCH,&found); - while(rc==0) { char entry[100]; int len; - if(strcmp(found.name,".")&&strcmp(found.name,"..")) { sprintf(entry,"%s\t%lu\t%s\n",found.name,found.size,(found.attrib&_A_SUBDIR)?"DIR":"FILE"); len=strlen(entry); if(used+(unsigned)len>=sizeof(output)) {truncated=1;break;} memcpy(output+used,entry,len); used+=(unsigned)len; ++entries; } - rc=_dos_findnext(&found); - } - log_line(truncated?0x0E:0x0A,"< LIST %u entries%s",entries,truncated?" (truncated)":""); - ok_data((unsigned char *)output,used); -} -static void command_exec(char *args) { - char command[700],elapsed[24]; const char *tmp="C:\\PIEXEC.TMP"; FILE *f; - int n,code=-1,fd=-1,save1=-1,save2=-1; long length=0; size_t count; unsigned long started; - n=decode_hex(args,(unsigned char *)command,sizeof(command)-1); if(n<0){error_text("Invalid command encoding");return;} command[n]='\0'; - started=ticks(); log_line(0x0E,"> EXEC %.640s",command); - fflush(stdout); fflush(stderr); - save1=dup(1); save2=dup(2); - fd=open(tmp,O_CREAT|O_TRUNC|O_WRONLY|O_BINARY,S_IREAD|S_IWRITE); - if(save1<0||save2<0||fd<0||dup2(fd,1)<0||dup2(fd,2)<0) { - if(fd>=0)close(fd); - if(save1>=0){dup2(save1,1);close(save1);} - if(save2>=0){dup2(save2,2);close(save2);} - log_line(0x0C,"< EXEC ERR redirect failed"); error_text("Cannot capture command output"); return; - } - close(fd); code=system(command); fflush(stdout); fflush(stderr); - dup2(save1,1); dup2(save2,2); close(save1); close(save2); - f=fopen(tmp,"rb"); - if(f){fseek(f,0,SEEK_END);length=ftell(f);fseek(f,0,SEEK_SET);} - elapsed_text(started,elapsed); - log_line(code?0x0C:0x0A,"< EXEC exit=%d %ldB %s",code,length,elapsed); - sprintf(linebuf,"RESULT %d %ld 0\r\n",code,length); write_text(linebuf); - while(f&&(count=fread(data,1,CHUNK_SIZE,f))>0)if(send_all(data,count)<0)break; - if(f)fclose(f); remove(tmp); -} -static void process_line(char *line) { - char *cmd=line,*args=strchr(line,' '); if(args)*args++='\0';else args=cmd+strlen(cmd); - /* PING is deliberately not logged: wait-ready polls it twice a second. */ - if(!strcmp(cmd,"PING"))ok_data((const unsigned char *)"PONG",4); - else if(!strcmp(cmd,"READ"))command_read(args); - else if(!strcmp(cmd,"WRITE"))command_write(args); - else if(!strcmp(cmd,"PUT"))command_put(args); - else if(!strcmp(cmd,"GET"))command_get(args); - else if(!strcmp(cmd,"HASH"))command_hash(args); - else if(!strcmp(cmd,"LIST"))command_list(args); - else if(!strcmp(cmd,"EXEC"))command_exec(args); - else if(!strcmp(cmd,"QUIT")){log_line(0x07,"* QUIT received");ok_data((const unsigned char *)"BYE",3);stop_requested=1;} - else { char message[96]; sprintf(message,"Unknown command: %.70s",cmd); log_line(0x0C,"< ERR %s",message); error_text(message); } -} -static int connect_host(void) { - IpAddr_t host={10,0,2,2}; int8_t rc; - socketp=TcpSocketMgr::getSocket(); if(!socketp)return -1; - socketp->setRecvBuffer(RECV_SIZE); - rc=socketp->connect(LOCAL_PORT,host,SERVER_PORT,10000); - while(rc==0&&!socketp->isConnectComplete()&&!socketp->isRemoteClosed()&&!stop_requested)drive(); - if(socketp->isRemoteClosed()){TcpSocketMgr::freeSocket(socketp);socketp=0;return -1;} - return 0; -} -int main(void) { - int used,rc; uint16_t key; unsigned attempts=0; - init_log(); - log_line(0x0B,"* TCPAGENT starting; Alt-X returns to DOS"); - if(Utils::parseEnv()!=0){log_line(0x0C,"* MTCP configuration error");return 2;} - if(Utils::initStack(1,TCP_SOCKET_RING_SIZE,ctrl_break,ctrl_c)){log_line(0x0C,"* TCP stack initialization error");return 3;} - while(!stop_requested) { - if(!attempts)log_line(0x07,"* connecting 10.0.2.2:%u",SERVER_PORT); - if(connect_host()!=0){unsigned long spins=0;++attempts;if(attempts==1||attempts%10==0)log_line(0x0C,"* connect failed; retry %u",attempts);while(spins++<60000UL&&!stop_requested)drive();continue;} - attempts=0; log_line(0x0A,"* connected; host automation owns console"); - write_text("TCPAGENT READY\r\n"); used=0; - while(!stop_requested&&!socketp->isRemoteClosed()) { - drive(); rc=socketp->recv((uint8_t *)data,CHUNK_SIZE); - if(rc<0)break; - for(int i=0;i>8)==45)stop_requested=1;} - } - socketp->close(); TcpSocketMgr::freeSocket(socketp); socketp=0; - if(!stop_requested)log_line(0x0C,"* link lost; retrying"); - } - log_line(0x07,"* stopped; returning to DOS"); - Utils::endStack(); return 0; -} diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py new file mode 100644 index 0000000..c386b62 --- /dev/null +++ b/tools/tests/test_milestones_dosboxx.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import os +import warnings + +import pytest + +from ferrolang_vm.dosboxx import SuiteRun, run_suite +from ferrolang_vm.registry import MAX_MILESTONE, all_cases, milestone_number +from ferrolang_vm.suite import Case + +ONLY = os.environ.get("FERRO_TEST_ONLY") +CASES = all_cases( + through=milestone_number(os.environ.get("FERRO_TEST_THROUGH", f"m{MAX_MILESTONE}")), + only=milestone_number(ONLY) if ONLY else None, +) + + +@pytest.fixture(scope="session") +def suite_run() -> SuiteRun: + run = run_suite( + CASES, + keep=os.environ.get("FERRO_TEST_KEEP_FAILED") == "1", + show_dos=os.environ.get("FERRO_TEST_SHOW_DOS") == "1", + trace_dos=os.environ.get("FERRO_TEST_TRACE_DOS") == "1", + ) + yield run + if os.environ.get("FERRO_TEST_DOS_LOG") == "1": + console = run.root / "CONSOLE.LOG" + if console.is_file(): + print(console.read_text(encoding="utf-8", errors="replace")) + run.cleanup() + + +def test_compiler_build(suite_run: SuiteRun) -> None: + assert suite_run.result() == "PASS", suite_run.log() + + +@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id) +def test_milestone_case(case: Case, suite_run: SuiteRun) -> None: + if suite_run.result() != "PASS": + pytest.skip("compiler build failed") + result = suite_run.result(case) + err = suite_run.err(case) + if result == "PASS" and err: + warning_lines = [line for line in err.splitlines() if "warning" in line.lower()] + if warning_lines: + warnings.warn("\n".join(warning_lines), stacklevel=1) + assert result == "PASS", ( + f"DOS command: {case.command}\nExpected success: {case.expect_success}\n" + f"{suite_run.log(case)}\n{err}" + ) diff --git a/tools/toolchains/dosboxx.lock.json b/tools/toolchains/dosboxx.lock.json new file mode 100644 index 0000000..08b1123 --- /dev/null +++ b/tools/toolchains/dosboxx.lock.json @@ -0,0 +1,23 @@ +{ + "schema": 1, + "dosboxx": { + "version": "2026.08.02", + "url": "https://github.com/joncampbell123/dosbox-x/releases/download/dosbox-x-v2026.08.02/dosbox-x-vsbuild-win64-2026.08.02-portable.zip", + "sha256": "ca28208f5fee25a74caf3a02cc0189c7f7943a42ce37c02848f5fc03450a96cf", + "executable": "bin/x64/Release/dosbox-x.exe" + }, + "open_watcom": { + "version": "2026-08-01-Build", + "url": "https://github.com/open-watcom/open-watcom-v2/releases/download/2026-08-01-Build/open-watcom-2_0-c-dos.exe", + "sha256": "80db4ab340f382e59bf3d396280576ec837964c2ef00e8ddd3b2b3724ab63edf", + "required": [ + "binw/wcl.exe", + "binw/wcl386.exe", + "binp/wlink.exe", + "h/stdio.h", + "lib286/dos/clibl.lib", + "lib386/dos/clib3r.lib", + "license.txt" + ] + } +} diff --git a/uv.lock b/uv.lock index c8f60c6..500d600 100644 --- a/uv.lock +++ b/uv.lock @@ -2,152 +2,6 @@ version = 1 revision = 1 requires-python = ">=3.12" -[[package]] -name = "antlr4-python3-runtime" -version = "4.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034 } - -[[package]] -name = "certifi" -version = "2026.7.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456 }, - { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530 }, - { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200 }, - { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222 }, - { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951 }, - { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801 }, - { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070 }, - { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110 }, - { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836 }, - { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712 }, - { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977 }, - { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207 }, - { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562 }, - { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507 }, - { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551 }, - { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700 }, - { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584 }, - { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359 }, - { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464 }, - { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676 }, - { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473 }, - { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156 }, - { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246 }, - { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660 }, - { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354 }, - { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638 }, - { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583 }, - { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038 }, - { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677 }, - { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491 }, - { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196 }, - { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660 }, - { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618 }, - { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362 }, - { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755 }, - { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295 }, - { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856 }, - { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318 }, - { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897 }, - { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848 }, - { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163 }, - { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823 }, - { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458 }, - { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717 }, - { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111 }, - { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128 }, - { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240 }, - { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282 }, - { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597 }, - { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376 }, - { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715 }, - { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848 }, - { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521 }, - { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054 }, - { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580 }, - { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325 }, - { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175 }, - { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123 }, - { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682 }, - { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826 }, - { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861 }, - { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758 }, - { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950 }, - { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329 }, - { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137 }, - { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820 }, - { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504 }, - { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087 }, - { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269 }, - { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766 }, - { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814 }, - { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074 }, - { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476 }, - { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115 }, - { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048 }, - { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997 }, - { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014 }, - { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174 }, - { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361 }, - { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143 }, - { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086 }, - { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231 }, - { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546 }, - { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033 }, - { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045 }, - { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866 }, - { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932 }, - { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320 }, - { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174 }, - { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126 }, - { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441 }, - { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742 }, - { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298 }, - { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500 }, - { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888 }, - { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243 }, - { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871 }, - { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580 }, - { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807 }, - { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083 }, - { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317 }, - { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173 }, - { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960 }, - { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186 }, - { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947 }, - { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909 }, - { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467 }, - { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057 }, - { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930 }, - { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822 }, - { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037 }, - { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097 }, - { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166 }, - { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821 }, - { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529 }, - { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348 }, - { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234 }, - { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917 }, - { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846 }, - { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216 }, - { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764 }, - { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318 }, - { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658 }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -157,185 +11,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] -[[package]] -name = "colorlog" -version = "6.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239 }, -] - [[package]] name = "ferrolang" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "onnxruntime" }, - { name = "rapidocr" }, + { name = "pytest" }, ] [package.metadata] -requires-dist = [ - { name = "onnxruntime", specifier = ">=1.28.0" }, - { name = "rapidocr", specifier = ">=3.9.2" }, -] +requires-dist = [{ name = "pytest", specifier = ">=9.0.0" }] [[package]] -name = "flatbuffers" -version = "25.12.19" +name = "iniconfig" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661 }, -] - -[[package]] -name = "idna" -version = "3.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, -] - -[[package]] -name = "numpy" -version = "2.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693 }, - { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109 }, - { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202 }, - { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736 }, - { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696 }, - { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264 }, - { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396 }, - { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044 }, - { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817 }, - { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674 }, - { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131 }, - { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595 }, - { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845 }, - { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880 }, - { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264 }, - { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566 }, - { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995 }, - { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511 }, - { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609 }, - { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204 }, - { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532 }, - { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725 }, - { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180 }, - { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878 }, - { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922 }, - { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168 }, - { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501 }, - { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701 }, - { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065 }, - { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031 }, - { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028 }, - { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627 }, - { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414 }, - { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967 }, - { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874 }, - { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276 }, - { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154 }, - { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909 }, - { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685 }, - { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181 }, - { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085 }, - { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971 }, - { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306 }, - { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274 }, - { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846 }, - { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892 }, - { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309 }, - { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850 }, - { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664 }, - { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749 }, - { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495 }, - { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696 }, - { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324 }, - { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466 }, - { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947 }, - { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331 }, - { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336 }, - { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387 }, - { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096 }, - { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730 }, - { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686 }, - { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727 }, - { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775 }, - { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559 }, - { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103 }, -] - -[[package]] -name = "omegaconf" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502 }, -] - -[[package]] -name = "onnxruntime" -version = "1.28.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flatbuffers" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "protobuf" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/f8/dcbe7700dca82fa540035abd3c868fe5ad0f86af00b9a3db7c2e27d15c7d/onnxruntime-1.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:26ff0fdd06efb6c155bae95387a09db1a2be89c7a03e4d0bffd5a171cc2826da", size = 19141362 }, - { url = "https://files.pythonhosted.org/packages/28/5b/1d77e62097fdbe07e2dc827f389b1c4c0c275f6fab0369a8f46d2461af27/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e81a23df16e7acb9d51b06d30cc098e49315ef9180f97bc2221d167b4b04d9c", size = 17050628 }, - { url = "https://files.pythonhosted.org/packages/95/df/5486ab03e9be288d5268867054c8b04bebcf95bfd12e801c05cc67703dab/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a83bdb70d143cede762b677789bf2a7acca54b3fb82565601d5c30695aa933c", size = 19214257 }, - { url = "https://files.pythonhosted.org/packages/3e/3b/986ca67c274932ba9ac5332fb10de56f643dfd433c74e33f8ae8f847cf24/onnxruntime-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:c35064f9b3c43c81c5d5d282091401d0f1ff22796d93ccade4ea2ece5e137ab8", size = 13755036 }, - { url = "https://files.pythonhosted.org/packages/1d/46/059dba81d46c6ba88e0c2d1c64321ac8098847d678423300a183d42ecbd6/onnxruntime-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:e02feeb0165c5f13b4cc954738078d59b90128516ac12b671ee24a530242bf02", size = 13454462 }, - { url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759 }, - { url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339 }, - { url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329 }, - { url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033 }, - { url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175 }, - { url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307 }, - { url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954 }, - { url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748 }, - { url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950 }, - { url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924 }, - { url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738 }, - { url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117 }, - { url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518 }, - { url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976 }, -] - -[[package]] -name = "opencv-python" -version = "5.0.0.93" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443 }, - { url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755 }, - { url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064 }, - { url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711 }, - { url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576 }, - { url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032 }, - { url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734 }, - { url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345 }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, ] [[package]] @@ -348,280 +41,35 @@ wheels = [ ] [[package]] -name = "pillow" -version = "12.3.0" +name = "pluggy" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } wheels = [ - { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969 }, - { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323 }, - { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838 }, - { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830 }, - { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383 }, - { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934 }, - { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684 }, - { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137 }, - { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267 }, - { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684 }, - { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487 }, - { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433 }, - { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889 }, - { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109 }, - { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736 }, - { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129 }, - { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562 }, - { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439 }, - { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287 }, - { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691 }, - { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185 }, - { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736 }, - { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435 }, - { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262 }, - { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344 }, - { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131 }, - { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757 }, - { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962 }, - { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171 }, - { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116 }, - { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209 }, - { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707 }, - { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995 }, - { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503 }, - { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956 }, - { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855 }, - { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642 }, - { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281 }, - { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716 }, - { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125 }, - { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939 }, - { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506 }, - { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063 }, - { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549 }, - { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331 }, - { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370 }, - { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147 }, - { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659 }, - { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439 }, - { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577 }, - { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394 }, - { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375 }, - { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048 }, - { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006 }, - { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509 }, - { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167 }, - { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237 }, - { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047 }, - { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440 }, - { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895 }, - { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384 }, - { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537 }, - { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] [[package]] -name = "protobuf" -version = "7.35.1" +name = "pygments" +version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } wheels = [ - { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226 }, - { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847 }, - { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030 }, - { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130 }, - { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945 }, - { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996 }, - { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659 }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, ] [[package]] -name = "pyclipper" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676 }, - { url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458 }, - { url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235 }, - { url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388 }, - { url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169 }, - { url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619 }, - { url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342 }, - { url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839 }, - { url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142 }, - { url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789 }, - { url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817 }, - { url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007 }, - { url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167 }, - { url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966 }, - { url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216 }, - { url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198 }, - { url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951 }, - { url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782 }, - { url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880 }, - { url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308 }, - { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608 }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, -] - -[[package]] -name = "rapidocr" -version = "3.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorlog" }, - { name = "numpy" }, - { name = "omegaconf" }, - { name = "opencv-python" }, - { name = "pillow" }, - { name = "pyclipper" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "shapely" }, - { name = "six" }, - { name = "tqdm" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/ed/0ee9b9281986974be9d2406ae0134c8d7c91d2fc613f16ffda9701eeda6f/rapidocr-3.9.2-py3-none-any.whl", hash = "sha256:04d6b8d151f823d930bd91910555f57bea897c0c44fa6794267b94cf9c1ef9a0", size = 27275208 }, -] - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075 }, -] - -[[package]] -name = "shapely" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550 }, - { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556 }, - { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308 }, - { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844 }, - { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842 }, - { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714 }, - { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745 }, - { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861 }, - { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644 }, - { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887 }, - { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931 }, - { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855 }, - { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960 }, - { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851 }, - { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890 }, - { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151 }, - { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130 }, - { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802 }, - { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460 }, - { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223 }, - { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760 }, - { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078 }, - { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178 }, - { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756 }, - { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290 }, - { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463 }, - { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145 }, - { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806 }, - { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803 }, - { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301 }, - { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247 }, - { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019 }, - { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137 }, - { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884 }, - { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320 }, - { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931 }, - { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406 }, - { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511 }, - { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607 }, - { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682 }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - -[[package]] -name = "tqdm" -version = "4.70.0" +name = "pytest" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438 } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184 }, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087 }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 }, ]