diff --git a/.github/workflows/build-executables.yml b/.github/workflows/build-executables.yml index 4a93b03..b9e06cc 100644 --- a/.github/workflows/build-executables.yml +++ b/.github/workflows/build-executables.yml @@ -14,15 +14,15 @@ jobs: # 1. Build Windows Executable # build-windows-exe: - name: 'script.exe creation using python 3.11 on Windows' - runs-on: 'windows-latest' + name: "script.exe creation using python 3.11 on Windows" + runs-on: "windows-latest" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: - version: "0.6.2" - python-version: '3.11' + version: "latest" + python-version: "3.11" enable-cache: true cache-dependency-glob: "uv.lock" - name: "Install dependencies" @@ -49,15 +49,16 @@ jobs: # Note that this produces an ARM based build, so won't work on older # intel based Macs. build-macos-executable: - name: 'script.app creation using python 3.11 on MacOS' - runs-on: 'macos-latest' + name: "script.app creation using python 3.11 on MacOS" + # we use 13 to build x86_64 apps + runs-on: "macos-13" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: - version: "0.6.2" - python-version: '3.11' + version: "latest" + python-version: "3.11" enable-cache: true cache-dependency-glob: "uv.lock" - name: "Install dependencies" @@ -65,10 +66,15 @@ jobs: uv run python -VV uv run python -m site uv sync + # notes: + # --onedir rather than --onefile is required for app bundle + # --target-arch=x86_64 is required for python-mip 1.15.0 - revisit once 1.16.0 is properly released + # See https://groups.google.com/g/python-mip/c/0R2NC_URD6M - name: "Build MacOS package" run: | - uv run python -m eel script.py web -n strat-select-macos --additional-hooks-dir=./pyinstallerhooks/ --onefile --windowed + uv run python -m eel script.py web -n strat-select-macos --additional-hooks-dir=./pyinstallerhooks/ --onedir --windowed --target-arch=x86_64 mkdir -p target/release + ls -l dist/strat-select-macos.app mv dist/strat-select-macos target/release cd dist && zip -r ../target/release/strat-select-macos.app.zip strat-select-macos.app ls -l ../target/release/ @@ -84,15 +90,15 @@ jobs: # 3. Build Linux Executable # build-linux-executable: - name: 'executable creation using python 3.11 on Linux' - runs-on: 'ubuntu-24.04' + name: "executable creation using python 3.11 on Linux" + runs-on: "ubuntu-24.04" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: - version: "0.6.2" - python-version: '3.11' + version: "latest" + python-version: "3.11" enable-cache: true cache-dependency-glob: "uv.lock" - name: "Install dependencies" diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index af78269..0000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,52 +0,0 @@ ---- -name: tests - -permissions: - contents: read - pull-requests: write - -on: [push] - -jobs: - build: - runs-on: ubuntu-24.04 - strategy: - matrix: - python-version: ["3.11", "3.12"] - - steps: - - uses: actions/checkout@v4 - - name: Install uv - uses: astral-sh/setup-uv@v5 - with: - version: "0.6.2" - python-version: ${{ matrix.python-version }} - enable-cache: true - cache-dependency-glob: "uv.lock" - - name: Report versions etc dependencies - run: | - uv run python -VV - uv run python -m site - uv sync --dev - - name: Test with pytest - run: | - uv run pytest -v --cov --cov-report=xml --cov-report=html --junitxml=junit/test-results-${{ matrix.python-version }}.xml - - name: Lint with Ruff - run: | - uv run ruff check --output-format=github --target-version=py312 - continue-on-error: true - - name: Reformat with Ruff (diff only) - run: | - uv run ruff format --diff --line-length=120 --target-version=py312 - continue-on-error: true - - name: Upload pytest test results - uses: actions/upload-artifact@v4 - with: - name: pytest-results-${{ matrix.python-version }} - path: | - junit/test-results-${{ matrix.python-version }}.xml - htmlcov/ - # default retention is 90 - retention-days: 30 - # Use always() to always run this step to publish test results when there are test failures - if: ${{ always() }} diff --git a/fixtures/candidates.csv b/fixtures/candidates.csv deleted file mode 100644 index b847a5c..0000000 --- a/fixtures/candidates.csv +++ /dev/null @@ -1,151 +0,0 @@ -nationbuilder_id,first_name,last_name,email,mobile_number,primary_address1,primary_address2,primary_city,primary_zip,gender,age_bracket,geo_bucket,edu_level -p0,first_name0,last_name0,email0,mobile_number0,primary_address10,primary_address20,primary_city0,primary_zip0,Male,30-44,South Scotland,Level 4 and above -p1,first_name1,last_name1,email1,mobile_number1,primary_address11,primary_address21,primary_city1,primary_zip1,Female,16-29,Lothian,Level 1 -p2,first_name2,last_name2,email2,mobile_number2,primary_address12,primary_address22,primary_city2,primary_zip2,Female,45-59,West Scotland,Level 2 or 3 -p3,first_name3,last_name3,email3,mobile_number3,primary_address13,primary_address23,primary_city3,primary_zip3,Male,30-44,Glasgow,Level 4 and above -p4,first_name4,last_name4,email4,mobile_number4,primary_address14,primary_address24,primary_city4,primary_zip4,Female,45-59,South Scotland,Level 1 -p5,first_name5,last_name5,email5,mobile_number5,primary_address15,primary_address25,primary_city5,primary_zip5,Male,16-29,Mid Scotland and Fife,Level 2 or 3 -p6,first_name6,last_name6,email6,mobile_number6,primary_address16,primary_address26,primary_city6,primary_zip6,Female,30-44,Lothian,Level 1 -p7,first_name7,last_name7,email7,mobile_number7,primary_address17,primary_address27,primary_city7,primary_zip7,Male,30-44,Mid Scotland and Fife,Level 4 and above -p8,first_name8,last_name8,email8,mobile_number8,primary_address18,primary_address28,primary_city8,primary_zip8,Female,45-59,Mid Scotland and Fife,Level 1 -p9,first_name9,last_name9,email9,mobile_number9,primary_address19,primary_address29,primary_city9,primary_zip9,Male,60+,North East Scotland,Level 1 -p10,first_name10,last_name10,email10,mobile_number10,primary_address110,primary_address210,primary_city10,primary_zip10,Male,45-59,Mid Scotland and Fife,Level 4 and above -p11,first_name11,last_name11,email11,mobile_number11,primary_address111,primary_address211,primary_city11,primary_zip11,Male,16-29,Central Scotland,Level 2 or 3 -p12,first_name12,last_name12,email12,mobile_number12,primary_address112,primary_address212,primary_city12,primary_zip12,Male,45-59,Mid Scotland and Fife,No qualifications -p13,first_name13,last_name13,email13,mobile_number13,primary_address113,primary_address213,primary_city13,primary_zip13,Male,16-29,Highlands and Islands,No qualifications -p14,first_name14,last_name14,email14,mobile_number14,primary_address114,primary_address214,primary_city14,primary_zip14,Female,60+,Lothian,Level 1 -p15,first_name15,last_name15,email15,mobile_number15,primary_address115,primary_address215,primary_city15,primary_zip15,Female,45-59,Mid Scotland and Fife,Level 1 -p16,first_name16,last_name16,email16,mobile_number16,primary_address116,primary_address216,primary_city16,primary_zip16,Male,16-29,North East Scotland,Level 1 -p17,first_name17,last_name17,email17,mobile_number17,primary_address117,primary_address217,primary_city17,primary_zip17,Female,45-59,Lothian,No qualifications -p18,first_name18,last_name18,email18,mobile_number18,primary_address118,primary_address218,primary_city18,primary_zip18,Female,45-59,Mid Scotland and Fife,Level 2 or 3 -p19,first_name19,last_name19,email19,mobile_number19,primary_address119,primary_address219,primary_city19,primary_zip19,Female,16-29,Glasgow,Level 4 and above -p20,first_name20,last_name20,email20,mobile_number20,primary_address120,primary_address220,primary_city20,primary_zip20,Male,60+,Highlands and Islands,No qualifications -p21,first_name21,last_name21,email21,mobile_number21,primary_address121,primary_address221,primary_city21,primary_zip21,Female,30-44,West Scotland,Level 2 or 3 -p22,first_name22,last_name22,email22,mobile_number22,primary_address122,primary_address222,primary_city22,primary_zip22,Female,16-29,North East Scotland,Level 2 or 3 -p23,first_name23,last_name23,email23,mobile_number23,primary_address123,primary_address223,primary_city23,primary_zip23,Female,16-29,Highlands and Islands,Level 1 -p24,first_name24,last_name24,email24,mobile_number24,primary_address124,primary_address224,primary_city24,primary_zip24,Male,30-44,Glasgow,Level 4 and above -p25,first_name25,last_name25,email25,mobile_number25,primary_address125,primary_address225,primary_city25,primary_zip25,Female,45-59,Highlands and Islands,No qualifications -p26,first_name26,last_name26,email26,mobile_number26,primary_address126,primary_address226,primary_city26,primary_zip26,Male,45-59,South Scotland,Level 4 and above -p27,first_name27,last_name27,email27,mobile_number27,primary_address127,primary_address227,primary_city27,primary_zip27,Male,16-29,Glasgow,Level 4 and above -p28,first_name28,last_name28,email28,mobile_number28,primary_address128,primary_address228,primary_city28,primary_zip28,Male,16-29,Mid Scotland and Fife,No qualifications -p29,first_name29,last_name29,email29,mobile_number29,primary_address129,primary_address229,primary_city29,primary_zip29,Male,60+,North East Scotland,Level 4 and above -p30,first_name30,last_name30,email30,mobile_number30,primary_address130,primary_address230,primary_city30,primary_zip30,Female,60+,North East Scotland,Level 1 -p31,first_name31,last_name31,email31,mobile_number31,primary_address131,primary_address231,primary_city31,primary_zip31,Female,60+,Glasgow,Level 4 and above -p32,first_name32,last_name32,email32,mobile_number32,primary_address132,primary_address232,primary_city32,primary_zip32,Male,60+,Highlands and Islands,Level 4 and above -p33,first_name33,last_name33,email33,mobile_number33,primary_address133,primary_address233,primary_city33,primary_zip33,Male,16-29,West Scotland,Level 2 or 3 -p34,first_name34,last_name34,email34,mobile_number34,primary_address134,primary_address234,primary_city34,primary_zip34,Male,60+,Mid Scotland and Fife,No qualifications -p35,first_name35,last_name35,email35,mobile_number35,primary_address135,primary_address235,primary_city35,primary_zip35,Female,60+,North East Scotland,Level 4 and above -p36,first_name36,last_name36,email36,mobile_number36,primary_address136,primary_address236,primary_city36,primary_zip36,Male,60+,North East Scotland,Level 1 -p37,first_name37,last_name37,email37,mobile_number37,primary_address137,primary_address237,primary_city37,primary_zip37,Male,30-44,Mid Scotland and Fife,No qualifications -p38,first_name38,last_name38,email38,mobile_number38,primary_address138,primary_address238,primary_city38,primary_zip38,Female,60+,South Scotland,Level 1 -p39,first_name39,last_name39,email39,mobile_number39,primary_address139,primary_address239,primary_city39,primary_zip39,Female,45-59,Highlands and Islands,Level 1 -p40,first_name40,last_name40,email40,mobile_number40,primary_address140,primary_address240,primary_city40,primary_zip40,Male,16-29,Highlands and Islands,Level 1 -p41,first_name41,last_name41,email41,mobile_number41,primary_address141,primary_address241,primary_city41,primary_zip41,Male,16-29,Highlands and Islands,No qualifications -p42,first_name42,last_name42,email42,mobile_number42,primary_address142,primary_address242,primary_city42,primary_zip42,Female,45-59,Highlands and Islands,Level 1 -p43,first_name43,last_name43,email43,mobile_number43,primary_address143,primary_address243,primary_city43,primary_zip43,Male,60+,Mid Scotland and Fife,No qualifications -p44,first_name44,last_name44,email44,mobile_number44,primary_address144,primary_address244,primary_city44,primary_zip44,Male,16-29,Lothian,Level 1 -p45,first_name45,last_name45,email45,mobile_number45,primary_address145,primary_address245,primary_city45,primary_zip45,Female,60+,West Scotland,No qualifications -p46,first_name46,last_name46,email46,mobile_number46,primary_address146,primary_address246,primary_city46,primary_zip46,Female,60+,Mid Scotland and Fife,No qualifications -p47,first_name47,last_name47,email47,mobile_number47,primary_address147,primary_address247,primary_city47,primary_zip47,Female,45-59,South Scotland,No qualifications -p48,first_name48,last_name48,email48,mobile_number48,primary_address148,primary_address248,primary_city48,primary_zip48,Female,16-29,South Scotland,No qualifications -p49,first_name49,last_name49,email49,mobile_number49,primary_address149,primary_address249,primary_city49,primary_zip49,Female,30-44,Mid Scotland and Fife,No qualifications -p50,first_name50,last_name50,email50,mobile_number50,primary_address150,primary_address250,primary_city50,primary_zip50,Male,60+,Mid Scotland and Fife,No qualifications -p51,first_name51,last_name51,email51,mobile_number51,primary_address151,primary_address251,primary_city51,primary_zip51,Male,60+,Mid Scotland and Fife,Level 2 or 3 -p52,first_name52,last_name52,email52,mobile_number52,primary_address152,primary_address252,primary_city52,primary_zip52,Female,30-44,Central Scotland,Level 2 or 3 -p53,first_name53,last_name53,email53,mobile_number53,primary_address153,primary_address253,primary_city53,primary_zip53,Female,60+,North East Scotland,Level 4 and above -p54,first_name54,last_name54,email54,mobile_number54,primary_address154,primary_address254,primary_city54,primary_zip54,Male,60+,Highlands and Islands,Level 1 -p55,first_name55,last_name55,email55,mobile_number55,primary_address155,primary_address255,primary_city55,primary_zip55,Male,16-29,Highlands and Islands,Level 2 or 3 -p56,first_name56,last_name56,email56,mobile_number56,primary_address156,primary_address256,primary_city56,primary_zip56,Male,16-29,West Scotland,No qualifications -p57,first_name57,last_name57,email57,mobile_number57,primary_address157,primary_address257,primary_city57,primary_zip57,Female,60+,Highlands and Islands,Level 2 or 3 -p58,first_name58,last_name58,email58,mobile_number58,primary_address158,primary_address258,primary_city58,primary_zip58,Male,45-59,West Scotland,Level 2 or 3 -p59,first_name59,last_name59,email59,mobile_number59,primary_address159,primary_address259,primary_city59,primary_zip59,Male,16-29,West Scotland,No qualifications -p60,first_name60,last_name60,email60,mobile_number60,primary_address160,primary_address260,primary_city60,primary_zip60,Male,16-29,North East Scotland,Level 4 and above -p61,first_name61,last_name61,email61,mobile_number61,primary_address161,primary_address261,primary_city61,primary_zip61,Male,16-29,Highlands and Islands,No qualifications -p62,first_name62,last_name62,email62,mobile_number62,primary_address162,primary_address262,primary_city62,primary_zip62,Male,16-29,Highlands and Islands,Level 2 or 3 -p63,first_name63,last_name63,email63,mobile_number63,primary_address163,primary_address263,primary_city63,primary_zip63,Male,45-59,South Scotland,No qualifications -p64,first_name64,last_name64,email64,mobile_number64,primary_address164,primary_address264,primary_city64,primary_zip64,Female,45-59,South Scotland,No qualifications -p65,first_name65,last_name65,email65,mobile_number65,primary_address165,primary_address265,primary_city65,primary_zip65,Male,30-44,Central Scotland,Level 4 and above -p66,first_name66,last_name66,email66,mobile_number66,primary_address166,primary_address266,primary_city66,primary_zip66,Female,30-44,South Scotland,No qualifications -p67,first_name67,last_name67,email67,mobile_number67,primary_address167,primary_address267,primary_city67,primary_zip67,Male,45-59,Mid Scotland and Fife,Level 4 and above -p68,first_name68,last_name68,email68,mobile_number68,primary_address168,primary_address268,primary_city68,primary_zip68,Female,60+,Mid Scotland and Fife,No qualifications -p69,first_name69,last_name69,email69,mobile_number69,primary_address169,primary_address269,primary_city69,primary_zip69,Male,16-29,South Scotland,Level 4 and above -p70,first_name70,last_name70,email70,mobile_number70,primary_address170,primary_address270,primary_city70,primary_zip70,Male,45-59,Lothian,Level 2 or 3 -p71,first_name71,last_name71,email71,mobile_number71,primary_address171,primary_address271,primary_city71,primary_zip71,Male,30-44,Highlands and Islands,No qualifications -p72,first_name72,last_name72,email72,mobile_number72,primary_address172,primary_address272,primary_city72,primary_zip72,Male,60+,West Scotland,Level 4 and above -p73,first_name73,last_name73,email73,mobile_number73,primary_address173,primary_address273,primary_city73,primary_zip73,Male,60+,Mid Scotland and Fife,Level 1 -p74,first_name74,last_name74,email74,mobile_number74,primary_address174,primary_address274,primary_city74,primary_zip74,Male,45-59,West Scotland,Level 1 -p75,first_name75,last_name75,email75,mobile_number75,primary_address175,primary_address275,primary_city75,primary_zip75,Female,30-44,Central Scotland,Level 4 and above -p76,first_name76,last_name76,email76,mobile_number76,primary_address176,primary_address276,primary_city76,primary_zip76,Male,16-29,Central Scotland,Level 2 or 3 -p77,first_name77,last_name77,email77,mobile_number77,primary_address177,primary_address277,primary_city77,primary_zip77,Female,60+,West Scotland,Level 2 or 3 -p78,first_name78,last_name78,email78,mobile_number78,primary_address178,primary_address278,primary_city78,primary_zip78,Female,45-59,Mid Scotland and Fife,Level 4 and above -p79,first_name79,last_name79,email79,mobile_number79,primary_address179,primary_address279,primary_city79,primary_zip79,Male,30-44,Highlands and Islands,Level 1 -p80,first_name80,last_name80,email80,mobile_number80,primary_address180,primary_address280,primary_city80,primary_zip80,Male,30-44,West Scotland,No qualifications -p81,first_name81,last_name81,email81,mobile_number81,primary_address181,primary_address281,primary_city81,primary_zip81,Male,30-44,Central Scotland,No qualifications -p82,first_name82,last_name82,email82,mobile_number82,primary_address182,primary_address282,primary_city82,primary_zip82,Female,16-29,Glasgow,Level 2 or 3 -p83,first_name83,last_name83,email83,mobile_number83,primary_address183,primary_address283,primary_city83,primary_zip83,Male,16-29,Mid Scotland and Fife,Level 1 -p84,first_name84,last_name84,email84,mobile_number84,primary_address184,primary_address284,primary_city84,primary_zip84,Female,45-59,Glasgow,Level 1 -p85,first_name85,last_name85,email85,mobile_number85,primary_address185,primary_address285,primary_city85,primary_zip85,Female,45-59,Lothian,Level 4 and above -p86,first_name86,last_name86,email86,mobile_number86,primary_address186,primary_address286,primary_city86,primary_zip86,Female,60+,Glasgow,Level 1 -p87,first_name87,last_name87,email87,mobile_number87,primary_address187,primary_address287,primary_city87,primary_zip87,Male,45-59,Glasgow,Level 1 -p88,first_name88,last_name88,email88,mobile_number88,primary_address188,primary_address288,primary_city88,primary_zip88,Female,45-59,Lothian,No qualifications -p89,first_name89,last_name89,email89,mobile_number89,primary_address189,primary_address289,primary_city89,primary_zip89,Female,30-44,West Scotland,Level 1 -p90,first_name90,last_name90,email90,mobile_number90,primary_address190,primary_address290,primary_city90,primary_zip90,Male,60+,Central Scotland,No qualifications -p91,first_name91,last_name91,email91,mobile_number91,primary_address191,primary_address291,primary_city91,primary_zip91,Male,30-44,Highlands and Islands,Level 4 and above -p92,first_name92,last_name92,email92,mobile_number92,primary_address192,primary_address292,primary_city92,primary_zip92,Male,45-59,South Scotland,Level 1 -p93,first_name93,last_name93,email93,mobile_number93,primary_address193,primary_address293,primary_city93,primary_zip93,Female,45-59,North East Scotland,Level 1 -p94,first_name94,last_name94,email94,mobile_number94,primary_address194,primary_address294,primary_city94,primary_zip94,Male,16-29,West Scotland,Level 2 or 3 -p95,first_name95,last_name95,email95,mobile_number95,primary_address195,primary_address295,primary_city95,primary_zip95,Female,30-44,Lothian,No qualifications -p96,first_name96,last_name96,email96,mobile_number96,primary_address196,primary_address296,primary_city96,primary_zip96,Male,45-59,Lothian,No qualifications -p97,first_name97,last_name97,email97,mobile_number97,primary_address197,primary_address297,primary_city97,primary_zip97,Male,16-29,Mid Scotland and Fife,Level 1 -p98,first_name98,last_name98,email98,mobile_number98,primary_address198,primary_address298,primary_city98,primary_zip98,Male,16-29,Glasgow,Level 1 -p99,first_name99,last_name99,email99,mobile_number99,primary_address199,primary_address299,primary_city99,primary_zip99,Female,16-29,Lothian,Level 4 and above -p100,first_name100,last_name100,email100,mobile_number100,primary_address1100,primary_address2100,primary_city100,primary_zip100,Male,60+,Glasgow,Level 1 -p101,first_name101,last_name101,email101,mobile_number101,primary_address1101,primary_address2101,primary_city101,primary_zip101,Female,60+,Highlands and Islands,Level 4 and above -p102,first_name102,last_name102,email102,mobile_number102,primary_address1102,primary_address2102,primary_city102,primary_zip102,Female,60+,Glasgow,No qualifications -p103,first_name103,last_name103,email103,mobile_number103,primary_address1103,primary_address2103,primary_city103,primary_zip103,Female,16-29,North East Scotland,Level 4 and above -p104,first_name104,last_name104,email104,mobile_number104,primary_address1104,primary_address2104,primary_city104,primary_zip104,Male,60+,South Scotland,No qualifications -p105,first_name105,last_name105,email105,mobile_number105,primary_address1105,primary_address2105,primary_city105,primary_zip105,Female,30-44,Central Scotland,Level 1 -p106,first_name106,last_name106,email106,mobile_number106,primary_address1106,primary_address2106,primary_city106,primary_zip106,Female,45-59,Highlands and Islands,Level 4 and above -p107,first_name107,last_name107,email107,mobile_number107,primary_address1107,primary_address2107,primary_city107,primary_zip107,Female,45-59,West Scotland,Level 4 and above -p108,first_name108,last_name108,email108,mobile_number108,primary_address1108,primary_address2108,primary_city108,primary_zip108,Female,30-44,West Scotland,Level 2 or 3 -p109,first_name109,last_name109,email109,mobile_number109,primary_address1109,primary_address2109,primary_city109,primary_zip109,Male,60+,Central Scotland,Level 4 and above -p110,first_name110,last_name110,email110,mobile_number110,primary_address1110,primary_address2110,primary_city110,primary_zip110,Male,30-44,North East Scotland,No qualifications -p111,first_name111,last_name111,email111,mobile_number111,primary_address1111,primary_address2111,primary_city111,primary_zip111,Female,60+,Lothian,Level 2 or 3 -p112,first_name112,last_name112,email112,mobile_number112,primary_address1112,primary_address2112,primary_city112,primary_zip112,Male,16-29,Mid Scotland and Fife,Level 4 and above -p113,first_name113,last_name113,email113,mobile_number113,primary_address1113,primary_address2113,primary_city113,primary_zip113,Female,16-29,Mid Scotland and Fife,Level 1 -p114,first_name114,last_name114,email114,mobile_number114,primary_address1114,primary_address2114,primary_city114,primary_zip114,Female,30-44,Glasgow,No qualifications -p115,first_name115,last_name115,email115,mobile_number115,primary_address1115,primary_address2115,primary_city115,primary_zip115,Male,16-29,Lothian,Level 4 and above -p116,first_name116,last_name116,email116,mobile_number116,primary_address1116,primary_address2116,primary_city116,primary_zip116,Female,60+,North East Scotland,Level 4 and above -p117,first_name117,last_name117,email117,mobile_number117,primary_address1117,primary_address2117,primary_city117,primary_zip117,Female,60+,West Scotland,Level 4 and above -p118,first_name118,last_name118,email118,mobile_number118,primary_address1118,primary_address2118,primary_city118,primary_zip118,Male,16-29,Central Scotland,Level 2 or 3 -p119,first_name119,last_name119,email119,mobile_number119,primary_address1119,primary_address2119,primary_city119,primary_zip119,Male,60+,North East Scotland,Level 4 and above -p120,first_name120,last_name120,email120,mobile_number120,primary_address1120,primary_address2120,primary_city120,primary_zip120,Male,30-44,Lothian,Level 4 and above -p121,first_name121,last_name121,email121,mobile_number121,primary_address1121,primary_address2121,primary_city121,primary_zip121,Female,60+,Glasgow,Level 1 -p122,first_name122,last_name122,email122,mobile_number122,primary_address1122,primary_address2122,primary_city122,primary_zip122,Female,30-44,Mid Scotland and Fife,Level 4 and above -p123,first_name123,last_name123,email123,mobile_number123,primary_address1123,primary_address2123,primary_city123,primary_zip123,Female,45-59,North East Scotland,Level 4 and above -p124,first_name124,last_name124,email124,mobile_number124,primary_address1124,primary_address2124,primary_city124,primary_zip124,Male,30-44,Mid Scotland and Fife,Level 4 and above -p125,first_name125,last_name125,email125,mobile_number125,primary_address1125,primary_address2125,primary_city125,primary_zip125,Female,45-59,Highlands and Islands,Level 1 -p126,first_name126,last_name126,email126,mobile_number126,primary_address1126,primary_address2126,primary_city126,primary_zip126,Female,30-44,South Scotland,Level 1 -p127,first_name127,last_name127,email127,mobile_number127,primary_address1127,primary_address2127,primary_city127,primary_zip127,Female,16-29,Lothian,Level 2 or 3 -p128,first_name128,last_name128,email128,mobile_number128,primary_address1128,primary_address2128,primary_city128,primary_zip128,Male,30-44,Glasgow,No qualifications -p129,first_name129,last_name129,email129,mobile_number129,primary_address1129,primary_address2129,primary_city129,primary_zip129,Female,60+,Central Scotland,Level 1 -p130,first_name130,last_name130,email130,mobile_number130,primary_address1130,primary_address2130,primary_city130,primary_zip130,Female,60+,Lothian,No qualifications -p131,first_name131,last_name131,email131,mobile_number131,primary_address1131,primary_address2131,primary_city131,primary_zip131,Female,30-44,South Scotland,Level 4 and above -p132,first_name132,last_name132,email132,mobile_number132,primary_address1132,primary_address2132,primary_city132,primary_zip132,Female,45-59,Central Scotland,Level 1 -p133,first_name133,last_name133,email133,mobile_number133,primary_address1133,primary_address2133,primary_city133,primary_zip133,Female,45-59,Lothian,Level 2 or 3 -p134,first_name134,last_name134,email134,mobile_number134,primary_address1134,primary_address2134,primary_city134,primary_zip134,Female,45-59,Lothian,Level 1 -p135,first_name135,last_name135,email135,mobile_number135,primary_address1135,primary_address2135,primary_city135,primary_zip135,Female,60+,Glasgow,Level 2 or 3 -p136,first_name136,last_name136,email136,mobile_number136,primary_address1136,primary_address2136,primary_city136,primary_zip136,Female,45-59,Glasgow,No qualifications -p137,first_name137,last_name137,email137,mobile_number137,primary_address1137,primary_address2137,primary_city137,primary_zip137,Male,45-59,Central Scotland,Level 2 or 3 -p138,first_name138,last_name138,email138,mobile_number138,primary_address1138,primary_address2138,primary_city138,primary_zip138,Female,16-29,Glasgow,Level 2 or 3 -p139,first_name139,last_name139,email139,mobile_number139,primary_address1139,primary_address2139,primary_city139,primary_zip139,Female,60+,Central Scotland,Level 4 and above -p140,first_name140,last_name140,email140,mobile_number140,primary_address1140,primary_address2140,primary_city140,primary_zip140,Female,60+,South Scotland,Level 2 or 3 -p141,first_name141,last_name141,email141,mobile_number141,primary_address1141,primary_address2141,primary_city141,primary_zip141,Female,45-59,South Scotland,Level 2 or 3 -p142,first_name142,last_name142,email142,mobile_number142,primary_address1142,primary_address2142,primary_city142,primary_zip142,Male,16-29,Central Scotland,Level 4 and above -p143,first_name143,last_name143,email143,mobile_number143,primary_address1143,primary_address2143,primary_city143,primary_zip143,Male,45-59,Glasgow,Level 2 or 3 -p144,first_name144,last_name144,email144,mobile_number144,primary_address1144,primary_address2144,primary_city144,primary_zip144,Female,16-29,Central Scotland,Level 4 and above -p145,first_name145,last_name145,email145,mobile_number145,primary_address1145,primary_address2145,primary_city145,primary_zip145,Female,60+,Highlands and Islands,Level 1 -p146,first_name146,last_name146,email146,mobile_number146,primary_address1146,primary_address2146,primary_city146,primary_zip146,Female,16-29,North East Scotland,Level 2 or 3 -p147,first_name147,last_name147,email147,mobile_number147,primary_address1147,primary_address2147,primary_city147,primary_zip147,Female,60+,Highlands and Islands,No qualifications -p148,first_name148,last_name148,email148,mobile_number148,primary_address1148,primary_address2148,primary_city148,primary_zip148,Male,16-29,Mid Scotland and Fife,Level 1 -p149,first_name149,last_name149,email149,mobile_number149,primary_address1149,primary_address2149,primary_city149,primary_zip149,Male,45-59,West Scotland,Level 4 and above diff --git a/fixtures/categories.csv b/fixtures/categories.csv deleted file mode 100644 index 73ab8c1..0000000 --- a/fixtures/categories.csv +++ /dev/null @@ -1,21 +0,0 @@ -category,name,min,max,min_flex,max_flex -gender,Female,11,12,0,12 -gender,Male,10,11,0,12 -gender,Non-binary or other,0,1,0,1 -age_bracket,0-15,0,0,0,0 -age_bracket,16-29,5,7,0,7 -age_bracket,30-44,5,7,0,7 -age_bracket,45-59,5,6,0,6 -age_bracket,60+,5,6,0,6 -edu_level,No qualifications,5,7,0,7 -edu_level,Level 1,5,7,0,7 -edu_level,Level 2 or 3,5,6,0,6 -edu_level,Level 4 and above,5,6,0,6 -geo_bucket,Central Scotland,3,5,0,5 -geo_bucket,Glasgow,3,5,0,5 -geo_bucket,Highlands and Islands,2,5,0,5 -geo_bucket,Lothian,3,5,0,5 -geo_bucket,Mid Scotland and Fife,3,5,0,5 -geo_bucket,North East Scotland,2,5,0,5 -geo_bucket,South Scotland,3,5,0,5 -geo_bucket,West Scotland,3,5,0,5 diff --git a/pyproject.toml b/pyproject.toml index c6296c1..4fe165f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,26 +5,21 @@ description = "Application to select people from a spreadsheet according to give authors = [ "Nick Gill ", "Brett Hennig ", - "Paul Goelz " + "Paul Goelz ", + "Hamish Downer ", ] readme = "README.md" requires-python = ">=3.11,<3.13" dependencies = [ - "cvxpy==1.5.3", - "Eel==0.17.0", - "gspread==6.1.4", - "mip==1.15.0", - "oauth2client==4.1.3", - "pyinstaller==6.11.1", - "toml==0.10.2", + "Eel==0.18.2", + "oauth2client<5", + "pyinstaller<7", + "sortition-algorithms==0.11.5", + "toml", ] [dependency-groups] -dev = [ - "pytest>=8.3.4", - "pytest-cov>=6.0.0", - "ruff>=0.9.7", -] +dev = ["pytest>=8.3.4", "pytest-cov>=6.0.0", "ruff>=0.9.7"] [tool.mypy] python_version = "3.12" @@ -34,113 +29,118 @@ warn_unused_ignores = true warn_redundant_casts = true warn_unused_configs = true +# eel does not have type hints/stubs +[[tool.mypy.overrides]] +module = "eel.*" +ignore_missing_imports = true +ignore_errors = true + [tool.ruff] target-version = "py312" line-length = 120 [tool.ruff.lint] select = [ - "F", - "E", - "W", - "C90", - "I", - "N", - "UP", - "YTT", - # "ANN", # flake8-annotations: we should support this in the future but 100+ errors atm - "ASYNC", - "S", - "BLE", - "FBT", - "B", - "A", - "COM", - "C4", - "DTZ", - "T10", - "DJ", - "EM", - "EXE", - "FA", - "ISC", - "ICN", - "G", - "INP", - "PIE", - "T20", - "PYI", - # "PT", - "Q", - "RSE", - "RET", - "SLF", - "SLOT", - "SIM", - "TID", - "TCH", - "INT", - # "ARG", # Unused function argument - "PTH", - "ERA", - "PD", - "PGH", - "PL", - "TRY", - "FLY", - # "NPY", - # "AIR", - "PERF", - # "FURB", - # "LOG", - "RUF", + "F", + "E", + "W", + "C90", + "I", + "N", + "UP", + "YTT", + # "ANN", # flake8-annotations: we should support this in the future but 100+ errors atm + "ASYNC", + "S", + "BLE", + "FBT", + "B", + "A", + "COM", + "C4", + "DTZ", + "T10", + "DJ", + "EM", + "EXE", + "FA", + "ISC", + "ICN", + "G", + "INP", + "PIE", + "T20", + "PYI", + # "PT", + "Q", + "RSE", + "RET", + "SLF", + "SLOT", + "SIM", + "TID", + "TCH", + "INT", + # "ARG", # Unused function argument + "PTH", + "ERA", + "PD", + "PGH", + "PL", + "TRY", + "FLY", + # "NPY", + # "AIR", + "PERF", + # "FURB", + # "LOG", + "RUF", ] ignore = [ - # TODO: reinstate this - "B017", #assertRaises(Exception) - "C901", # too complex - "FBT001", # positional boolean arg in function defn - "PLR0912", # too many branches - "PLR0913", # too many args in function defn - "PLR0915", # too many statements - "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` - "S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/ - "S311", # pseudo-random - it's not security - "SIM102", # sometimes it's better to nest - "T201", # `print` found - fine to use it for now - "TRY301", # abstract raise to an inner function - "UP038", # Checks for uses of isinstance/issubclass that take a tuple - # of types for comparison. - # Deactivated because it can make the code slow: - # https://github.com/astral-sh/ruff/issues/7871 + # TODO: reinstate this + "B017", # assertRaises(Exception) + "BLE001", # don't catch Exception + "C901", # too complex + "FBT001", # positional boolean arg in function defn + "PLR0912", # too many branches + "PLR0913", # too many args in function defn + "PLR0915", # too many statements + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` + "S101", # Use of assert detected https://docs.astral.sh/ruff/rules/assert/ + "S311", # pseudo-random - it's not security + "SIM102", # sometimes it's better to nest + "T201", # `print` found - fine to use it for now + "TRY301", # abstract raise to an inner function + "UP038", # Checks for uses of isinstance/issubclass that take a tuple + # of types for comparison. + # Deactivated because it can make the code slow: + # https://github.com/astral-sh/ruff/issues/7871 ] # The fixes in extend-unsafe-fixes will require # provide the `--unsafe-fixes` flag when fixing. -extend-unsafe-fixes = [ - "UP038", -] +extend-unsafe-fixes = ["UP038"] #[tool.ruff.per-file-ignores] [tool.ruff.lint.extend-per-file-ignores] "**/tests/**/*.py" = [ - # at least these should be fine in tests: - "S101", # asserts allowed in tests... - "S106", # hardcoded passwords - "ARG", # Unused function args -> fixtures nevertheless are functionally relevant... - "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize() - # The below are debateable - "PLR2004", # Magic value used in comparison, ... - "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + # at least these should be fine in tests: + "S101", # asserts allowed in tests... + "S106", # hardcoded passwords + "ARG", # Unused function args -> fixtures nevertheless are functionally relevant... + "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize() + # The below are debateable + "PLR2004", # Magic value used in comparison, ... + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes ] "**/tests/*" = [ - # at least these should be fine in tests: - "S101", # asserts allowed in tests... - "S106", # hardcoded passwords - "ARG", # Unused function args -> fixtures nevertheless are functionally relevant... - "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize() - # The below are debateable - "PLR2004", # Magic value used in comparison, ... - "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes + # at least these should be fine in tests: + "S101", # asserts allowed in tests... + "S106", # hardcoded passwords + "ARG", # Unused function args -> fixtures nevertheless are functionally relevant... + "FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize() + # The below are debateable + "PLR2004", # Magic value used in comparison, ... + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes ] [tool.ruff.lint.isort] diff --git a/script.py b/script.py index d9f1452..c181572 100644 --- a/script.py +++ b/script.py @@ -1,317 +1,572 @@ import platform import sys +from collections.abc import Iterable +from enum import Enum +from pathlib import Path import eel import gspread +from sortition_algorithms import Settings, adapters, core, features, people -from stratification import ( - PeopleAndCatsCSV, - PeopleAndCatsGoogleSheet, - Settings, -) +DEFAULT_SETTINGS_PATH = Path.home() / "sf_stratification_settings.toml" +DEFAULT_AUTH_JSON_PATH = Path.home() / "secret_do_not_commit.json" -# to be honest this is no longer a file contents class - it's a GUI interface handler -# all the "content" has been moved into the PeopleAndCats class and its children -class FileContents: +class KnownFailureError(Exception): + """Class to raise without extra errors""" + + +class LogType(Enum): + CSV_FEATURES = 1 + CSV_SELECTION = 2 + GSHEET_FEATURES = 3 + GSHEET_SELECTION = 4 + DETAILED_LOG = 5 + + +class GuiLog: + """Singleton class for sending messages to different divs""" + + def __init__(self) -> None: + self.lines: dict[LogType, list[str]] = {lt: [""] for lt in LogType} + + def reset(self, section: LogType, new_message: str = "") -> None: + self.lines[section] = [new_message] + self.update_area(section) + + def add_lines(self, section: LogType, lines: Iterable[str]) -> None: + self.lines[section] += lines + self.update_area(section) + + def add_line(self, section: LogType, line: str) -> None: + self.lines[section].append(line) + self.update_area(section) + + def update_area(self, section: LogType) -> None: + update_str = "
".join(line for line in self.lines[section] if line.strip()) + if section == LogType.CSV_FEATURES: + eel.update_csv_features_output_area(update_str) + elif section == LogType.CSV_SELECTION: + eel.update_csv_selection_output_area(update_str) + elif section == LogType.GSHEET_FEATURES: + eel.update_g_sheet_features_output_area(update_str) + elif section == LogType.GSHEET_SELECTION: + eel.update_g_sheet_selection_output_area(update_str) + elif section == LogType.DETAILED_LOG: + eel.update_detailed_log_messages_area(update_str) + + def update_all_areas(self) -> None: + for log_type in LogType: + self.update_area(log_type) + + +gui_log = GuiLog() + + +class SettingsHolder: def __init__(self): - self.PeopleAndCats = None - self._settings = None - # All of these below are only used in the Google Sheet version - self.g_sheet_name = "" - self.respondents_tab_name = "Respondents" # Instance attribute for Advanced Settings - self.category_tab_name = "Categories" # Instance attribute for Advanced Settings - self.gen_rem_tab = "on" # Instance attribute for Advanced Settings - self.number_selections = ( - 1 # Instance attribute for Advanced Settings (then later stored in PeopleAndCats) - ) + self._settings: Settings | None = None @property - def settings(self): - self._init_settings() + def settings(self) -> Settings: + self.init_settings() + assert self._settings is not None return self._settings - def _init_settings(self): + def init_settings(self) -> str: """ Call from lots of places to report the error early """ - message = "" if self._settings is None: - self._settings, message = Settings.load_from_file() - return message + try: + self._settings, report = Settings.load_from_file( + settings_file_path=DEFAULT_SETTINGS_PATH, + ) + return report.as_html() + except Exception as error: + return f"Error reading in settings file: {error}" + return "" - def _add_category_content(self, input_content): - min_selection = 0 - max_selection = 0 - all_msg: list[str] = [] - try: - message = self._init_settings() - if message != "": - all_msg.append(message) - # we want to catch and report unexpected exceptions here - except Exception as error: # noqa: BLE001 - self.PeopleAndCats.category_content_loaded = False - all_msg.append(f"Error reading in settings file: {error}") + def init_settings_log(self, section: LogType) -> None: + message = self.init_settings() + if message: + gui_log.add_line(section, message) + + def loaded(self) -> bool: + return self._settings is not None + + +settings_holder = SettingsHolder() + + +class CSVHandler: + def __init__(self): + self.data_source = adapters.CSVStringDataSource("", "") + self.select_data = adapters.SelectionData(self.data_source) + self.features: features.FeatureCollection | None = None + self.people: people.People | None = None + # cache this in case we need to reload + self.people_contents: str = "" + self.panel_size_str = "0" # Number of people in each panel + + @property + def panel_size_num(self) -> int: + if self.panel_size_str == "": + return 0 + return int(self.panel_size_str) + + def _set_panel_size(self, panel_size: str | int, *, update_eel: bool = True) -> None: + self.panel_size_str = str(panel_size) + # check if we can convert to int, and that it is >= 0 try: - msg2, min_selection, max_selection = self.PeopleAndCats.load_cats( - input_content, - self.category_tab_name, - self._settings, - ) - all_msg += msg2 - except gspread.exceptions.APIError as error: - all_msg.append( - f"API error causing delay. Please wait a couple of seconds while gsheet updates. " - f"After waiting you may need to reload sheet. " - f"For the record, the API error is {error}", + panel_size_int = int(self.panel_size_str) + if panel_size_int < 0: + self.panel_size_str = "" + except ValueError: + self.panel_size_str = "" + # finally update the display - unless we've been called from the JS + # in which case skip this to avoid infinite loops. + if update_eel: + eel.set_csv_panel_size(self.panel_size_str) + + def update_panel_size(self, panel_size: str) -> None: + # this comes from the UI, so don't update_eel - otherwise we have a loop + self._set_panel_size(panel_size.strip(), update_eel=False) + self.update_run_button() + + def update_run_button(self): + if self.features and self.people and self.panel_size_num > 0: + eel.enable_csv_run_button() + else: + eel.disable_csv_run_button() + + def add_feature_content(self, file_contents: str): + gui_log.reset(LogType.CSV_FEATURES) + if not file_contents: + gui_log.add_line( + LogType.CSV_FEATURES, + "No file contents - was the file empty?", ) - # we want to catch and report unexpected exceptions here - except Exception as error: # noqa: BLE001 - self.PeopleAndCats.category_content_loaded = False - all_msg.append(f"Error reading in categories file: {error}") - print(all_msg) # noqa: T201 - eel.update_categories_output_area("
".join(all_msg)) - self.update_selection_content() - eel.update_selection_range(min_selection, max_selection) + return + settings_holder.init_settings_log(LogType.CSV_FEATURES) + if not settings_holder.loaded(): + return + try: + self.data_source.features_data = file_contents + self.features, report = self.select_data.load_features() + gui_log.add_line(LogType.CSV_FEATURES, report.as_html()) + except Exception as error: + gui_log.add_line(LogType.CSV_FEATURES, f"Failed to load features: {error}") + if not self.features: + return + eel.enable_csv_selection_content() + min_size = features.minimum_selection(self.features) + max_size = features.maximum_selection(self.features) + eel.update_csv_selection_range(min_size, max_size) # if these are the same just set the value! - if min_selection == max_selection and min_selection > 0: - eel.set_select_number_people(str(min_selection)) - self.PeopleAndCats.number_people_to_select = int(min_selection) - # if we've already uploaded people, we need to re-process them with the - # (possibly) new categories settings - if self.PeopleAndCats.people_content_loaded: - dummy_file_contents = "" - all_msg = self.PeopleAndCats.load_people( - self.settings, - dummy_file_contents, - self.respondents_tab_name, - self.category_tab_name, - self.gen_rem_tab, + if min_size == max_size and min_size > 0: + self._set_panel_size(min_size) + # reset people, might need to be reloaded with features + if self.people: + self.add_people_content(self.people_contents) + + def add_people_content(self, file_contents: str) -> None: + gui_log.reset(LogType.CSV_SELECTION) + assert self.features is not None + try: + self.data_source.people_data = file_contents + self.people, report = self.select_data.load_people(settings_holder.settings, self.features) + # now we've done a successful load, cache the results + self.people_contents = file_contents + gui_log.add_line(LogType.CSV_SELECTION, report.as_html()) + gui_log.add_line( + LogType.CSV_SELECTION, + f"Loaded {self.people.count} people.", ) - eel.update_selection_output_area("
".join(all_msg)) + gui_log.add_line(LogType.CSV_SELECTION, "Successfully loaded features and people.") + except Exception as error: + gui_log.add_line(LogType.CSV_SELECTION, f"Failed to load people: {error}") self.update_run_button() - # called from CSV input - def add_category_content(self, file_contents): - if file_contents != "": - self.PeopleAndCats = PeopleAndCatsCSV() - self._add_category_content(file_contents) + def run_selection(self, test_selection: bool) -> None: + assert self.people is not None and self.features is not None + # they may have hit this button again, so clear the output area so it's more obvious + gui_log.reset(LogType.DETAILED_LOG, "Selecting... please wait...
") + try: + success, people_selected, report = core.run_stratification( + features=self.features, + people=self.people, + number_people_wanted=self.panel_size_num, + settings=settings_holder.settings, + test_selection=test_selection, + ) + except Exception as err: + gui_log.add_lines( + LogType.DETAILED_LOG, + [f"Unexpected error during selection: {err}", "Selection failed, process ended."], + ) + return + gui_log.add_line(LogType.DETAILED_LOG, report.as_html()) + if not success: + gui_log.add_line(LogType.DETAILED_LOG, "No panels written to CSV, process ended.") + return + try: + selected_rows, remaining_rows, _ = core.selected_remaining_tables( + full_people=self.people, + people_selected=people_selected[0], + features=self.features, + settings=settings_holder.settings, + ) + if success: + self.select_data.output_selected_remaining(selected_rows, remaining_rows, settings_holder.settings) + eel.enable_csv_selected_download( + self.data_source.selected_file.getvalue(), + "selected.csv", + ) + eel.enable_csv_remaining_download( + self.data_source.remaining_file.getvalue(), + "remaining.csv", + ) + except Exception as err: + gui_log.add_lines( + LogType.DETAILED_LOG, + [f"Unexpected error during writing selection: {err}", "Writing failed, process ended."], + ) + + +class GSheetHandler: + clear_message = "Number of features: You must (re)load sheet..." + original_selected_tab_name = "Original Selected - output - " + remaining_tab_name = "Remaining - output - " + + def __init__(self): + self.data_source = adapters.GSheetDataSource( + feature_tab_name="", + people_tab_name="", + auth_json_path=DEFAULT_AUTH_JSON_PATH, + ) + self.select_data = adapters.SelectionData(self.data_source) + self.features: features.FeatureCollection | None = None + self.people: people.People | None = None + self.g_sheet_name = "" + self.features_tab_name = "Categories" + self.people_tab_name = "Respondents" + self.gen_rem_tab = True + self.number_selections = 1 # How many panels to create + self.panel_size_str = "0" # Number of people in each panel - def _clear_messages(self, normal_message="Number of categories: You must (re)load sheet..."): - eel.update_categories_output_area(normal_message) - eel.update_selection_output_area(normal_message) - eel.update_selection_output_messages_area("") - eel.set_select_number_people("") + @property + def panel_size_num(self) -> int: + if self.panel_size_str == "": + return 0 + return int(self.panel_size_str) + + def _set_panel_size(self, panel_size: str | int, *, update_eel: bool = True) -> None: + self.panel_size_str = str(panel_size) + # check if we can convert to int, and that it is >= 0 + try: + panel_size_int = int(self.panel_size_str) + if panel_size_int < 0: + self.panel_size_str = "" + except ValueError: + self.panel_size_str = "" + # finally update the display - unless we've been called from the JS + # in which case skip this to avoid infinite loops. + if update_eel: + eel.set_g_sheet_panel_size(self.panel_size_str) + + def _clear_messages(self, features_message: str = clear_message) -> None: + """Clear all messages (and optionally put a new message in place)""" + gui_log.reset(LogType.GSHEET_FEATURES, features_message) + gui_log.reset(LogType.GSHEET_SELECTION) + gui_log.reset(LogType.DETAILED_LOG) + self._set_panel_size("") + + def _reset_spreadsheet(self, *, reset_features: bool = True) -> None: + if reset_features: + self.features = None + self.people = None + self._set_panel_size("") + self.update_run_button() # called from g-sheet input - def update_g_sheet_name(self, g_sheet_name_input): + def update_g_sheet_name(self, g_sheet_name_input) -> None: self._clear_messages() + self._reset_spreadsheet() self.g_sheet_name = g_sheet_name_input if self.g_sheet_name != "": eel.enable_load_g_sheet_btn() - # user has hit the (re)load button + def update_panel_size(self, panel_size: str) -> None: + # this comes from the UI, so don't update_eel - otherwise we have a loop + self._set_panel_size(panel_size.strip(), update_eel=False) + self.update_run_button() - # do cats and people at same time... - def load_g_sheet(self): - # this can happen if they enter something and then delete it... - if self.g_sheet_name == "": - self._clear_messages("Please enter a spreadsheet name...") + def update_run_button(self) -> None: + if self.features and self.people and self.panel_size_num > 0: + eel.enable_g_sheet_run_button() else: - self._clear_messages("Requesting data from sheet...") - try: - self.PeopleAndCats = PeopleAndCatsGoogleSheet() - # tell this object what this currently is... - self.PeopleAndCats.number_selections = self.number_selections - all_msg: list[str] = [] - if self.number_selections > 1: - all_msg.append( - f"WARNING: You've asked for {self.number_selections} selections. " - f"You cannot use the Produce a Test Panel button if you want more " - f"than 1 selection and no Remaining tab will be created.", - ) - self._add_category_content(self.g_sheet_name) - dummy_file_contents = "" - all_msg += self.PeopleAndCats.load_people( - self.settings, - dummy_file_contents, - self.respondents_tab_name, - self.category_tab_name, - self.gen_rem_tab, - ) - eel.update_selection_output_area("
".join(all_msg)) - self.update_run_button() - eel.enable_load_g_sheet_btn() - except Exception as error: # noqa: BLE001 - eel.update_categories_output_area( - f"Please wait a couple of seconds while gsheet updates. " - f"After waiting you may need to reload sheet. Current error is: {error}", - ) + eel.disable_g_sheet_run_button() - ############################################################################### - ### The next functions read in extra instance variables for advanced settings### - ############################################################################### - def update_respondents_tab_name(self, respondents_tab_name_input): + def update_people_tab_name(self, people_tab_name_input: str) -> None: self._clear_messages() - self.respondents_tab_name = respondents_tab_name_input + self._reset_spreadsheet(reset_features=False) + self.people_tab_name = people_tab_name_input - def update_categories_tab_name(self, categories_tab_name_input): + def update_features_tab_name(self, features_tab_name_input: str) -> None: self._clear_messages() - self.category_tab_name = categories_tab_name_input + self._reset_spreadsheet() + self.features_tab_name = features_tab_name_input - def update_gen_rem_tab(self, gen_rem_tab_input): - self.gen_rem_tab = gen_rem_tab_input + def update_gen_rem_tab(self, gen_rem_tab: bool) -> None: + self.gen_rem_tab = gen_rem_tab + + def _safe_gen_rem_tab(self) -> bool: + """Get self.gen_rem_tab - but set to false if number_selections > 1""" # never generate a remaining tab if doing a multiple selection if self.number_selections > 1: - self.gen_rem_tab = "off" + return False + return self.gen_rem_tab - def update_number_selections(self, number_selections_input): + def update_number_selections(self, number_selections_input: str) -> None: self._clear_messages() - if number_selections_input == "": - self.number_selections = 1 - else: - self.number_selections = int(number_selections_input) - # never generate a remaining tab if doing a multiple selection - if self.number_selections > 1: - self.gen_rem_tab = "off" - # but turn it on if = 1 (this could be wrong if the person wants it off!) - # if this has changed back to 1... - else: - self.gen_rem_tab = "on" - - ######################################## - ###End of Advanced Settings variables### - ######################################## - ### From here 'selection' means people... - def add_selection_content(self, file_contents): - self._init_settings() - # this calls update internally - msg = self.PeopleAndCats.load_people( - self.settings, - file_contents, - self.respondents_tab_name, - self.category_tab_name, - self.gen_rem_tab, - ) - eel.update_selection_output_area("
".join(msg)) - self.update_run_button() + self.number_selections = 1 if number_selections_input == "" else int(number_selections_input) - # 'selection' means people... - def update_selection_content(self): - if self.PeopleAndCats.category_content_loaded: - eel.enable_selection_content() + # do features and people at same time... + def load_g_sheet(self) -> None: + # forget about any previously loaded spreadsheet, and disable the run button + self._reset_spreadsheet() + # this can happen if they enter something and then delete it... + if self.g_sheet_name == "": + self._clear_messages("Please enter a spreadsheet name...") + return + self._clear_messages(f"Requesting category data from spreadsheet tab {self.features_tab_name} ...") + settings_holder.init_settings_log(LogType.GSHEET_FEATURES) + if not settings_holder.loaded(): + return + try: + if self.number_selections > 1: + gui_log.add_line( + LogType.GSHEET_SELECTION, + f"WARNING: You've asked for {self.number_selections} selections. " + f"You cannot use the Produce a Test Panel button if you want more " + f"than 1 selection and no Remaining tab will be created.", + ) + self.data_source.set_g_sheet_name(self.g_sheet_name) + self.add_feature_content(self.features_tab_name) + gui_log.add_line( + LogType.GSHEET_SELECTION, + f"Requesting people data from spreadsheet tab {self.people_tab_name} ...", + ) + self.add_people_content(self.people_tab_name) + gui_log.add_line(LogType.GSHEET_SELECTION, "Successfully loaded features and people.") + self.update_run_button() + eel.enable_load_g_sheet_btn() + except KnownFailureError: + # this is for when the function called has already logged the error, so we don't need + # to report it again. + gui_log.add_lines( + LogType.GSHEET_FEATURES, + ["Loading spreadsheet failed, see above messages.", "Fix the problems and try loading again."], + ) + except Exception as error: + gui_log.add_line(LogType.GSHEET_FEATURES, f"Loading spreadsheet failed: {error}") - def update_run_button(self): - if ( - self.PeopleAndCats.category_content_loaded - and self.PeopleAndCats.people_content_loaded - and self.PeopleAndCats.number_people_to_select > 0 - ): - eel.enable_run_button() - else: - eel.disable_run_button() - if self.PeopleAndCats.number_people_to_select <= 0: - eel.set_select_number_people("") + def add_feature_content(self, features_tab_name: str) -> None: + try: + self.data_source.feature_tab_name = features_tab_name + self.features, report = self.select_data.load_features() + gui_log.add_line(LogType.GSHEET_FEATURES, report.as_html()) + except gspread.exceptions.APIError as error: + gui_log.add_line( + LogType.GSHEET_FEATURES, + f"API error causing delay. Please wait a couple of seconds while gsheet updates. " + f"After waiting you may need to reload sheet. " + f"For the record, the API error is {error}", + ) + raise KnownFailureError from error + except Exception as error: + gui_log.add_line(LogType.GSHEET_FEATURES, f"Failed to load features: {error}") + raise KnownFailureError from error + if not self.features: + gui_log.add_line(LogType.GSHEET_FEATURES, "Failed to load features") + raise KnownFailureError + eel.update_g_sheet_selection_range( + features.minimum_selection(self.features), + features.maximum_selection(self.features), + ) + min_size = features.minimum_selection(self.features) + max_size = features.maximum_selection(self.features) + eel.update_g_sheet_selection_range(min_size, max_size) + # if these are the same just set the value! + if min_size == max_size and min_size > 0: + self._set_panel_size(min_size) - def update_number_people(self, number_people): - if number_people == "": - self.PeopleAndCats.number_people_to_select = 0 - else: - self.PeopleAndCats.number_people_to_select = int(number_people) - self.update_run_button() + def add_people_content(self, people_tab_name: str) -> None: + assert self.features is not None + try: + self.data_source.people_tab_name = people_tab_name + self.people, report = self.select_data.load_people(settings_holder.settings, self.features) + gui_log.add_line(LogType.GSHEET_SELECTION, report.as_html()) + except Exception as error: + gui_log.add_line( + LogType.GSHEET_SELECTION, + f"Failed to load people: {error}", + ) + raise KnownFailureError from error + if not self.people: + gui_log.add_line( + LogType.GSHEET_SELECTION, + "Failed to load people", + ) + raise KnownFailureError - def run_selection(self, test_selection): - self._init_settings() - # they may have hit this button again, so clear the output area so it's more obvious - eel.update_selection_output_messages_area("Selecting... please wait...
") - success, output_lines = self.PeopleAndCats.people_cats_run_stratification( - self.settings, - test_selection, - ) - if ( - success - and self.PeopleAndCats.get_selected_file() is not None - and self.PeopleAndCats.get_remaining_file() is not None - ): - eel.enable_selected_download( - self.PeopleAndCats.get_selected_file().getvalue(), - "selected.csv", + def run_selection(self, test_selection: bool) -> None: + assert self.features is not None and self.people is not None + gui_log.reset(LogType.DETAILED_LOG, "Selecting... please wait...
") + try: + success, people_selected, report = core.run_stratification( + features=self.features, + people=self.people, + number_people_wanted=self.panel_size_num, + settings=settings_holder.settings, + test_selection=test_selection, + number_selections=self.number_selections, ) - eel.enable_remaining_download( - self.PeopleAndCats.get_remaining_file().getvalue(), - "remaining.csv", + except Exception as err: + gui_log.add_lines( + LogType.DETAILED_LOG, + [f"Unexpected error during selection: {err}", "Selection failed, process ended."], ) - # print output_lines to the App: - eel.update_selection_output_messages_area("
".join(output_lines)) + return + gui_log.add_line(LogType.DETAILED_LOG, report.as_html()) + if not success: + gui_log.add_line(LogType.DETAILED_LOG, "No panels written to spreadsheet, process ended.") + return + + gui_log.add_line(LogType.DETAILED_LOG, "About to write to spreadsheet.") + try: + selected_rows, remaining_rows, _ = core.selected_remaining_tables( + full_people=self.people, + people_selected=people_selected[0], + features=self.features, + settings=settings_holder.settings, + ) + self.data_source.selected_tab_name = self.original_selected_tab_name + self.data_source.remaining_tab_name = self.remaining_tab_name + self.select_data.gen_rem_tab = self._safe_gen_rem_tab() + dupes, report = self.select_data.output_selected_remaining( + selected_rows, + remaining_rows, + settings_holder.settings, + ) + gui_log.add_line(LogType.DETAILED_LOG, report.as_html()) + gui_log.add_line( + LogType.DETAILED_LOG, + f"In the remaining tab there are {len(dupes)} people who share the same address as " + f"someone else in the tab. They are highlighted in orange.", + ) + gui_log.add_line(LogType.DETAILED_LOG, "All spreadsheet writing has finished.") + gui_log.add_line(LogType.DETAILED_LOG, "Selection process finished.") + except Exception as err: + gui_log.add_lines( + LogType.DETAILED_LOG, + [f"Unexpected error during writing selection: {err}", "Writing failed, process ended."], + ) + +# globals - GUI event handlers +csv_handler = CSVHandler() +g_sheet_handler = GSheetHandler() -# global to hold contents uploaded from JS -# not really - now just a GUI event handler more or less... -csv_files = FileContents() + +####################### +# CSV functions for eel +####################### @eel.expose -def handle_category_contents(file_contents): - csv_files.add_category_content(file_contents) +def handle_csv_file_features_content(file_contents): + csv_handler.add_feature_content(file_contents) # 'selection' means people... @eel.expose -def handle_selection_contents(file_contents): - csv_files.add_selection_content(file_contents) +def handle_csv_file_people_content(file_contents): + csv_handler.add_people_content(file_contents) + + +@eel.expose +def update_csv_panel_size(panel_size): + csv_handler.update_panel_size(panel_size) + + +@eel.expose +def csv_run_selection(): + csv_handler.run_selection(test_selection=False) + + +@eel.expose +def csv_run_test_selection(): + csv_handler.run_selection(test_selection=True) + + +########################### +# G Sheet functions for eel +########################### @eel.expose def update_g_sheet_name(g_sheet_name): - csv_files.update_g_sheet_name(g_sheet_name) + g_sheet_handler.update_g_sheet_name(g_sheet_name) @eel.expose def load_g_sheet(): - csv_files.load_g_sheet() + g_sheet_handler.load_g_sheet() ############################# ###Start Advanced Settings### ############################# @eel.expose -def update_respondents_tab_name(respondents_tab_name): - csv_files.update_respondents_tab_name(respondents_tab_name) +def update_respondents_tab_name(people_tab_name): + g_sheet_handler.update_people_tab_name(people_tab_name) @eel.expose def reload_respondents_tab(): - csv_files.update_respondents_tab_name("") + g_sheet_handler.update_people_tab_name("") @eel.expose -def update_categories_tab_name(categories_tab_name): - csv_files.update_categories_tab_name(categories_tab_name) +def update_features_tab_name(features_tab_name): + g_sheet_handler.update_features_tab_name(features_tab_name) @eel.expose -def reload_categories_tab(): - csv_files.update_categories_tab_name("") +def reload_features_tab(): + g_sheet_handler.update_features_tab_name("") @eel.expose def update_gen_rem_tab(gen_rem_tab): - csv_files.update_gen_rem_tab(gen_rem_tab) + g_sheet_handler.update_gen_rem_tab(gen_rem_tab) @eel.expose def reload_gen_rem_tab(): - csv_files.update_gen_rem_tab("") + g_sheet_handler.update_gen_rem_tab(gen_rem_tab=False) @eel.expose def update_number_selections(number_selections): - csv_files.update_number_selections(number_selections) + g_sheet_handler.update_number_selections(number_selections) @eel.expose def reload_number_selections(): - csv_files.update_number_selections("") + g_sheet_handler.update_number_selections("") ########################### @@ -320,18 +575,18 @@ def reload_number_selections(): @eel.expose -def update_number_people(number_people): - csv_files.update_number_people(number_people) +def update_g_sheet_panel_size(panel_size): + g_sheet_handler.update_panel_size(panel_size) @eel.expose -def run_selection(): - csv_files.run_selection(test_selection=False) +def g_sheet_run_selection(): + g_sheet_handler.run_selection(test_selection=False) @eel.expose -def run_test_selection(): - csv_files.run_selection(test_selection=True) +def g_sheet_run_test_selection(): + g_sheet_handler.run_selection(test_selection=True) MIN_WINDOWS_VERSION = 10 diff --git a/stratification.py b/stratification.py deleted file mode 100644 index 7344ab8..0000000 --- a/stratification.py +++ /dev/null @@ -1,2398 +0,0 @@ -""" -Python (3) script to do a stratified, random selection from respondents to random mail out - -Copyright (C) 2019-2023 Brett Hennig bsh [AT] sortitionfoundation.org & Paul Gölz goelz (AT) seas.harvard.edu - -This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public -License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later -version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied -warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program; if not, see -. - -Additional permission under GNU GPL version 3 section 7 - -If you modify this Program, or any covered work, by linking or combining it with Gurobi (or a modified version of that -library), containing parts covered by the terms of GUROBI OPTIMIZATION, LLC END-USER LICENSE AGREEMENT, the licensors of -this Program grant you additional permission to convey the resulting work. -""" - -import codecs -import copy -import csv -import random -import typing -from collections.abc import Iterable -from copy import deepcopy -from importlib.util import find_spec -from io import StringIO -from math import log -from pathlib import Path -from typing import Any - -import cvxpy as cp - -# For how to use gspread see: -# https://www.analyticsvidhya.com/blog/2020/07/read-and-update-google-spreadsheets-with-python/ -# and: -# https://github.com/burnash/gspread -# https://gspread.readthedocs.io/en/latest/ -# https://gspread.readthedocs.io/en/latest/api.html -import gspread -import mip -import numpy as np -import toml -from oauth2client.service_account import ServiceAccountCredentials - -# 0 means no debug message, higher number (could) mean more messages -debug = 0 -# numerical deviation accepted as equality when dealing with solvers -EPS = 0.0005 # TODO: Find good value -EPS_NASH = 0.1 -EPS2 = 0.00000001 - -DEFAULT_SETTINGS = """ -# ##################################################################### -# -# IF YOU EDIT THIS FILE YOU NEED TO RESTART THE APPLICATION -# -# ##################################################################### - -# this is written in TOML - https://github.com/toml-lang/toml - -# this is the name of the (unique) field for each person -id_column = "nationbuilder_id" - -# if check_same_address is true, then no 2 people from the same address will be selected -# the comparison checks if the TWO fields listed here are the same for any person -check_same_address = true -check_same_address_columns = [ - "primary_address1", - "zip_royal_mail" -] - -max_attempts = 100 -columns_to_keep = [ - "first_name", - "last_name", - "mobile_number", - "email", - "primary_address1", - "primary_address2", - "primary_city", - "zip_royal_mail", - "tag_list", - "age", - "gender" -] - -# selection_algorithm can either be "legacy", "maximin", "leximin", or "nash" -selection_algorithm = "leximin" - -# random number seed - if this is NOT zero then it is used to set the random number generator seed -random_number_seed = 0 -""" - - -class Settings: - def __init__( - self, - id_column, - columns_to_keep, - check_same_address, - check_same_address_columns, - max_attempts, - selection_algorithm, - random_number_seed, - json_file_path, - ): - try: - assert isinstance(id_column, str) - assert isinstance(columns_to_keep, list) - # if they have no personal data then columns_to_keep could be empty - for column in columns_to_keep: - assert isinstance(column, str) - assert isinstance(check_same_address, bool) - assert isinstance(check_same_address_columns, list) - # this could be empty - assert len(check_same_address_columns) in (0, 2) - for column in check_same_address_columns: - assert isinstance(column, str) - assert isinstance(max_attempts, int) - assert isinstance(random_number_seed, int) - assert selection_algorithm in ["legacy", "maximin", "nash"] - except AssertionError as error: - print(error) - - self.id_column = id_column - self.columns_to_keep = columns_to_keep - self.check_same_address = check_same_address - self.check_same_address_columns = check_same_address_columns - self.max_attempts = max_attempts - self.selection_algorithm = selection_algorithm - self.random_number_seed = random_number_seed - self.json_file_path = json_file_path - - @classmethod - def load_from_file(cls): - all_msg: list[str] = [] - settings_file_path = Path.home() / "sf_stratification_settings.toml" - if not settings_file_path.is_file(): - settings_file_path.write_text(DEFAULT_SETTINGS, encoding="utf-8") - all_msg.append( - f"Wrote default settings to '{settings_file_path.absolute()}' - " - f"if editing is required, restart this app.", - ) - with settings_file_path.open(encoding="utf-8") as settings_file: - settings = toml.load(settings_file) - # you can't check an address if there is no info about which columns to check... - if not settings["check_same_address"]: - all_msg.append( - "WARNING: Settings file is such that we do NOT check if respondents have same address.", - ) - settings["check_same_address_columns"] = [] - if len(settings["check_same_address_columns"]) == 0 and settings["check_same_address"]: - all_msg.append( - "\nERROR: in sf_stratification_settings.toml file check_same_address is TRUE " - "but there are no columns listed to check! FIX THIS and RESTART this program!", - ) - settings["json_file_path"] = Path.home() / "secret_do_not_commit.json" - return cls( - settings["id_column"], - settings["columns_to_keep"], - settings["check_same_address"], - settings["check_same_address_columns"], - settings["max_attempts"], - settings["selection_algorithm"], - settings["random_number_seed"], - settings["json_file_path"], - ), "".join(all_msg) - - -# class for throwing error/fail exceptions -class SelectionError(Exception): - def __init__(self, message): - self.msg = message - - -class PeopleAndCats: - """ - The PeopleAndCats classes below hold all the people and category info sourced from - (and written to) the relevant place - - `categories` is a dict of dicts of dicts... like: - - categories = {"gender" : gender, "age" : age, "geo" : geo, "socio" : socio} - - with each category a dict of possible values with set data, like: - - gender = { - "Gender: Male": {"min" : 20, "max" : 24, "selected" : 0, "remaining" : 0}, - "Gender: Female": {"min" : 21, "max" : 25, "selected" : 0, "remaining" : 0} - # etc - } - - Note that there are now optional extra fields in the above: min_flex and max_flex... - """ - - # Warning: all / most of these values are hardcoded also somewhere below :-) - category_file_field_names = ["category", "name", "min", "max", "min_flex", "max_flex"] - - def __init__(self): - # mins and maxs (from category data) for number of people one can select - # min_flex and max_flex are how much we are happy for this to "stretch"... - self.min_max_people = {} - self.original_categories = None - self.categories_after_people = None - self.category_content_loaded = False - self.people_content_loaded = False - # this is the main data structure where all the info about the people are kept, - # including columns to keep, same address column data and category data - # i.e. everything we need to track for these people! - self.people = None - self.columns_data = None - # after selection, this is just "number_selections" lists of people IDs - self.people_selected: list[frozenset[str]] = [] - self.number_people_to_select = 0 - self.number_selections = 1 # default to 1 - why not? - # these, and the two functions below, are the only annoying things needed to distinguish CSV in GUI.. - self.enable_file_download = False - self.gen_rem_tab = "" - - def get_selected_file(self): - return None - - def get_remaining_file(self): - return None - - # read in stratified selection categories and values - a dict of dicts of dicts... - def _read_in_cats(self, cat_head, cat_body) -> tuple[list[str], int, int]: - self.original_categories = {} - all_msg: list[str] = [] - min_val = 0 - max_val = 0 - # to keep track of number in cats - number people selected MUST be between these limits in every cat... - self.min_max_people = {} - # check that the fieldnames are (at least) what we expect, and only once, - # BUT (for reverse compatibility) let min_flex and max_flex be optional - cat_flex = False - try: - if cat_head.count("min_flex") == 1 and cat_head.count("max_flex") == 1: - cat_flex = True - for fn in PeopleAndCats.category_file_field_names: - cat_head_fn_count = cat_head.count(fn) - if cat_head_fn_count == 0 and (fn not in ("min_flex", "max_flex")): - msg = f"Did not find required column name '{fn}' in the input " - raise SelectionError(msg) - if cat_head_fn_count > 1: - msg = f"Found MORE THAN 1 column named '{fn}' in the input (found {cat_head_fn_count}) " - raise SelectionError(msg) - for row in cat_body: - # allow for some dirty data - at least strip white space from cat and name - # but only if they are strings! - # (sometimes people use ints as cat names or values and then strip produces an exception) - cat = row["category"] - # and skip over any blank lines... - if cat == "": - continue - if isinstance(cat, str): - cat = cat.strip() - # check for blank entries and report a meaningful error - cat_value = row["name"] - if cat_value == "" or row["min"] == "" or row["max"] == "": - msg = f"ERROR reading in category file: found a blank cell in a row of the category: {cat}. " - raise SelectionError(msg) - if isinstance(cat_value, str): - cat_value = cat_value.strip() - # must convert min/max to ints - cat_min = int(row["min"]) - cat_max = int(row["max"]) - if cat_flex: - if row["min_flex"] == "" or row["max_flex"] == "": - msg = ( - f"ERROR reading in category file: found a blank min_flex or " - f"max_flex cell in a category value: {cat_value}. " - ) - raise SelectionError(msg) - cat_min_flex = int(row["min_flex"]) - cat_max_flex = int(row["max_flex"]) - # if these values exist they must be at least this... - if cat_min_flex > cat_min or cat_max_flex < cat_max: - msg = ( - f"Inconsistent numbers in min_flex and max_flex in the categories input for {cat_value}: " - f"the flex values must be equal or outside the max and min values. " - ) - raise SelectionError(msg) - else: - cat_min_flex = 0 - # since we don't know self.number_people_to_select yet! We correct this below - cat_max_flex = -1 - if cat in self.original_categories: - self.min_max_people[cat]["min"] += cat_min - self.min_max_people[cat]["max"] += cat_max - self.original_categories[cat].update( - { - str(cat_value): { ###forcing this to be a string - "min": cat_min, - "max": cat_max, - "selected": 0, - "remaining": 0, - "min_flex": cat_min_flex, - "max_flex": cat_max_flex, - }, - }, - ) - else: - self.min_max_people.update( - { - cat: { - "min": cat_min, - "max": cat_max, - }, - }, - ) - self.original_categories.update( - { - cat: { - str(cat_value): { ###forcing this to be a string - "min": cat_min, - "max": cat_max, - "selected": 0, - "remaining": 0, - "min_flex": cat_min_flex, - "max_flex": cat_max_flex, - }, - }, - }, - ) - - all_msg.append(f"Number of categories: {len(self.original_categories.keys())}") - - # work out what the min and max number of people should be, - # given these cats - max_values = [v["max"] for v in self.min_max_people.values()] - max_val = min(max_values) - # to avoid errors, if max_flex is not set we must set it at least as high as the highest - max_flex_val = max(max_values) - min_values = [v["min"] for v in self.min_max_people.values()] - min_val = max(min_values) - # if the min is bigger than the max we're in trouble i.e. there's an input error - if min_val > max_val: - msg = ( - "Inconsistent numbers in min and max in the categories input: " - "the sum of the minimum values of a category is larger than " - "the sum of the maximum values of a(nother) category." - ) - raise SelectionError(msg) - - # check cat_flex to see if we need to set the max here - # this is only used if these (optional) flex values are NOT given - for cat_values in self.original_categories.values(): - for cat_value in cat_values.values(): - if cat_value["max_flex"] == -1: - # this must be bigger than the largest max - and could even be more than number of people - cat_value["max_flex"] = max_flex_val - - except Exception as error: # noqa: BLE001 - all_msg.append(f"Error loading categories: {error}") - return all_msg, min_val, max_val - - # simple helper function to tidy the code below - def _check_columns_exist_or_multiple(self, people_head, column_list, error_text): - for column in column_list: - column_count = people_head.count(column) - if column_count == 0: - msg = f"No '{column}' column {error_text} found in people data!" - raise SelectionError(msg) - if column_count > 1: - msg = f"MORE THAN 1 '{column}' column {error_text} found in people data!" - raise SelectionError(msg) - - # read in people and calculate how many people in each category in database - def _init_categories_people(self, people_head, people_body, settings: Settings) -> list[str]: - people = {} - columns_data = {} - # this modifies the categories, so we keep the original categories here - self.categories_after_people = deepcopy(self.original_categories) - categories = self.categories_after_people - # check that id_column and all the categories, columns_to_keep and check_same_address_columns are in - # the people data fields... - all_msgs: list[str] = [] - try: - # check both for existence and duplicate column names - self._check_columns_exist_or_multiple(people_head, [settings.id_column], "(unique id)") - self._check_columns_exist_or_multiple(people_head, categories.keys(), "(a category)") - self._check_columns_exist_or_multiple( - people_head, - settings.columns_to_keep, - "(to keep)", - ) - self._check_columns_exist_or_multiple( - people_head, - settings.check_same_address_columns, - "(to check same address)", - ) - # let's just merge the check_same_address_columns into columns_to_keep in case they aren't in both - for col in settings.check_same_address_columns: - if col not in settings.columns_to_keep: - settings.columns_to_keep.append(col) - for row in people_body: - pkey = row[settings.id_column] - # skip over any blank lines... but warn the user - if pkey == "": - all_msgs.append("WARNING: blank cell found in ID column - skipped that line!") - continue - value = {} - # get the category values: these are the most important and we must check them - for cat_key, cats in categories.items(): - # check for input errors here - if it's not in the list of category values... - # allow for some unclean data - at least strip empty space, but only if a str! - # (some values will can be numbers) - p_value = row[cat_key] - if isinstance(row[cat_key], str): - p_value = p_value.strip() - if p_value not in cats: - msg = ( - f"ERROR reading in people (init_categories_people): " - f"Person (id = {pkey}) has value '{p_value}' not in category '{cat_key}'" - ) - raise SelectionError(msg) - value.update({cat_key: p_value}) - categories[cat_key][p_value]["remaining"] += 1 - # then get the other column values we need - # this is address, name etc that we need to keep for output file - # we don't check anything here - it's just for user convenience - col_value = {} - for col in settings.columns_to_keep: - value.update({col: row[col]}) - col_value.update({col: row[col]}) - # add all the data to our people object - people.update({pkey: value}) - columns_data.update({pkey: col_value}) - # check if any cat[max] is set to zero... if so delete everyone with that cat... - # NOT DONE: could then check if anyone is left... - total_num_people = len(people.keys()) - all_msgs.append(f"Number of people: {total_num_people}.") - total_num_deleted = 0 - for cat_key, cats in categories.items(): - for cat, cat_item in cats.items(): - if cat_item["max"] == 0: # we don't want any of these people - # pass the message in as deleting them might throw an exception - all_msgs.append(f"Category {cat} full - deleting people...") - num_deleted, num_left = delete_all_in_cat(categories, people, cat_key, cat) - # if no expcetion was thrown above add this bit to the end of the previous message - all_msgs[-1] += f" Deleted {num_deleted}, {num_left} left." - total_num_deleted += num_deleted - # if the total number of people deleted is lots then we're probably doing a replacement selection, - # which means the 'remaining' file will be useless - remind the user of this! - if total_num_deleted > total_num_people / 2: - all_msgs.append( - ">>> WARNING <<< That deleted MANY PEOPLE - are you doing a replacement? " - "If so your REMAINING FILE WILL BE USELESS!!!", - ) - self.people = people - self.columns_data = columns_data - except Exception as error: # noqa: BLE001 - self.people_content_loaded = False - all_msgs.append(f"Error loading people: {error}") - return all_msgs - - def people_cats_run_stratification(self, settings: Settings, test_selection: bool): - # if this is being called again (the user hit the button again!) we want to make sure all data is cleared etc - # but the function called here makes deep copies of categories_after_people and people - self.people_selected = [] - success, self.people_selected, output_lines = run_stratification( - self.categories_after_people, - self.people, - self.columns_data, - self.number_people_to_select, - self.min_max_people, - settings, - test_selection, - self.number_selections, - ) - if success: - # this also outputs them... - output_lines += self._get_selected_people_lists(settings) - return success, output_lines - - # this also outputs them by calling the appropriate derived class method... - # currently this DOES NOT WORK if self.number_selections > 1 ... - def _get_selected_people_lists(self, settings: Settings): - people_working = copy.deepcopy(self.people) - people_selected = self.people_selected - output_lines = [] - - # if we are doing a multiple selection (only possible from G-sheet at the moment) just spit out selected as is - # and no remaining tab - assert len(people_selected) == self.number_selections - if self.number_selections > 1: - # people_selected should be list of frozensets... - people_selected_header_row = [] - for index in range(self.number_selections): - people_selected_header_row += [f"Assembly {index}"] - # initialise an empty 2d list - yes, not pythonic... - people_selected_rows = [[""] * self.number_selections for _ in range(self.number_people_to_select)] - people_remaining_rows = [[]] - # put all the assemblies in columns of the output - for set_count, fset in enumerate(people_selected): - for p_count, pkey in enumerate(fset): - people_selected_rows[p_count][set_count] = pkey - # prepend the header row afterwards - people_selected_rows.insert(0, people_selected_header_row) - self._output_selected_remaining(settings, people_selected_rows, people_remaining_rows) - else: # self.number_selections == 1 - categories = self.categories_after_people - - # columns_to_keep ALSO contains check_same_address_columns - people_selected_rows = [ - [settings.id_column, *settings.columns_to_keep, *list(categories.keys())], - ] - people_remaining_rows = [ - [settings.id_column, *settings.columns_to_keep, *list(categories.keys())], - ] - - num_same_address_deleted = 0 - for pkey in people_selected[0]: - row = [pkey] - # this is also just all in here, but in an unordered mess... - row += [people_working[pkey][col] for col in settings.columns_to_keep] - row += [people_working[pkey][cat_key] for cat_key in categories] - people_selected_rows.append(row) - # if check address then delete all those at this address (will NOT delete the one we want as well) - if settings.check_same_address: - people_to_delete, new_output_lines = get_people_at_same_address( - people_working, - pkey, - settings.check_same_address_columns, - ) - output_lines += new_output_lines - num_same_address_deleted += len(new_output_lines) # don't include original - # then delete this/these people at the same address from the reserve/remaining pool - del people_working[pkey] - num_same_address_deleted += 1 - for del_person_key in people_to_delete: - del people_working[del_person_key] - else: - del people_working[pkey] - - # add the columns to keep into remaining people - # as above all these values are all in people_working but this is tidier... - for pkey in people_working: - row = [pkey] - for col in settings.columns_to_keep: - row.append(people_working[pkey][col]) - for cat_key in categories: - row.append(people_working[pkey][cat_key]) - people_remaining_rows += [row] - dupes = self._output_selected_remaining( - settings, - people_selected_rows, - people_remaining_rows, - ) - if settings.check_same_address and self.gen_rem_tab == "on": - output_lines.append( - f"Deleted {num_same_address_deleted} people from remaining file who had the same " - f"address as selected people.", - ) - m = min(30, len(dupes)) - output_lines.append( - f"In the remaining tab there are {len(dupes)} people who share the same address as " - f"someone else in the tab. We highlighted the first {m} of these. " - f"The full list of lines is {dupes}", - ) - return output_lines - - -class PeopleAndCatsCSV(PeopleAndCats): - def __init__(self): - super().__init__() - self.selected_file = StringIO() - self.remaining_file = StringIO() - - def get_selected_file(self): - return self.selected_file - - def get_remaining_file(self): - return self.remaining_file - - def load_cats(self, file_contents, dummy_category_tab, settings: Settings): - self.category_content_loaded = True - category_file = StringIO(file_contents) - category_reader = csv.DictReader(category_file) - return self._read_in_cats(list(category_reader.fieldnames), category_reader) - - def load_people( - self, - settings: Settings, - file_contents="", - dummy_respondents_tab="", - dummy_category_tab="", - dummy_gen_rem="", - ): - if file_contents != "": - self.people_content_loaded = True - people_file = StringIO(file_contents) - people_data = csv.DictReader(people_file) - return self._init_categories_people(list(people_data.fieldnames), people_data, settings) - - # Actually useful to also write to a file all those who are NOT selected for later selection if people pull out etc - # BUT, we should not include in this people from the same address as someone who has been selected! - def _output_selected_remaining( - self, - settings: Settings, - people_selected_rows, - people_remaining_rows, - ): - # we have succeeded in CSV so can activate buttons in GUI... - self.enable_file_download = True - - people_selected_writer = csv.writer( - self.selected_file, - delimiter=",", - quotechar='"', - quoting=csv.QUOTE_MINIMAL, - ) - for row in people_selected_rows: - people_selected_writer.writerow(row) - - people_remaining_writer = csv.writer( - self.remaining_file, - delimiter=",", - quotechar='"', - quoting=csv.QUOTE_MINIMAL, - ) - for row in people_remaining_rows: - people_remaining_writer.writerow(row) - - -class PeopleAndCatsGoogleSheet(PeopleAndCats): - scope = None - creds = None - client = None - original_selected_tab_name = "Original Selected - output - " - selected_tab_name = "Selected" - columns_selected_first = "C" - column_selected_blank_num = 6 - remaining_tab_name = "Remaining - output - " - new_tab_default_size_rows = "2" - new_tab_default_size_cols = "40" - - def __init__(self): - super().__init__() - self.g_sheet_name = "" - self.respondents_tab_name = "" - self.category_tab_name = "" - self.spreadsheet = None - - def _tab_exists(self, tab_name): - if self.spreadsheet is None: - return False - tab_list = self.spreadsheet.worksheets() - return any(tab.title == tab_name for tab in tab_list) - - def _clear_or_create_tab(self, tab_name, other_tab_name, inc): - # this now does not clear data but increments the sheet number... - num = 0 - tab_ready = None - tab_name_new = tab_name + str(num) - other_tab_name_new = other_tab_name + str(num) - while tab_ready is None: - if self._tab_exists(tab_name_new) or self._tab_exists(other_tab_name_new): - num += 1 - tab_name_new = tab_name + str(num) - other_tab_name_new = other_tab_name + str(num) - else: - if inc == -1: - tab_name_new = tab_name + str(num - 1) - tab_ready = self.spreadsheet.add_worksheet( - title=tab_name_new, - rows=self.new_tab_default_size_rows, - cols=self.new_tab_default_size_cols, - ) - return tab_ready - - def load_cats(self, g_sheet_name, category_tab_name, settings: Settings): - self.category_content_loaded = True - self.g_sheet_name = g_sheet_name - self.category_tab_name = category_tab_name - - json_file_name = settings.json_file_path - min_val = 0 - max_val = 0 - msg = [] - try: - if self.scope is None: - self.scope = [ - "https://spreadsheets.google.com/feeds", - "https://www.googleapis.com/auth/drive", - ] - self.creds = ServiceAccountCredentials.from_json_keyfile_name( - json_file_name, - self.scope, - ) - self.client = gspread.authorize(self.creds) - self.spreadsheet = self.client.open(self.g_sheet_name) - msg += [f"Opened Google Sheet: '{self.g_sheet_name}'. "] - if self._tab_exists(self.category_tab_name): - tab_cats = self.spreadsheet.worksheet(self.category_tab_name) - cat_head_input = tab_cats.row_values(1) - cat_input = tab_cats.get_all_records(expected_headers=[]) - new_msg, min_val, max_val = self._read_in_cats(cat_head_input, cat_input) - msg += [f"Read in '{self.category_tab_name}' tab in above Google sheet."] - msg += new_msg - else: - msg += [f"Error in Google sheet: no tab called '{self.category_tab_name}' found. "] - self.category_content_loaded = False - except gspread.SpreadsheetNotFound: - msg += [f"Google spreadsheet not found: {self.g_sheet_name}. "] - self.category_content_loaded = False - return msg, min_val, max_val - - def load_people( - self, - settings: Settings, - dummy_file_contents, - respondents_tab_name, - category_tab_name, - gen_rem_tab, - ): - self.people_content_loaded = True - self.respondents_tab_name = respondents_tab_name # Added for respondents tab text box. - self.category_tab_name = category_tab_name # Added for category tab text box. - self.gen_rem_tab = gen_rem_tab # Added for checkbox. - msg = [] - try: - if self._tab_exists(self.respondents_tab_name): - tab_people = self.spreadsheet.worksheet(self.respondents_tab_name) - # if we don't read this in here we can't check if there are 2 columns with the same name - people_head_input = tab_people.row_values(1) - # the numericise_ignore doesn't convert the phone numbers to ints... - # 1 Oct 2024: the final argument with expected_headers is to deal with the fact that - # updated versions of gspread can't cope with duplicate headers - people_input = tab_people.get_all_records( - numericise_ignore=["all"], - expected_headers=[], - ) - msg = [f"Reading in '{self.respondents_tab_name}' tab in above Google sheet."] - msg += self._init_categories_people(people_head_input, people_input, settings) - else: - msg = [ - f"Error in Google sheet: no tab called '{self.respondents_tab_name}' found. ", - ] - self.people_content_loaded = False - except gspread.SpreadsheetNotFound: - msg += [f"Google spreadsheet not found: {self.g_sheet_name}. "] - self.people_content_loaded = False - return msg - - def _output_selected_remaining( - self, - settings: Settings, - people_selected_rows, - people_remaining_rows, - ): - # if self.number_selections > 1 then self.gen_rem_tab=='off' - assert self.number_selections == 1 or (self.number_selections > 1 and self.gen_rem_tab == "off") - tab_original_selected = self._clear_or_create_tab( - self.original_selected_tab_name, - self.remaining_tab_name, - 0, - ) - tab_original_selected.update(people_selected_rows) - tab_original_selected.format( - "A1:U1", - {"backgroundColor": {"red": 153 / 255, "green": 204 / 255, "blue": 255 / 255}}, - ) - dupes2 = [] - if self.gen_rem_tab == "on": - tab_remaining = self._clear_or_create_tab( - self.remaining_tab_name, - self.original_selected_tab_name, - -1, - ) - tab_remaining.update(people_remaining_rows) - tab_remaining.format( - "A1:U1", - {"backgroundColor": {"red": 153 / 255, "green": 204 / 255, "blue": 255 / 255}}, - ) - # highlight any people in remaining tab at the same address - if settings.check_same_address: - csa1 = settings.check_same_address_columns[0] - col1 = tab_remaining.find(csa1).col - csa2 = settings.check_same_address_columns[1] - col2 = tab_remaining.find(csa2).col - dupes_set = set() - n = len(people_remaining_rows) - for i in range(n): - rowrem1 = people_remaining_rows[i] - for j in range(i + 1, n): - rowrem2 = people_remaining_rows[j] - if ( - rowrem1 != rowrem2 - and rowrem1[col1 - 1] == rowrem2[col1 - 1] - and rowrem1[col2 - 1] == rowrem2[col2 - 1] - ): - dupes_set.add(i + 1) - dupes_set.add(j + 1) - dupes = sorted(dupes_set) - m = min(30, len(dupes)) - for i in range(m): - tab_remaining.format( - str(dupes[i]), - {"backgroundColor": {"red": 5, "green": 2.5, "blue": 0}}, - ) - return dupes2 - - -################################### -# -# End PeopleAndCats classes... -# -# ... of course in theory almost all of the below functions could be integrated into -# the above (base) class but may be useful to keep separated? Or at least I can't be bothered -# integrating them in now ... -# -################################### - - -# create READABLE example file of people -def create_readable_sample_file( - categories, - people_file: typing.TextIO, - number_people_example_file, - settings: Settings, -): - example_people_writer = csv.writer( - people_file, - delimiter=",", - quotechar='"', - quoting=csv.QUOTE_MINIMAL, - ) - cat_keys = categories.keys() - example_people_writer.writerow([settings.id_column, *settings.columns_to_keep, *list(cat_keys)]) - for x in range(number_people_example_file): - row = [f"p{x}"] - row += [col + str(x) for col in settings.columns_to_keep] - for cats in categories.values(): # e.g. gender - cat_items_list_weighted: list[str] = [] - for cats_key, cats_item in cats.items(): # e.g. male - cat_items_list_weighted += [cats_key for _ in range(cats_item["max"])] - random_cat_value = random.choice(cat_items_list_weighted) - row.append(random_cat_value) - example_people_writer.writerow(row) - - -# when a category is full we want to delete everyone in it -def delete_all_in_cat(categories, people, cat_check_key, cat_check_value): - people_to_delete = [] - for pkey, person in people.items(): - if person[cat_check_key] == cat_check_value: - people_to_delete.append(pkey) - # for pcat, pval in person.items(): - for cat_key in categories: - cat_item = categories[cat_key][person[cat_key]] - cat_item["remaining"] -= 1 - if cat_item["remaining"] == 0 and cat_item["selected"] < cat_item["min"]: - lpd = len(people_to_delete) - msg = ( - f"SELECTION IMPOSSIBLE: FAIL in delete_all_in_cat as after previous deletion " - f"no one/not enough left in {cat_key} {person[cat_key]}. " - f"Tried to delete: {lpd}", - ) - raise SelectionError(msg) - for p in people_to_delete: - del people[p] - # return the number of people deleted and the number of people left - return len(people_to_delete), len(people) - - -# selected = True means we are deleting because they have been chosen, -# otherwise they are being deleted because they live at same address as someone selected -def really_delete_person(categories, people, pkey: str, *, selected: bool) -> None: - for pcat, pval in people[pkey].items(): - if pcat not in categories: - # some categories are just first_name etc - so will - # not be in the target categories - continue - cat_item = categories[pcat][pval] - if selected: - cat_item["selected"] += 1 - cat_item["remaining"] -= 1 - if cat_item["remaining"] == 0 and cat_item["selected"] < cat_item["min"]: - raise SelectionError("FAIL in delete_person: no one left in " + pval) - del people[pkey] - - -def get_people_at_same_address(people, pkey, check_same_address_columns): - primary_address1 = people[pkey][check_same_address_columns[0]] - primary_zip = people[pkey][check_same_address_columns[1]] - # there may be multiple people to delete, and deleting them as we go gives an error - people_to_delete = [] - output_lines = [] - for compare_key in people: - # don't get yourself!? - if ( - pkey != compare_key - and primary_address1 == people[compare_key][check_same_address_columns[0]] - and primary_zip == people[compare_key][check_same_address_columns[1]] - ): - # found same address - if people_to_delete != []: - output_lines += [ - "Found someone with the same address as a selected person," - f" so deleting him/her. Address: {primary_address1} , {primary_zip}", - ] - people_to_delete.append(compare_key) - return people_to_delete, output_lines - - -# lucky person has been selected - delete person from DB -def delete_person(categories, people, pkey, check_same_address, check_same_address_columns): - output_lines = [] - # recalculate all category values that this person was in - person = people[pkey] - # check if there are other people at the same address - if so, remove them! - if check_same_address: - people_to_delete, output_lines = get_people_at_same_address( - people, - pkey, - check_same_address_columns, - ) - # then delete this/these people at the same address - for del_person_key in people_to_delete: - really_delete_person(categories, people, del_person_key, selected=False) - # delete the actual person after checking for people at the same address - really_delete_person(categories, people, pkey, selected=True) - # then check if any cats of selected person is (was) in are full - for pcat, pval in person.items(): - if pcat in categories: - cat_item = categories[pcat][pval] - if cat_item["selected"] == cat_item["max"]: - output_lines += [f"Category {pval} full - deleting people..."] - num_deleted, num_left = delete_all_in_cat(categories, people, pcat, pval) - output_lines[-1] += f" Deleted {num_deleted}, {num_left} left." - return output_lines - - -# returns dict of category key, category item name, random person number -def find_max_ratio_cat(categories): - ratio = -100.0 - key_max = "" - index_max_name = "" - random_person_num = -1 - for cat_key, cats in categories.items(): - for cat, cat_item in cats.items(): - # if there are zero remaining, or if there are less than how many we need we're in trouble - if cat_item["selected"] < cat_item["min"] and cat_item["remaining"] < ( - cat_item["min"] - cat_item["selected"] - ): - msg = f"FAIL in find_max_ratio_cat: No people (or not enough) in category {cat}" - raise SelectionError(msg) - # if there are none remaining, it must be because we have reached max and deleted them - # or, if max = 0, then we don't want any of these (could happen when seeking replacements) - if cat_item["remaining"] != 0 and cat_item["max"] != 0: - item_ratio = (cat_item["min"] - cat_item["selected"]) / float(cat_item["remaining"]) - # print item['name'],': ', item['remaining'], 'ratio : ', item_ratio - if item_ratio > 1: # trouble! - msg = "FAIL in find_max_ratio_cat: a ratio > 1..." - raise SelectionError(msg) - if item_ratio > ratio: - ratio = item_ratio - key_max = cat_key - index_max_name = cat - random_person_num = random.randint(1, cat_item["remaining"]) - if debug > 0: - print(f"Max ratio: {ratio} for {key_max} {index_max_name}") - # could also append random_person_num - return { - "ratio_cat": key_max, - "ratio_cat_val": index_max_name, - "ratio_random": random_person_num, - } - - -# First return is True if selection has been successful, second return are messages -# Print category info - if people_selected is empty it is assumed the output should be how many people initially -def print_category_info(categories, people, people_selected, number_people_wanted): - if len(people_selected) > 1: - return [ - "

We do not calculate target details for multiple selections - please see your output files.

", - ] - initial_print = len(people_selected) == 0 - # count and print - report_msg = "" - if initial_print: - report_msg += "" - else: - report_msg += "" - # create a local version of this to count stuff in, as this might be called from places that don't track this info - # and reset the info just in case it has been used - categories_working = copy.deepcopy(categories) - for cats in categories_working.values(): - for cat_item in cats.values(): - cat_item["selected"] = 0 - # count those either initially or selected, but use the same data item... - if initial_print: - for person in people.values(): - for feature in categories: - value = person[feature] - categories_working[feature][value]["selected"] += 1 - else: - assert len(people_selected) == 1 - for person in people_selected[0]: - for feature in categories: - value = people[person][feature] - categories_working[feature][value]["selected"] += 1 - - # print out how many in each - for cat_key, cats in categories_working.items(): - for cat, cat_item in cats.items(): - if initial_print: # don't bother about percents... - report_msg += "".format( - cat_key, - cat, - cat_item["selected"], - cat_item["min"], - cat_item["max"], - ) - else: - percent_selected = round( - cat_item["selected"] * 100 / float(number_people_wanted), - 2, - ) - report_msg += "".format( - cat_key, - cat, - cat_item["selected"], - percent_selected, - cat_item["min"], - cat_item["max"], - ) - report_msg += "
CategoryInitiallyWant
CategorySelectedWant
{}{}{}[{},{}]
{}{}{} ({}%)[{},{}]
" - return [report_msg] - - -def check_category_selected(categories, people, people_selected, number_selections): - hit_targets = True - last_cat_fail = "" - if number_selections > 1: - return hit_targets, [ - "

No target checks done for multiple selections - please see your output files.

", - ] - # count and print - # create a local version of this to count stuff in, as this might be called from places that don't track this info - # and reset the info just in case it has been used - categories_working = copy.deepcopy(categories) - for cats in categories_working.values(): - for cat_item in cats.values(): - cat_item["selected"] = 0 - # count those selected - but not at the start when people_selected is empty (the initial values should be zero) - if len(people_selected) == 1: - for person in people_selected[0]: - for feature in categories: - value = people[person][feature] - categories_working[feature][value]["selected"] += 1 - # check if quotas have been met or not - for cats in categories_working.values(): - for cat, cat_item in cats.items(): - if cat_item["selected"] < cat_item["min"] or cat_item["selected"] > cat_item["max"]: - hit_targets = False - last_cat_fail = cat - report_msg = f"

Failed to get minimum or got more than maximum in (at least) category: {last_cat_fail}

" - report_msg = ( - "" - if hit_targets is True - else f"

Failed to get minimum or got more than maximum in (at least) category: {last_cat_fail}

" - ) - return hit_targets, [report_msg] - - -def _distribution_stats( - people: dict[str, dict[str, str]], - committees: list[frozenset[str]], - probabilities: list[float], -) -> list[str]: - output_lines = [] - - assert len(committees) == len(probabilities) - num_non_zero = sum([1 for prob in probabilities if prob > 0]) - output_lines.append( - f"Algorithm produced distribution over {len(committees)} committees, out of which " - f"{num_non_zero} are chosen with positive probability.", - ) - - individual_probabilities = {pid: 0 for pid in people} - containing_committees = {pid: [] for pid in people} - for committee, prob in zip(committees, probabilities, strict=False): - if prob > 0: - for cid in committee: - individual_probabilities[cid] += prob - containing_committees[cid].append(committee) - - table = [ - "", - ] - - for _, cid in sorted((prob, cid) for cid, prob in individual_probabilities.items()): - table.append( - f"", - ) - table.append("
Agent IDProbability of selectionIncluded in #" - "of committees
{cid}{individual_probabilities[cid]:.4%}{len(containing_committees[cid])}" - "
") - output_lines.append("".join(table)) - - return output_lines - - -def _output_panel_table(panels: list[frozenset[str]], probs: list[float]): - def panel_to_tuple(panel: frozenset[str]) -> tuple[str]: - return tuple(sorted(panel)) - - k = len(panels[0]) - dist = {} - for panel, prob in zip(panels, probs, strict=False): - assert len(panel) == k - tup = panel_to_tuple(panel) - if tup not in dist: - dist[tup] = 0.0 - dist[tup] += prob - - with codecs.open("table.csv", "w", "utf8") as file: - file.write( - ",".join( - ["Panel number", "Suggested probability"] + [f"agent {i}" for i in range(1, k + 1)], - ), - ) - file.write("\n") - number = 0 - for tup, prob in dist.items(): - if prob > 0: - file.write(f"{number},{prob},") - number += 1 - file.write(",".join(f'"{item}"' for item in tup)) - file.write("\n") - - -def pipage_rounding(marginals: list[tuple[Any, float]]) -> list[Any]: - assert all(0.0 <= p <= 1.0 for _, p in marginals) - - outcomes = [] - while True: - if len(marginals) == 0: - return outcomes - if len(marginals) == 1: - obj, prob = marginals[0] - if random.random() < prob: - outcomes.append(obj) - marginals = [] - else: - obj0, prob0 = marginals[0] - if prob0 > 1.0 - EPS2: - outcomes.append(obj0) - marginals = marginals[1:] - continue - if prob0 < EPS2: - marginals = marginals[1:] - continue - - obj1, prob1 = marginals[1] - if prob1 > 1.0 - EPS2: - outcomes.append(obj1) - marginals = [marginals[0]] + marginals[2:] - continue - if prob1 < EPS2: - marginals = [marginals[0]] + marginals[2:] - continue - - inc0_dec1_amount = min( - 1.0 - prob0, - prob1, - ) # maximal amount that prob0 can be increased and prob1 can be - # decreased before they drop below 0 or above 1 - dec0_inc1_amount = min(prob0, 1.0 - prob1) - choice_probability = dec0_inc1_amount / (inc0_dec1_amount + dec0_inc1_amount) - - if random.random() < choice_probability: # increase prob0 and decrease prob1 - prob0 += inc0_dec1_amount - prob1 -= inc0_dec1_amount - else: - prob0 -= dec0_inc1_amount - prob1 += dec0_inc1_amount - marginals = [(obj0, prob0), (obj1, prob1)] + marginals[2:] - - -def standardize_distribution( - committees: list[frozenset[str]], - probabilities: list[float], -) -> tuple[list[frozenset[str]], list[float]]: - assert len(committees) == len(probabilities) - new_committees = [] - new_probabilities = [] - for committee, prob in zip(committees, probabilities, strict=False): - if prob >= EPS2: - new_committees.append(committee) - new_probabilities.append(prob) - prob_sum = sum(new_probabilities) - new_probabilities = [prob / prob_sum for prob in new_probabilities] - return new_committees, new_probabilities - - -ASSERT_TOLERANCE = 0.0001 - - -def lottery_rounding( - committees: list[frozenset[str]], - probabilities: list[float], - number_selections: int, -) -> list[frozenset[str]]: - assert len(committees) == len(probabilities) - assert number_selections >= 1 - - num_copies = [] - residuals = [] - for _, prob in zip(committees, probabilities, strict=False): - scaled_prob = prob * number_selections - num_copies.append(int(scaled_prob)) # give lower quotas - residuals.append(scaled_prob - int(scaled_prob)) - assert abs(sum(residuals) - round(sum(residuals))) <= ASSERT_TOLERANCE - - rounded_up_indices = pipage_rounding(list(enumerate(residuals))) - assert round(sum(residuals)) == len(rounded_up_indices) - for committee_index in rounded_up_indices: - num_copies[committee_index] += 1 - - committee_lottery = [] - for committee, committee_copies in zip(committees, num_copies, strict=False): - committee_lottery += [committee for _ in range(committee_copies)] - - return committee_lottery - - -def find_random_sample( - categories: dict[str, dict[str, dict[str, int]]], - people: dict[str, dict[str, str]], - columns_data: dict[str, dict[str, str]], - number_people_wanted: int, - check_same_address: bool, - check_same_address_columns: list[str], - selection_algorithm: str, - test_selection: bool, - number_selections: int, -) -> tuple[list[frozenset[str]], list[str]]: - """Main algorithm to try to find one or multiple random committees. - - Args: - categories: categories["feature"]["value"] is a dictionary with keys "min", "max", "min_flex", "max_flex", - "selected", "remaining". - people: people["nationbuilder_id"] is dictionary mapping "feature" to "value" for a person. - columns_data: columns_data["nationbuilder_id"] is dictionary mapping "contact_field" to "value" for a person. - number_people_wanted: - check_same_address: - check_same_address_columns: list of contact fields of columns that have to be equal for being - counted as residing at the same address - selection_algorithm: one out of "legacy", "maximin", "leximin", or "nash" - test_selection: if set, do not do a random selection, but just return some valid panel. Useful for quickly - testing whether quotas are satisfiable, but should always be false for the actual selection! - number_selections: how many panels to return. Most of the time, this should be set to `1`, which means that - a single panel is chosen. When specifying a value n ≥ 2, the function will return a list of length n, - containing multiple panels (some panels might be repeated in the list). In this case the eventual panel - should be drawn uniformly at random from the returned list. - Returns: - (committee_lottery, output_lines) - `committee_lottery` is a list of committees, where each committee is a frozen set of pool member ids guaranteed - to satisfy the constraints on a feasible committee. - `output_lines` is a list of debug strings. - Raises: - InfeasibleQuotasError if the quotas cannot be satisfied, which includes a suggestion for how to modify them. - SelectionError in multiple other failure cases. - Side Effects: - Existing callers assume the "selected" and "remaining" fields in `categories` to be changed. - """ - if not all( - "min_flex" in categories[feature][value] and "max_flex" in categories[feature][value] - for feature in categories - for value in categories[feature] - ): - msg = ( - "By the time they're fed into `find_random_sample`, the `categories` argument should always " - "contain the new fields 'min_flex' and 'max_flex'. If they're not in the categories file, the " - "code should set default values before calling this function." - ) - raise ValueError(msg) - for feature, values in categories.items(): - for value, info in values.items(): - if not (info["min_flex"] <= info["min"] <= info["max"] <= info["max_flex"]): - msg = ( - f"For feature ({feature}: {value}), the different quotas have incompatible values. " - f"It must hold that min_flex ({info['min_flex']}) <= min ({info['min']}) <= max " - f"({info['max']}) <= max_flex ({info['max_flex']})." - ) - raise ValueError(msg) - - if check_same_address and len(check_same_address_columns) == 0: - msg = ( - "Since the algorithm is configured to prevent multiple house members to appear on the same " - "panel (check_same_address = true), check_same_address_columns must not be empty." - ) - raise ValueError(msg) - - # just go quick and nasty so we can hook up our charts ands tables :-) - if test_selection: - print("Running test selection.") - if number_selections != 1: - msg = ( - "Running the test selection does not support generating a transparent lottery, so, if " - "`test_selection` is true, `number_selections` must be 1." - ) - raise ValueError(msg) - return _find_any_committee( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - output_lines = [] - if selection_algorithm == "leximin" and not find_spec("gurobipy"): - output_lines.append( - _print( - "The leximin algorithm requires the optimization library Gurobi to be installed " - "(commercial, free academic licenses available). Switching to the simpler " - "maximin algorithm, which can be run using open source solvers.", - ), - ) - selection_algorithm = "maximin" - - if selection_algorithm == "legacy": - if number_selections != 1: - msg = ( - "Currently, the legacy algorithm does not support generating a transparent lottery, " - "so `number_selections` must be set to 1." - ) - raise ValueError(msg) - return find_random_sample_legacy( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - if selection_algorithm == "leximin": - committees, probabilities, new_output_lines = find_distribution_leximin( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - elif selection_algorithm == "maximin": - committees, probabilities, new_output_lines = find_distribution_maximin( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - elif selection_algorithm == "nash": - committees, probabilities, new_output_lines = find_distribution_nash( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - else: - msg = ( - f"Unknown selection algorithm {selection_algorithm!r}, must be either 'legacy', 'leximin'," - f" 'maximin', or 'nash'." - ) - raise ValueError(msg) - - committees, probabilities = standardize_distribution(committees, probabilities) - if len(committees) > len(people): - print( - "INFO: The distribution over panels what is known as a 'basic solution'. There is no reason for concern " - "about the correctness of your output, but we'd appreciate if you could reach out to panelot" - f"@paulgoelz.de with the following information: algorithm={selection_algorithm}, " - f"num_panels={len(committees)}, num_agents={len(people)}, min_probs={min(probabilities)}.", - ) - - assert len(set(committees)) == len(committees) - - output_lines += new_output_lines - output_lines += _distribution_stats(people, committees, probabilities) - - committee_lottery = lottery_rounding(committees, probabilities, number_selections) - - return committee_lottery, output_lines - - -def find_random_sample_legacy( - categories: dict[str, dict[str, dict[str, int]]], - people: dict[str, dict[str, str]], - columns_data: dict[str, dict[str, str]], - number_people_wanted: int, - check_same_address: bool, - check_same_address_columns: list[str], -) -> tuple[list[frozenset[str]], list[str]]: - output_lines = ["Using legacy algorithm."] - people_selected = set() - for count in range(number_people_wanted): - ratio = find_max_ratio_cat(categories) - # find randomly selected person with the category value - for pkey, pvalue in people.items(): - if pvalue[ratio["ratio_cat"]] == ratio["ratio_cat_val"]: - # found someone with this category value... - ratio["ratio_random"] -= 1 - if ratio["ratio_random"] == 0: # means they are the random one we want - if debug > 0: - print("Found random person in this cat... adding them") - assert pkey not in people_selected - people_selected.add(pkey) - output_lines += delete_person( - categories, - people, - pkey, - check_same_address, - check_same_address_columns, - ) - break - if count < (number_people_wanted - 1) and len(people) == 0: - msg = "Fail! We've run out of people..." - raise SelectionError(msg) - return [frozenset(people_selected)], output_lines - - -def _ilp_results_to_committee(variables: dict[str, mip.entities.Var]) -> frozenset[str]: - try: - res = frozenset(item for item in variables if variables[item].x > 0.5) # noqa: PLR2004 - # unfortunately, MIP sometimes throws generic Exceptions rather than a subclass. - except Exception as error: - msg = f"It seems like some variables does not have a value. Original exception: {error}." - raise ValueError(msg) from error - - return res - - -def _same_address( - columns_data1: dict[str, str], - columns_data2: dict[str, str], - check_same_address_columns: list[str], -) -> bool: - return all(columns_data1[column] == columns_data2[column] for column in check_same_address_columns) - - -def _print(message: str) -> str: - print(message) - return message - - -def _compute_households( - people: dict[str, dict[str, str]], - columns_data: dict[str, dict[str, str]], - check_same_address_columns: list[str], -) -> dict[str, int]: - ids = list(people.keys()) - households = {pid: None for pid in people} # for each agent, the id of the earliest person with same address - - counter = 0 - for i, id1 in enumerate(ids): - if households[id1] is not None: - continue - households[id1] = counter - for id2 in ids[i + 1 :]: - if households[id2] is None and _same_address( - columns_data[id1], - columns_data[id2], - check_same_address_columns, - ): - households[id2] = counter - counter += 1 - - if counter == 1: - print( - "Warning: All pool members live in the same household. Probably, the configuration is wrong?", - ) - - return households - - -class InfeasibleQuotasError(Exception): - def __init__(self, quotas: dict[tuple[str, str], tuple[int, int]], output: list[str]): - self.quotas = quotas - self.output = ["The quotas are infeasible:", *output] - - def __str__(self): - return "\n".join(self.output) - - -class InfeasibleQuotasCantRelaxError(Exception): - def __init__(self, message: str): - self.message = message - - -def _relax_infeasible_quotas( - categories: dict[str, dict[str, dict[str, int]]], - people: dict[str, dict[str, str]], - number_people_wanted: int, - check_same_address: bool, - households: dict[str, int] | None = None, - ensure_inclusion: typing.Collection[Iterable[str]] = ((),), -) -> tuple[dict[tuple[str, str], tuple[int, int]], list[str]]: - """Assuming that the quotas are not satisfiable, suggest a minimal relaxation that would be. - - Args: - categories: quotas in the format described in `find_random_sample` - people: pool members in the format described in `find_random_sample` - number_people_wanted: desired size of the panel - check_same_address: whether members from the same household cannot simultaneously appear - households: if `check_same_address` is given, a dictionary mapping pool member ids to integers representing - households. if two agents have the same value in the dictionary, they are considered to live together. - ensure_inclusion: allows to specify that some panels should contain specific sets of agents. for example, - passing `(("a",), ("b", "c"))` means that the quotas should be relaxed such that some valid panel contains - agent "a" and some valid panel contains both agents "b" and "c". the default of `((),)` just requires - a panel to exist, without further restrictions. - """ - model = mip.Model(sense=mip.MINIMIZE) - model.verbose = debug - - assert len(ensure_inclusion) > 0 # otherwise, the existence of a panel is not required - - # for every feature, a variable for how much the upper and lower quotas are relaxed - feature_values = [(feature, value) for feature in categories for value in categories[feature]] - min_vars = {fv: model.add_var(var_type=mip.INTEGER, lb=0.0) for fv in feature_values} - max_vars = {fv: model.add_var(var_type=mip.INTEGER, lb=0.0) for fv in feature_values} - - # relaxations cannot drop lower quotas below min_flex or upper quotas beyond max_flex - for feature, value in feature_values: - model.add_constr( - categories[feature][value]["min"] - min_vars[(feature, value)] >= categories[feature][value]["min_flex"], - ) - model.add_constr( - categories[feature][value]["max"] + max_vars[(feature, value)] <= categories[feature][value]["max_flex"], - ) - - # we might not be able to select multiple persons from the same household - people_by_household = {} - if check_same_address: - assert households is not None - - for hid, household in households.items(): - if household not in people_by_household: - people_by_household[household] = [] - people_by_household[household].append(hid) - - for inclusion_set in ensure_inclusion: - # for every person, we have a binary variable indicating whether they are in the committee - agent_vars = {pid: model.add_var(var_type=mip.BINARY) for pid in people} - for agent in inclusion_set: - model.add_constr(agent_vars[agent] == 1) - - # we have to select exactly `number_people_wanted` many persons - model.add_constr(mip.xsum(agent_vars.values()) == number_people_wanted) - - # we have to respect the relaxed quotas - for feature, value in feature_values: - number_feature_value_agents = mip.xsum( - agent_vars[pid] for pid, person in people.items() if person[feature] == value - ) - model.add_constr( - number_feature_value_agents >= categories[feature][value]["min"] - min_vars[(feature, value)], - ) - model.add_constr( - number_feature_value_agents <= categories[feature][value]["max"] + max_vars[(feature, value)], - ) - - if check_same_address: - for members in people_by_household.values(): - if len(members) > 1: - model.add_constr(mip.xsum(agent_vars[mid] for mid in members) <= 1) - - def reduction_weight(feature, value): - """Make the algorithm more recluctant to reduce lower quotas that are already low. If the lower quotas was 1, - reducing it one more (to 0) is 3 times more salient than increasing a quota by 1. This bonus tampers off - quickly, reducing from 10 is only 1.2 times as salient as an increase.""" - old_quota = categories[feature][value]["min"] - if old_quota == 0: - return 0 # cannot be relaxed anyway - return 1 + 2 / old_quota - - # we want to minimize the amount by which we have to relax quotas - model.objective = mip.xsum( - [reduction_weight(*fv) * min_vars[fv] for fv in feature_values] + [max_vars[fv] for fv in feature_values], - ) - - # Optimize once without any constraints to check if no feasible committees exist at all. - status = model.optimize() - if status == mip.OptimizationStatus.INFEASIBLE: - msg = ( - "No feasible committees found, even with relaxing the quotas. Most " - "likely, quotas would have to be relaxed beyond what the 'min_flex' and " - "'max_flex' columns allow." - ) - raise InfeasibleQuotasCantRelaxError(msg) - if status != mip.OptimizationStatus.OPTIMAL: - msg = ( - f"No feasible committees found, solver returns code {status} (see " - f"https://docs.python-mip.com/en/latest/classes.html#optimizationstatus). Either the pool " - f"is very bad or something is wrong with the solver." - ) - raise SelectionError(msg) - - output_lines = [] - new_quotas = {} - for fv in feature_values: - feature, value = fv - lower = categories[feature][value]["min"] - round(min_vars[fv].x) - assert lower <= categories[feature][value]["min"] - if lower < categories[feature][value]["min"]: - output_lines.append(f"Recommend lowering lower quota of {feature}:{value} to {lower}.") - upper = categories[feature][value]["max"] + round(max_vars[fv].x) - assert upper >= categories[feature][value]["max"] - if upper > categories[feature][value]["max"]: - assert lower == categories[feature][value]["min"] - output_lines.append(f"Recommend raising upper quota of {feature}:{value} to {upper}.") - new_quotas[fv] = (lower, upper) - - return new_quotas, output_lines - - -def _setup_committee_generation( - categories: dict[str, dict[str, dict[str, int]]], - people: dict[str, dict[str, str]], - number_people_wanted: int, - check_same_address: bool, - households: dict[str, int] | None, -) -> tuple[mip.model.Model, dict[str, mip.entities.Var]]: - model = mip.Model(sense=mip.MAXIMIZE) - model.verbose = debug - - # for every person, we have a binary variable indicating whether they are in the committee - agent_vars = {pid: model.add_var(var_type=mip.BINARY) for pid in people} - - # we have to select exactly `number_people_wanted` many persons - model.add_constr(mip.xsum(agent_vars.values()) == number_people_wanted) - - # we have to respect quotas - for feature, values in categories.items(): - for value in values: - number_feature_value_agents = mip.xsum( - agent_vars[pid] for pid, person in people.items() if person[feature] == value - ) - model.add_constr(number_feature_value_agents >= categories[feature][value]["min"]) - model.add_constr(number_feature_value_agents <= categories[feature][value]["max"]) - - # we might not be able to select multiple persons from the same household - if check_same_address: - people_by_household = {} - for hid, household in households.items(): - if household not in people_by_household: - people_by_household[household] = [] - people_by_household[household].append(hid) - - for members in people_by_household.values(): - if len(members) > 1: - model.add_constr(mip.xsum(agent_vars[mid] for mid in members) <= 1) - - # Optimize once without any constraints to check if no feasible committees exist at all. - status = model.optimize() - if status == mip.OptimizationStatus.INFEASIBLE: - new_quotas, output_lines = _relax_infeasible_quotas( - categories, - people, - number_people_wanted, - check_same_address, - households, - ) - raise InfeasibleQuotasError(new_quotas, output_lines) - if status != mip.OptimizationStatus.OPTIMAL: - msg = ( - f"No feasible committees found, solver returns code {status} (see " - "https://docs.python-mip.com/en/latest/classes.html#optimizationstatus)." - ) - raise SelectionError(msg) - - return model, agent_vars - - -def _find_any_committee( - categories: dict[str, dict[str, dict[str, int]]], - people: dict[str, dict[str, str]], - columns_data: dict[str, dict[str, str]], - number_people_wanted: int, - check_same_address: bool, - check_same_address_columns: list[str], -) -> tuple[list[frozenset[str]], list[str]]: - households = _compute_households(people, columns_data, check_same_address_columns) if check_same_address else None - - model, agent_vars = _setup_committee_generation( - categories, - people, - number_people_wanted, - check_same_address, - households, - ) - committee = _ilp_results_to_committee(agent_vars) - return [committee], [] - - -def _generate_initial_committees( - new_committee_model: mip.model.Model, - agent_vars: dict[str, mip.entities.Var], - multiplicative_weights_rounds: int, -) -> tuple[set[frozenset[str]], frozenset[str], list[str]]: - """To speed up the main iteration of the maximin and Nash algorithms, start from a diverse set of feasible - committees. In particular, each agent that can be included in any committee will be included in at least one of - these committees. - """ - new_output_lines = [] - committees: set[frozenset[str]] = set() # Committees discovered so far - covered_agents: set[str] = set() # All agents included in some committee - - # We begin using a multiplicative-weight stage. Each agent has a weight between 0.99 and 1 - # Note that if all start with weight `1` then we can end up with some committees - # having the wrong number of results. - # Further investigation of this to happen under https://github.com/sortitionfoundation/stratification-app/issues/23 - weights = {item: random.uniform(0.99, 1.0) for item in agent_vars} - for i in range(multiplicative_weights_rounds): - # In each round, we find a - # feasible committee such that the sum of weights of its members is maximal. - new_committee_model.objective = mip.xsum(weights[item] * agent_vars[item] for item in agent_vars) - new_committee_model.optimize() - new_set = _ilp_results_to_committee(agent_vars) - - # We then decrease the weight of each agent in the new committee by a constant factor. As a result, future - # rounds will strongly prioritize including agents that appear in few committees. - for item in new_set: - weights[item] *= 0.8 - # We rescale the weights, which does not change the conceptual algorithm but prevents floating point problems. - coefficient_sum = sum(weights.values()) - for item in agent_vars: - weights[item] *= len(agent_vars) / coefficient_sum - - if new_set not in committees: - # We found a new committee, and repeat. - committees.add(new_set) - for item in new_set: - covered_agents.add(item) - else: - # If our committee is already known, make all weights a bit more equal again to mix things up a little. - for item in agent_vars: - weights[item] = 0.9 * weights[item] + 0.1 - - print( - f"Multiplicative weights phase, round {i + 1}/{multiplicative_weights_rounds}. Discovered {len(committees)}" - " committees so far.", - ) - - # If there are any agents that have not been included so far, try to find a committee including this specific agent. - for item, value in agent_vars.items(): - if item not in covered_agents: - new_committee_model.objective = value # only care about agent `item` being included. - new_committee_model.optimize() - new_set: frozenset[str] = _ilp_results_to_committee(agent_vars) - if item in new_set: - committees.add(new_set) - for id2 in new_set: - covered_agents.add(id2) - else: - new_output_lines.append( - _print(f"Agent {item} not contained in any feasible committee."), - ) - - # We assume in this stage that the quotas are feasible. - assert len(committees) >= 1 - - if len(covered_agents) == len(agent_vars): - new_output_lines.append(_print("All agents are contained in some feasible committee.")) - - return committees, frozenset(covered_agents), new_output_lines - - -def _dual_leximin_stage( - people: dict[str, dict[str, str]], - committees: set[frozenset[str]], - fixed_probabilities: dict[str, float], -): - """This implements the dual LP described in `find_distribution_leximin`, but where P only ranges over the panels - in `committees` rather than over all feasible panels: - minimize ŷ - Σ_{i in fixed_probabilities} fixed_probabilities[i] * yᵢ - s.t. Σ_{i ∈ P} yᵢ ≤ ŷ ∀ P - Σ_{i not in fixed_probabilities} yᵢ = 1 - ŷ, yᵢ ≥ 0 ∀ i - - Returns a Tuple[grb.Model, Dict[str, grb.Var], grb.Var] (not in type signature to prevent global gurobi import.) - """ - import gurobipy as grb - - assert len(committees) != 0 - - model = grb.Model() - agent_vars = {person: model.addVar(vtype=grb.GRB.CONTINUOUS, lb=0.0) for person in people} # yᵢ - cap_var = model.addVar(vtype=grb.GRB.CONTINUOUS, lb=0.0) # ŷ - model.addConstr( - grb.quicksum(agent_vars[person] for person in people if person not in fixed_probabilities) == 1, - ) - for committee in committees: - model.addConstr(grb.quicksum(agent_vars[person] for person in committee) <= cap_var) - model.setObjective( - cap_var - grb.quicksum(fixed_probabilities[person] * agent_vars[person] for person in fixed_probabilities), - grb.GRB.MINIMIZE, - ) - - # Change Gurobi configuration to encourage strictly complementary (“inner”) solutions. These solutions will - # typically allow to fix more probabilities per outer loop of the leximin algorithm. - model.setParam("Method", 2) # optimize via barrier only - model.setParam("Crossover", 0) # deactivate cross-over - - return model, agent_vars, cap_var - - -def find_distribution_leximin( - categories: dict[str, dict[str, dict[str, int]]], - people: dict[str, dict[str, str]], - columns_data: dict[str, dict[str, str]], - number_people_wanted: int, - check_same_address: bool, - check_same_address_columns: list[str], -) -> tuple[list[frozenset[str]], list[float], list[str]]: - """Find a distribution over feasible committees that maximizes the minimum probability of an agent being selected - (just like maximin), but breaks ties to maximize the second-lowest probability, breaks further ties to maximize the - third-lowest probability and so forth. - - Arguments follow the pattern of `find_random_sample`. - - Returns: - (committees, probabilities, output_lines) - `committees` is a list of feasible committees, where each committee is represented by a frozen set of included - agent ids. - `probabilities` is a list of probabilities of equal length, describing the probability with which each committee - should be selected. - `output_lines` is a list of debug strings. - """ - import gurobipy as grb - - output_lines = ["Using leximin algorithm."] - grb.setParam("OutputFlag", 0) - - households = _compute_households(people, columns_data, check_same_address_columns) if check_same_address else None - - # Set up an ILP `new_committee_model` that can be used for discovering new feasible committees maximizing some - # sum of weights over the agents. - new_committee_model, agent_vars = _setup_committee_generation( - categories, - people, - number_people_wanted, - check_same_address, - households, - ) - - # Start by finding some initial committees, guaranteed to cover every agent that can be covered by some committee - committees: set[frozenset[str]] # set of feasible committees, add more over time - covered_agents: frozenset[str] # all agent ids for agents that can actually be included - committees, covered_agents, new_output_lines = _generate_initial_committees( - new_committee_model, - agent_vars, - 3 * len(people), - ) - output_lines += new_output_lines - - # Over the course of the algorithm, the selection probabilities of more and more agents get fixed to a certain value - fixed_probabilities: dict[str, float] = {} - - reduction_counter = 0 - - # The outer loop maximizes the minimum of all unfixed probabilities while satisfying the fixed probabilities. - # In each iteration, at least one more probability is fixed, but often more than one. - while len(fixed_probabilities) < len(people): - print(f"Fixed {len(fixed_probabilities)}/{len(people)} probabilities.") - - dual_model, dual_agent_vars, dual_cap_var = _dual_leximin_stage( - people, - committees, - fixed_probabilities, - ) - # In the inner loop, there is a column generation for maximizing the minimum of all unfixed probabilities - while True: - """The primal LP being solved by column generation, with a variable x_P for each feasible panel P: - - maximize z - s.t. Σ_{P : i ∈ P} x_P ≥ z ∀ i not in fixed_probabilities - Σ_{P : i ∈ P} x_P ≥ fixed_probabilities[i] ∀ i in fixed_probabilities - Σ_P x_P ≤ 1 (This should be thought of as equality, and wlog. - optimal solutions have equality, but simplifies dual) - x_P ≥ 0 ∀ P - - We instead solve its dual linear program: - minimize ŷ - Σ_{i in fixed_probabilities} fixed_probabilities[i] * yᵢ - s.t. Σ_{i ∈ P} yᵢ ≤ ŷ ∀ P - Σ_{i not in fixed_probabilities} yᵢ = 1 - ŷ, yᵢ ≥ 0 ∀ i - """ - dual_model.optimize() - if dual_model.status != grb.GRB.OPTIMAL: - # In theory, the LP is feasible in the first iterations, and we only add constraints (by fixing - # probabilities) that preserve feasibility. Due to floating-point issues, however, it may happen that - # Gurobi still cannot satisfy all the fixed probabilities in the primal (meaning that the dual will be - # unbounded). In this case, we slightly relax the LP by slightly reducing all fixed probabilities. - for agent in fixed_probabilities: - # Relax all fixed probabilities by a small constant - fixed_probabilities[agent] = max(0.0, fixed_probabilities[agent] - 0.0001) - dual_model, dual_agent_vars, dual_cap_var = _dual_leximin_stage( - people, - committees, - fixed_probabilities, - ) - print(dual_model.status, f"REDUCE PROBS for {reduction_counter}th time.") - reduction_counter += 1 - continue - - # Find the panel P for which Σ_{i ∈ P} yᵢ is largest, i.e., for which Σ_{i ∈ P} yᵢ ≤ ŷ is tightest - agent_weights = {person: agent_var.x for person, agent_var in dual_agent_vars.items()} - new_committee_model.objective = mip.xsum(agent_weights[person] * agent_vars[person] for person in people) - new_committee_model.optimize() - new_set = _ilp_results_to_committee(agent_vars) # panel P - value = new_committee_model.objective_value # Σ_{i ∈ P} yᵢ - - upper = dual_cap_var.x # ŷ - dual_obj = dual_model.objVal # ŷ - Σ_{i in fixed_probabilities} fixed_probabilities[i] * yᵢ - - output_lines.append( - _print( - f"Maximin is at most {dual_obj - upper + value:.2%}, can do {dual_obj:.2%} with " - f"{len(committees)} committees. Gap {value - upper:.2%}.", - ), - ) - if value <= upper + EPS: - # Within numeric tolerance, the panels in `committees` are enough to constrain the dual, i.e., they are - # enough to support an optimal primal solution. - for person, agent_weight in agent_weights.items(): - if agent_weight > EPS and person not in fixed_probabilities: - # `agent_weight` is the dual variable yᵢ of the constraint "Σ_{P : i ∈ P} x_P ≥ z" for - # i = `person` in the primal LP. If yᵢ is positive, this means that the constraint must be - # binding in all optimal solutions [1], and we can fix `person`'s probability to the - # optimal value of the primal/dual LP. - # [1] Theorem 3.3 in: Renato Pelessoni. Some remarks on the use of the strict complementarity in - # checking coherence and extending coherent probabilities. 1998. - fixed_probabilities[person] = max(0, dual_obj) - break - # Given that Σ_{i ∈ P} yᵢ > ŷ, the current solution to `dual_model` is not yet a solution to the dual. - # Thus, add the constraint for panel P and recurse. - assert new_set not in committees - committees.add(new_set) - dual_model.addConstr( - grb.quicksum(dual_agent_vars[item] for item in new_set) <= dual_cap_var, - ) - - # The previous algorithm computed the leximin selection probabilities of each agent and a set of panels such that - # the selection probabilities can be obtained by randomizing over these panels. Here, such a randomization is found. - primal = grb.Model() - # Variables for the output probabilities of the different panels - committee_vars = [primal.addVar(vtype=grb.GRB.CONTINUOUS, lb=0.0) for _ in committees] - # To avoid numerical problems, we formally minimize the largest downward deviation from the fixed probabilities. - eps = primal.addVar(vtype=grb.GRB.CONTINUOUS, lb=0.0) - primal.addConstr(grb.quicksum(committee_vars) == 1) # Probabilities add up to 1 - for person, prob in fixed_probabilities.items(): - person_probability = grb.quicksum( - comm_var for committee, comm_var in zip(committees, committee_vars, strict=False) if person in committee - ) - primal.addConstr(person_probability >= prob - eps) - primal.setObjective(eps, grb.GRB.MINIMIZE) - primal.optimize() - - # Bound variables between 0 and 1 and renormalize, because np.random.choice is sensitive to small deviations here - probabilities = np.array([comm_var.x for comm_var in committee_vars]).clip(0, 1) - probabilities = list(probabilities / sum(probabilities)) - - return list(committees), probabilities, output_lines - - -def _find_maximin_primal( - committees: list[frozenset[str]], - covered_agents: frozenset[str], -) -> list[float]: - model = mip.Model(sense=mip.MAXIMIZE) - - committee_variables = [model.add_var(var_type=mip.CONTINUOUS, lb=0.0, ub=1.0) for _ in committees] - model.add_constr(mip.xsum(committee_variables) == 1) - agent_panel_variables = {item: [] for item in covered_agents} - for committee, var in zip(committees, committee_variables, strict=False): - for item in committee: - if item in covered_agents: - agent_panel_variables[item].append(var) - - lower = model.add_var(var_type=mip.CONTINUOUS, lb=0.0, ub=1.0) - - for agent_variables in agent_panel_variables.values(): - model.add_constr(lower <= mip.xsum(agent_variables)) - model.objective = lower - model.optimize() - - probabilities = [var.x for var in committee_variables] - probabilities = [max(p, 0) for p in probabilities] - sum_probabilities = sum(probabilities) - return [p / sum_probabilities for p in probabilities] - - -def find_distribution_maximin( - categories: dict[str, dict[str, dict[str, int]]], - people: dict[str, dict[str, str]], - columns_data: dict[str, dict[str, str]], - number_people_wanted: int, - check_same_address: bool, - check_same_address_columns: list[str], -) -> tuple[list[frozenset[str]], list[float], list[str]]: - """Find a distribution over feasible committees that maximizes the minimum probability of an agent being selected. - - Arguments follow the pattern of `find_random_sample`. - - Returns: - (committees, probabilities, output_lines) - `committees` is a list of feasible committees, where each committee is represented by a frozen set of included - agent ids. - `probabilities` is a list of probabilities of equal length, describing the probability with which each committee - should be selected. - `output_lines` is a list of debug strings. - """ - output_lines = [_print("Using maximin algorithm.")] - - households = _compute_households(people, columns_data, check_same_address_columns) if check_same_address else None - - # Set up an ILP `new_committee_model` that can be used for discovering new feasible committees maximizing some - # sum of weights over the agents. - new_committee_model, agent_vars = _setup_committee_generation( - categories, - people, - number_people_wanted, - check_same_address, - households, - ) - - # Start by finding some initial committees, guaranteed to cover every agent that can be covered by some committee - committees: set[frozenset[str]] # set of feasible committees, add more over time - covered_agents: frozenset[str] # all agent ids for agents that can actually be included - committees, covered_agents, new_output_lines = _generate_initial_committees( - new_committee_model, - agent_vars, - len(people), - ) - output_lines += new_output_lines - - # The incremental model is an LP with a variable y_e for each entitlement e and one more variable z. - # For an agent i, let e(i) denote her entitlement. Then, the LP is: - # - # minimize z - # s.t. Σ_{i ∈ B} y_{e(i)} ≤ z ∀ feasible committees B (*) - # Σ_e y_e = 1 - # y_e ≥ 0 ∀ e - # - # At any point in time, constraint (*) is only enforced for the committees in `committees`. By linear-programming - # duality, if the optimal solution with these reduced constraints satisfies all possible constraints, the committees - # in `committees` are enough to find the maximin distribution among them. - incremental_model = mip.Model(sense=mip.MINIMIZE) - incremental_model.verbose = debug - - upper_bound = incremental_model.add_var( - var_type=mip.CONTINUOUS, - lb=0.0, - ub=mip.INF, - ) # variable z - # variables y_e - incr_agent_vars = { - item: incremental_model.add_var(var_type=mip.CONTINUOUS, lb=0.0, ub=1.0) for item in covered_agents - } - - # Σ_e y_e = 1 - incremental_model.add_constr(mip.xsum(incr_agent_vars.values()) == 1) - # minimize z - incremental_model.objective = upper_bound - - for committee in committees: - committee_sum = mip.xsum([incr_agent_vars[cid] for cid in committee]) - # Σ_{i ∈ B} y_{e(i)} ≤ z ∀ B ∈ `committees` - incremental_model.add_constr(committee_sum <= upper_bound) - - while True: - status = incremental_model.optimize() - assert status == mip.OptimizationStatus.OPTIMAL - - # currently optimal values for y_e - entitlement_weights = {item: incr_agent_vars[item].x for item in covered_agents} - upper = upper_bound.x # currently optimal value for z - - # For these fixed y_e, find the feasible committee B with maximal Σ_{i ∈ B} y_{e(i)}. - new_committee_model.objective = mip.xsum( - entitlement_weights[item] * agent_vars[item] for item in covered_agents - ) - new_committee_model.optimize() - new_set = _ilp_results_to_committee(agent_vars) - value = sum(entitlement_weights[item] for item in new_set) - - output_lines.append( - _print( - f"Maximin is at most {value:.2%}, can do {upper:.2%} with {len(committees)} " - f"committees. Gap {value - upper:.2%}{'≤' if value - upper <= EPS else '>'}{EPS:%}.", - ), - ) - if value <= upper + EPS: - # No feasible committee B violates Σ_{i ∈ B} y_{e(i)} ≤ z (at least up to EPS, to prevent rounding errors). - # Thus, we have enough committees. - committee_list = list(committees) - probabilities = _find_maximin_primal(committee_list, covered_agents) - return committee_list, probabilities, output_lines - # Some committee B violates Σ_{i ∈ B} y_{e(i)} ≤ z. We add B to `committees` and recurse. - assert new_set not in committees - committees.add(new_set) - incremental_model.add_constr(mip.xsum(incr_agent_vars[item] for item in new_set) <= upper_bound) - - # Heuristic for better speed in practice: - # Because optimizing `incremental_model` takes a long time, we would like to get multiple committees out - # of a single run of `incremental_model`. Rather than reoptimizing for optimal y_e and z, we find some - # feasible values y_e and z by modifying the old solution. - # This heuristic only adds more committees, and does not influence correctness. - counter = 0 - for _ in range(10): - # scale down the y_{e(i)} for i ∈ `new_set` to make Σ_{i ∈ `new_set`} y_{e(i)} ≤ z true. - for item in new_set: - entitlement_weights[item] *= upper / value - # This will change Σ_e y_e to be less than 1. We rescale the y_e and z. - sum_weights = sum(entitlement_weights.values()) - if sum_weights < EPS: - break - for item in entitlement_weights: - entitlement_weights[item] /= sum_weights - upper /= sum_weights - - new_committee_model.objective = mip.xsum( - entitlement_weights[item] * agent_vars[item] for item in covered_agents - ) - new_committee_model.optimize() - new_set = _ilp_results_to_committee(agent_vars) - value = sum(entitlement_weights[item] for item in new_set) - if value <= upper + EPS or new_set in committees: - break - committees.add(new_set) - incremental_model.add_constr( - mip.xsum(incr_agent_vars[item] for item in new_set) <= upper_bound, - ) - counter += 1 - if counter > 0: - print(f"Heuristic successfully generated {counter} additional committees.") - - -def _define_entitlements(covered_agents: frozenset[str]) -> tuple[list[str], dict[str, int]]: - entitlements = list(covered_agents) - contributes_to_entitlement = {} - for item in covered_agents: - contributes_to_entitlement[item] = entitlements.index(item) - - return entitlements, contributes_to_entitlement - - -def _committees_to_matrix( - committees: list[frozenset[str]], - entitlements: list, - contributes_to_entitlement: dict[str, int], -) -> np.ndarray: - columns = [] - for committee in committees: - column = [0 for _ in entitlements] - for item in committee: - column[contributes_to_entitlement[item]] += 1 - columns.append(np.array(column)) - return np.column_stack(columns) - - -def find_distribution_nash( - categories: dict[str, dict[str, dict[str, int]]], - people: dict[str, dict[str, str]], - columns_data: dict[str, dict[str, str]], - number_people_wanted: int, - check_same_address: bool, - check_same_address_columns: list[str], -) -> tuple[list[frozenset[str]], list[float], list[str]]: - """Find a distribution over feasible committees that maximizes the so-called Nash welfare, i.e., the product of - selection probabilities over all persons. - - Arguments follow the pattern of `find_random_sample`. - - Returns: - (committees, probabilities, output_lines) - `committees` is a list of feasible committees, where each committee is represented by a frozen set of included - agent ids. - `probabilities` is a list of probabilities of equal length, describing the probability with which each committee - should be selected. - `output_lines` is a list of debug strings. - - The following gives more details about the algorithm: - Instead of directly maximizing the product of selection probabilities Πᵢ pᵢ, we equivalently maximize - log(Πᵢ pᵢ) = Σᵢ log(pᵢ). If some person/household i is not included in any feasible committee, their pᵢ is 0, and - this sum is -∞. We will then try to maximize Σᵢ log(pᵢ) where i is restricted to range over persons/households that - can possibly be included. - """ - output_lines = ["Using Nash algorithm."] - - households = _compute_households(people, columns_data, check_same_address_columns) if check_same_address else None - - # `new_committee_model` is an integer linear program (ILP) used for discovering new feasible committees. - # We will use it many times, putting different weights on the inclusion of different agents to find many feasible - # committees. - new_committee_model, agent_vars = _setup_committee_generation( - categories, - people, - number_people_wanted, - check_same_address, - households, - ) - - # Start by finding committees including every agent, and learn which agents cannot possibly be included. - committees: list[frozenset[str]] # set of feasible committees, add more over time - covered_agents: frozenset[str] # all agent ids for agents that can actually be included - committee_set, covered_agents, new_output_lines = _generate_initial_committees( - new_committee_model, - agent_vars, - 2 * len(people), - ) - committees = list(committee_set) - output_lines += new_output_lines - - # Map the covered agents to indices in a list for easier matrix representation. - entitlements: list[str] - contributes_to_entitlement: dict[ - str, - int, - ] # for id of a covered agent, the corresponding index in `entitlements` - entitlements, contributes_to_entitlement = _define_entitlements(covered_agents) - - # Now, the algorithm proceeds iteratively. First, it finds probabilities for the committees already present in - # `committees` that maximize the sum of logarithms. Then, reusing the old ILP, it finds the feasible committee - # (possibly outside of `committees`) such that the partial derivative of the sum of logarithms with respect to the - # probability of outputting this committee is maximal. If this partial derivative is less than the maximal partial - # derivative of any committee already in `committees`, the Karush-Kuhn-Tucker conditions (which are sufficient in - # this case) imply that the distribution is optimal even with all other committees receiving probability 0. - start_lambdas = [1 / len(committees) for _ in committees] - while True: - lambdas = cp.Variable(len(committees)) # probability of outputting a specific committee - lambdas.value = start_lambdas - # A is a binary matrix, whose (i,j)th entry indicates whether agent `feasible_agents[i]` - matrix = _committees_to_matrix(committees, entitlements, contributes_to_entitlement) - assert matrix.shape == (len(entitlements), len(committees)) - - objective = cp.Maximize(cp.sum(cp.log(matrix * lambdas))) - constraints = [lambdas >= 0, sum(lambdas) == 1] - problem = cp.Problem(objective, constraints) - # TODO: test relative performance of both solvers, see whether warm_start helps. - try: - nash_welfare = problem.solve(solver=cp.SCS, warm_start=True) - except cp.SolverError: - # At least the ECOS solver in cvxpy crashes sometimes (numerical instabilities?). In this case, try another - # solver. But hope that SCS is more stable. - output_lines.append(_print("Had to switch to ECOS solver.")) - nash_welfare = problem.solve(solver=cp.ECOS, warm_start=True) - scaled_welfare = nash_welfare - len(entitlements) * log( - number_people_wanted / len(entitlements), - ) - output_lines.append(_print(f"Scaled Nash welfare is now: {scaled_welfare}.")) - - assert lambdas.value.shape == (len(committees),) - entitled_utilities = matrix.dot(lambdas.value) - assert entitled_utilities.shape == (len(entitlements),) - assert (entitled_utilities > EPS2).all() - entitled_reciprocals = 1 / entitled_utilities - assert entitled_reciprocals.shape == (len(entitlements),) - differentials = entitled_reciprocals.dot(matrix) - assert differentials.shape == (len(committees),) - - obj = [entitled_reciprocals[contributes_to_entitlement[item]] * agent_vars[item] for item in covered_agents] - new_committee_model.objective = mip.xsum(obj) - new_committee_model.optimize() - - new_set = _ilp_results_to_committee(agent_vars) - value = sum(entitled_reciprocals[contributes_to_entitlement[item]] for item in new_set) - if value <= differentials.max() + EPS_NASH: - probabilities = np.array(lambdas.value).clip(0, 1) - probabilities = list(probabilities / sum(probabilities)) - # TODO: filter 0-probability committees? - return committees, probabilities, output_lines - print(value, differentials.max(), value - differentials.max()) - assert new_set not in committees - committees.append(new_set) - start_lambdas = np.array(lambdas.value).resize(len(committees)) - - -################################### -# -# main algorithm call -# -################################### - - -def run_stratification( - categories, - people, - columns_data, - number_people_wanted: int, - min_max_people: dict[str, dict[str, int]], - settings: Settings, - test_selection: bool, - number_selections: int, -) -> tuple[bool, list[frozenset[str]], list[str]]: - # First check if numbers in cat file and to select make sense - for mkey, mvalue in min_max_people.items(): - if settings.selection_algorithm == "legacy" and ( # For other algorithms, quotas are analyzed later - number_people_wanted < mvalue["min"] or number_people_wanted > mvalue["max"] - ): - error_msg = ( - "The number of people to select ({}) is out of the range of the numbers of people " - "in one of the {} categories. It should be within [{}, {}].".format( - number_people_wanted, - mkey, - mvalue["min"], - mvalue["max"], - ) - ) - return False, [], [error_msg] - # set the random seed if it is NOT zero - if settings.random_number_seed: - random.seed(settings.random_number_seed) - - tries = 0 - success = False - output_lines = [] - if test_selection: - output_lines.append( - "WARNING: Panel is not selected at random! Only use for testing!
", - ) - output_lines.append("Initial: (selected = 0)") - categories_working = {} - people_selected = [] - while not success and tries < settings.max_attempts: - people_selected = [] - people_working = copy.deepcopy(people) - categories_working = copy.deepcopy(categories) - if tries == 0: - new_output_lines = print_category_info( - categories_working, - people, - people_selected, - number_people_wanted, - ) - output_lines += new_output_lines - - output_lines.append(f"Trial number: {tries}") - try: - people_selected, new_output_lines = find_random_sample( - categories_working, - people_working, - columns_data, - number_people_wanted, - settings.check_same_address, - settings.check_same_address_columns, - settings.selection_algorithm, - test_selection, - number_selections, - ) - output_lines += new_output_lines - # check we have met targets needed in all cats - # note this only works for number_selections = 1 - new_output_lines = print_category_info( - categories_working, - people, - people_selected, - number_people_wanted, - ) - success, check_output_lines = check_category_selected( - categories_working, - people, - people_selected, - number_selections, - ) - if success: - output_lines.append("SUCCESS!! Final:") - output_lines += new_output_lines + check_output_lines - except ValueError as err: - output_lines.append(str(err)) - break - except InfeasibleQuotasError as err: - output_lines += err.output - break - except InfeasibleQuotasCantRelaxError as err: - output_lines.append(err.message) - break - except SelectionError as serr: - output_lines.append("Failed: Selection Error thrown: " + serr.msg) - tries += 1 - if not success: - output_lines.append(f"Failed {tries} times... gave up.") - return success, people_selected, output_lines diff --git a/test_end_to_end.py b/test_end_to_end.py deleted file mode 100644 index 6089d1c..0000000 --- a/test_end_to_end.py +++ /dev/null @@ -1,87 +0,0 @@ -from pathlib import Path - -import pytest - -from stratification import ( - PeopleAndCatsCSV, - Settings, -) - -# legacy is broken, so exclude that for now -ALGORITHMS = ("legacy", "maximin", "leximin", "nash") -PEOPLE_TO_SELECT = 22 - -categories_content = Path("fixtures/categories.csv").read_text("utf8") -candidates_content = Path("fixtures/candidates.csv").read_text("utf8") -candidates_lines = [line.strip() for line in candidates_content.split("\n") if line.strip()] - -dummy = """ -The header line of candidates.csv is: -nationbuilder_id,first_name,last_name,email,mobile_number,primary_address1,primary_address2,primary_city,primary_zip,gender,age_bracket,geo_bucket,edu_level -""" - - -def get_settings(algorithm="leximin"): - columns_to_keep = [ - "first_name", - "last_name", - "mobile_number", - "email", - "primary_address1", - "primary_address2", - "primary_city", - "primary_zip", - "gender", - "age_bracket", - "geo_bucket", - "edu_level", - ] - return Settings( - id_column="nationbuilder_id", - columns_to_keep=columns_to_keep, - check_same_address=True, - check_same_address_columns=["primary_address1", "primary_zip"], - max_attempts=100, - selection_algorithm=algorithm, - random_number_seed=0, - json_file_path=Path.home() / "secret_do_not_commit.json", - ) - - -# TODO parametrize - each of the 4 algorithms - check coverage after that! -@pytest.mark.parametrize("algorithm", ALGORITHMS) -def test_csv_selection_happy_path_defaults(algorithm): - """ - Objective: Check the happy path completes. - Context: - This test is meant to do what the use will do via the GUI when using a CSV file. - Expectations: - Given default settings and an easy selection, we should get selected and remaining. - """ - settings = get_settings(algorithm) - people_cats = PeopleAndCatsCSV() - people_cats.load_cats(categories_content, "Categories", settings) - people_cats.number_people_to_select = PEOPLE_TO_SELECT - message = people_cats.load_people(settings, candidates_content, "Respondents", "Categories", "") - print("load_people_message: ") - print(message) - success, output_lines = people_cats.people_cats_run_stratification( - settings, - test_selection=False, - ) - # we are removing the header line with the [1:] - selected_lines = [ - line.strip() - for line in people_cats.get_selected_file().getvalue().split("\n") - if line.strip() - ][1:] - remaining_lines = [ - line.strip() - for line in people_cats.get_remaining_file().getvalue().split("\n") - if line.strip() - ][1:] - print(output_lines) - assert success - assert len(selected_lines) == PEOPLE_TO_SELECT - # we have the -1 to remove the header - assert len(selected_lines) + len(remaining_lines) == len(candidates_lines) - 1 diff --git a/test_stratification.py b/test_stratification.py deleted file mode 100644 index 5296426..0000000 --- a/test_stratification.py +++ /dev/null @@ -1,501 +0,0 @@ -from copy import deepcopy -from dataclasses import dataclass -from itertools import combinations -from unittest import TestCase - -from stratification import ( - InfeasibleQuotasCantRelaxError, - find_distribution_maximin, - find_distribution_nash, -) - - -@dataclass -class Example: - categories: dict[str, dict[str, dict[str, int]]] - people: dict[str, dict[str, str]] - columns_data: dict[str, dict[str, str]] - number_people_wanted: int - - -example1 = Example( - { - "age": {"child": {"min": 1, "max": 2}, "adult": {"min": 1, "max": 2}}, - "franchise": {"simpsons": {"min": 1, "max": 2}, "ducktales": {"min": 1, "max": 2}}, - }, - { - "lisa": {"age": "child", "franchise": "simpsons"}, - "marge": {"age": "adult", "franchise": "simpsons"}, - "louie": {"age": "child", "franchise": "ducktales"}, - "dewey": {"age": "child", "franchise": "ducktales"}, - "scrooge": {"age": "adult", "franchise": "ducktales"}, - }, - { - "lisa": {"home": "1"}, - "marge": {"home": "3"}, - "louie": {"home": "2"}, - "dewey": {"home": "2"}, - "scrooge": {"home": "1"}, - }, - 2, -) - -example2 = deepcopy(example1) -example2.columns_data = { - "lisa": {"home": "1"}, - "marge": {"home": "3"}, - "louie": {"home": "1"}, - "dewey": {"home": "2"}, - "scrooge": {"home": "1"}, -} - -# In this example, every committee must include agent "a" because that is the only way to get a v1 agent for all three -# features with only two agents on the committee. -example3 = Example( - { - "f1": {"v1": {"min": 1, "max": 2}, "v2": {"min": 0, "max": 2}}, - "f2": {"v1": {"min": 1, "max": 2}, "v2": {"min": 0, "max": 2}}, - "f3": {"v1": {"min": 1, "max": 2}, "v2": {"min": 0, "max": 2}}, - }, - { - "a": {"f1": "v1", "f2": "v1", "f3": "v1"}, - "b": {"f1": "v1", "f2": "v2", "f3": "v2"}, - "c": {"f1": "v2", "f2": "v1", "f3": "v2"}, - "d": {"f1": "v2", "f2": "v2", "f3": "v1"}, - }, - {"a": {"home": "1"}, "b": {"home": "2"}, "c": {"home": "3"}, "d": {"home": "3"}}, - 2, -) - -# Because we need _exactly_ one v1 agent from every agent, there are no feasible committees. Any committee without "a" -# does not have any v1 agent for one of the three features; if "a" is combined with any other agent, one feature will -# have to v1 agents.""" -example4 = deepcopy(example3) -example4.categories = { - "f1": { - "v1": {"min": 1, "max": 1, "min_flex": 0, "max_flex": 0}, - "v2": {"min": 0, "max": 2, "min_flex": 0, "max_flex": 0}, - }, - "f2": { - "v1": {"min": 1, "max": 1, "min_flex": 0, "max_flex": 0}, - "v2": {"min": 0, "max": 2, "min_flex": 0, "max_flex": 0}, - }, - "f3": { - "v1": {"min": 1, "max": 1, "min_flex": 0, "max_flex": 0}, - "v2": {"min": 0, "max": 2, "min_flex": 0, "max_flex": 0}, - }, -} - -# Categories: gender (female/male) and political leaning (liberal/conservative) -# Quotas: must include exactly 4 males, 1 female, 4 liberals, and 1 conservative. -# Pool: 4 liberal men, 1 liberal female, 1 conservative male, 1 conservative female. -# with: k = 5 -example5 = Example( - { - "gender": {"female": {"min": 1, "max": 1}, "male": {"min": 4, "max": 4}}, - "political": {"liberal": {"min": 4, "max": 4}, "conservative": {"min": 1, "max": 1}}, - }, - { - "adam": {"gender": "male", "political": "liberal"}, - "brian": {"gender": "male", "political": "liberal"}, - "cameron": {"gender": "male", "political": "liberal"}, - "dave": {"gender": "male", "political": "liberal"}, - "elinor": {"gender": "female", "political": "liberal"}, - "frank": {"gender": "male", "political": "conservative"}, - "grace": {"gender": "female", "political": "conservative"}, - }, - {"adam": {}, "brian": {}, "cameron": {}, "dave": {}, "elinor": {}, "frank": {}, "grace": {}}, - 5, -) - -# In this example, agent "p61" cannot be chosen for the committee, but in a somewhat subtle way. Consider just groups -# A, B, and C for now (D, E, and F are symmetric). The lower quotas on the v1's need at least 23 agents on A, B, and C; -# e.g. it could place 8 on A, 8 on B, and 7 on C. The same is true for D, E, and F; so none of the 46 seats is free for -# the extra person. This is subtle because the situation would be very different if we could choose fractions of a -# person on our committee. Then, we could choose 7.5 people from A through F each and even choose the extra person each -# time, or choose every person with equal probability. -example6_categories = { - "f1": { - "v1": {"min": 15, "max": 46}, # at least 15 people from A & B together - "v2": {"min": 15, "max": 46}, # at least 15 people from D & E together - "v3": {"min": 0, "max": 46}, - }, - "f2": { - "v1": {"min": 15, "max": 46}, # at least 15 people from A & C together - "v2": {"min": 15, "max": 46}, # at least 15 people from D & F together - "v3": {"min": 0, "max": 46}, - }, - "f3": { - "v1": {"min": 15, "max": 46}, # at least 15 people from B & C together - "v2": {"min": 15, "max": 46}, # at least 15 people from E & F together - "v3": {"min": 0, "max": 46}, - }, -} -example6_people = {} -for i in range(1, 11): - example6_people["p" + str(i)] = {"f1": "v1", "f2": "v1", "f3": "v3"} # 10 people of kind A - example6_people["p" + str(i + 10)] = {"f1": "v1", "f2": "v3", "f3": "v1"} # 10 people of kind B - example6_people["p" + str(i + 20)] = {"f1": "v3", "f2": "v1", "f3": "v1"} # 10 people of kind C - example6_people["p" + str(i + 30)] = {"f1": "v2", "f2": "v2", "f3": "v3"} # 10 people of kind D - example6_people["p" + str(i + 40)] = {"f1": "v2", "f2": "v3", "f3": "v2"} # 10 people of kind E - example6_people["p" + str(i + 50)] = {"f1": "v3", "f2": "v2", "f3": "v2"} # 10 people of kind F -example6_people["p61"] = {"f1": "v3", "f2": "v3", "f3": "v3"} # 1 extra person -example6_columns_data = {pid: {} for pid in example6_people} -example6 = Example(example6_categories, example6_people, example6_columns_data, 46) - - -def _calculate_marginals(people, committees, probabilities): - marginals = {pid: 0 for pid in people} - for committee, prob in zip(committees, probabilities, strict=False): - for pid in committee: - marginals[pid] += prob - return marginals - - -class FindDistributionTests(TestCase): - PRECISION = 5 - - def _probabilities_well_formed(self, probabilities): - self.assertGreaterEqual(len(probabilities), 1) - for prob in probabilities: - self.assertGreaterEqual(prob, 0) - self.assertLessEqual(prob, 1) - prob_sum = sum(probabilities) - self.assertAlmostEqual(prob_sum, 1, places=self.PRECISION) - - def _allocation_feasible( # noqa: PLR0913 - self, - committee, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ): - self.assertEqual(len(committee), len(set(committee))) - self.assertEqual(len(committee), number_people_wanted) - for pid in committee: - self.assertIn(pid, people) - for feature in categories: - for value in categories[feature]: - num_value = sum(1 for pid in committee if people[pid][feature] == value) - self.assertGreaterEqual(num_value, categories[feature][value]["min"]) - self.assertLessEqual(num_value, categories[feature][value]["max"]) - if check_same_address: - for id1, id2 in combinations(committee, r=2): - self.assertNotEqual( - [columns_data[id1][col] for col in check_same_address_columns], - [columns_data[id2][col] for col in check_same_address_columns], - ) - - def _distribution_okay( # noqa: PLR0913 - self, - committees, - probabilities, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ): - self._probabilities_well_formed(probabilities) - for committee in committees: - self._allocation_feasible( - committee, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - -class FindDistributionMaximinTests(FindDistributionTests): - def test_no_address_fair_to_people_1(self): - categories = example1.categories - people = example1.people - columns_data = example1.columns_data - number_people_wanted = example1.number_people_wanted - check_same_address = False - check_same_address_columns = [] - committees, probabilities, _ = find_distribution_maximin( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - self._distribution_okay( - committees, - probabilities, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - # maximin is 1/3, can be achieved uniquely by - # 1/3: {louie, marge}, 1/3: {dewey, marge}, 1/3: {scrooge, lisa} - marginals = _calculate_marginals(people, committees, probabilities) - self.assertAlmostEqual(marginals["lisa"], 1 / 3, places=self.PRECISION) - self.assertAlmostEqual(marginals["scrooge"], 1 / 3, places=self.PRECISION) - self.assertAlmostEqual(marginals["louie"], 1 / 3, places=self.PRECISION) - self.assertAlmostEqual(marginals["dewey"], 1 / 3, places=self.PRECISION) - self.assertAlmostEqual(marginals["marge"], 2 / 3, places=self.PRECISION) - - def test_address_fair_to_people_1(self): - categories = example1.categories - people = example1.people - columns_data = example1.columns_data - number_people_wanted = example1.number_people_wanted - check_same_address = True - check_same_address_columns = ["home"] - committees, probabilities, _ = find_distribution_maximin( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - self._distribution_okay( - committees, - probabilities, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - # Scrooge and Lisa can no longer be included. E.g. if Scrooge is included, we need a simpsons child for the - # second position. Only Lisa qualifies, but lives in the same household. Unique maximin among everyone else is: - # 1/2: {louie, marge}, 1/2: {dewey, marge} - marginals = _calculate_marginals(people, committees, probabilities) - self.assertAlmostEqual(marginals["lisa"], 0, places=self.PRECISION) - self.assertAlmostEqual(marginals["scrooge"], 0, places=self.PRECISION) - self.assertAlmostEqual(marginals["louie"], 1 / 2, places=self.PRECISION) - self.assertAlmostEqual(marginals["dewey"], 1 / 2, places=self.PRECISION) - self.assertAlmostEqual(marginals["marge"], 1, places=self.PRECISION) - - def test_no_address_fair_to_people_2(self): - categories = example3.categories - people = example3.people - columns_data = example3.columns_data - number_people_wanted = example3.number_people_wanted - check_same_address = False - check_same_address_columns = [] - committees, probabilities, _ = find_distribution_maximin( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - self._distribution_okay( - committees, - probabilities, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - # maximin is 1/3, can be achieved uniquely by - # 1/3: {a, b}, 1/3: {a, c}, 1/3: {a, d} - marginals = _calculate_marginals(people, committees, probabilities) - self.assertAlmostEqual(marginals["a"], 1, places=self.PRECISION) - self.assertAlmostEqual(marginals["b"], 1 / 3, places=self.PRECISION) - self.assertAlmostEqual(marginals["c"], 1 / 3, places=self.PRECISION) - self.assertAlmostEqual(marginals["d"], 1 / 3, places=self.PRECISION) - - def test_no_address_fair_to_people_3(self): - categories = example4.categories - people = example4.people - columns_data = example4.columns_data - number_people_wanted = example4.number_people_wanted - check_same_address = False - check_same_address_columns = ["home"] - - # There are no feasible committees at all. - with self.assertRaises(InfeasibleQuotasCantRelaxError): - find_distribution_maximin( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - def test_no_address_fair_to_people_4(self): - categories = example5.categories - people = example5.people - columns_data = example5.columns_data - number_people_wanted = example5.number_people_wanted - check_same_address = False - check_same_address_columns = [] - committees, probabilities, _ = find_distribution_maximin( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - self._distribution_okay( - committees, - probabilities, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - # maximin is 1/2 (for individuals) - marginals = _calculate_marginals(people, committees, probabilities) - self.assertGreaterEqual(marginals["adam"], 1 / 2 - 1e-5) - self.assertGreaterEqual(marginals["brian"], 1 / 2 - 1e-05) - self.assertGreaterEqual(marginals["cameron"], 1 / 2 - 1e-05) - self.assertGreaterEqual(marginals["dave"], 1 / 2 - 1e-05) - self.assertGreaterEqual(marginals["frank"], 1 / 2 - 1e-05) - self.assertAlmostEqual(marginals["elinor"], 1 / 2, places=self.PRECISION) - self.assertAlmostEqual(marginals["grace"], 1 / 2, places=self.PRECISION) - - def test_no_address_fair_to_people_5(self): - categories = example6.categories - people = example6.people - columns_data = example6.columns_data - number_people_wanted = example6.number_people_wanted - check_same_address = False - check_same_address_columns = [] - committees, probabilities, _ = find_distribution_maximin( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - self._distribution_okay( - committees, - probabilities, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - # The full maximin is 0 because p61 cannot be selected. But our algorithm should aim for the maximin among the - # remaining agents, which means choosing everyone else with probability 46/60. - marginals = _calculate_marginals(people, committees, probabilities) - self.assertEqual(marginals["p61"], 0) - for i in range(1, 61): - self.assertAlmostEqual(marginals["p" + str(i)], 46 / 60, places=self.PRECISION) - - -class FindDistributionNashTests(FindDistributionTests): - PRECISION = 3 - - def test_no_address_fair_to_people_3(self): - categories = example4.categories - people = example4.people - columns_data = example4.columns_data - number_people_wanted = example4.number_people_wanted - check_same_address = False - check_same_address_columns = ["home"] - - # There are no feasible committees at all. - with self.assertRaises(InfeasibleQuotasCantRelaxError): - find_distribution_nash( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - def test_no_address_fair_to_people_4(self): - categories = example5.categories - people = example5.people - columns_data = example5.columns_data - number_people_wanted = example5.number_people_wanted - check_same_address = False - check_same_address_columns = [] - committees, probabilities, _ = find_distribution_nash( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - self._probabilities_well_formed(probabilities) - for committee in committees: - self._allocation_feasible( - committee, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - # hand-calculated unique nash optimum - marginals = _calculate_marginals(people, committees, probabilities) - self.assertAlmostEqual(marginals["adam"], 6 / 7, places=self.PRECISION) - self.assertAlmostEqual(marginals["brian"], 6 / 7, places=self.PRECISION) - self.assertAlmostEqual(marginals["cameron"], 6 / 7, places=self.PRECISION) - self.assertAlmostEqual(marginals["dave"], 6 / 7, places=self.PRECISION) - self.assertAlmostEqual(marginals["frank"], 4 / 7, places=self.PRECISION) - self.assertAlmostEqual(marginals["elinor"], 4 / 7, places=self.PRECISION) - self.assertAlmostEqual(marginals["grace"], 3 / 7, places=self.PRECISION) - - def test_no_address_fair_to_people_5(self): - categories = example6.categories - people = example6.people - columns_data = example6.columns_data - number_people_wanted = example6.number_people_wanted - check_same_address = False - check_same_address_columns = [] - committees, probabilities, _ = find_distribution_nash( - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - self._distribution_okay( - committees, - probabilities, - categories, - people, - columns_data, - number_people_wanted, - check_same_address, - check_same_address_columns, - ) - - # The full maximin is -∞ because p61 cannot be selected. But our algorithm should maximize the Nash welfare of - # the remaining agents, which means choosing everyone else with probability 46/60. - marginals = _calculate_marginals(people, committees, probabilities) - self.assertEqual(marginals["p61"], 0) - for i in range(1, 61): - self.assertAlmostEqual(marginals["p" + str(i)], 46 / 60, places=self.PRECISION - 1) diff --git a/uv.lock b/uv.lock index 6039f2e..e51adc5 100644 --- a/uv.lock +++ b/uv.lock @@ -1,23 +1,37 @@ version = 1 -revision = 1 +revision = 3 requires-python = ">=3.11, <3.13" +resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform == 'emscripten'", + "sys_platform != 'emscripten' and sys_platform != 'win32'", +] [[package]] name = "altgraph" -version = "0.17.4" +version = "0.17.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/a8/7145824cf0b9e3c28046520480f207df47e927df83aa9555fb47f8505922/altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406", size = 48418 } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/3f/3bc3f1d83f6e4a7fcb834d3720544ca597590425be5ba9db032b2bf322a2/altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff", size = 21212 }, + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, ] [[package]] name = "bottle" -version = "0.13.2" +version = "0.13.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/97839b95c2a2ea1ca91877a846988f90f4ca16ee42c0bb79e079171c0c06/bottle-0.13.2.tar.gz", hash = "sha256:e53803b9d298c7d343d00ba7d27b0059415f04b9f6f40b8d58b5bf914ba9d348", size = 98472 } +sdist = { url = "https://files.pythonhosted.org/packages/7a/71/cca6167c06d00c81375fd668719df245864076d284f7cb46a694cbeb5454/bottle-0.13.4.tar.gz", hash = "sha256:787e78327e12b227938de02248333d788cfe45987edca735f8f88e03472c3f47", size = 98717, upload-time = "2025-06-15T10:08:59.439Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/0a/a5260c758ff813acc6967344339aa7ba15f815575f4d49141685c4345d39/bottle-0.13.2-py2.py3-none-any.whl", hash = "sha256:27569ab8d1332fbba3e400b3baab2227ab4efb4882ff147af05a7c00ed73409c", size = 104053 }, + { url = "https://files.pythonhosted.org/packages/83/f6/b55ec74cfe68c6584163faa311503c20b0da4c09883a41e8e00d6726c954/bottle-0.13.4-py2.py3-none-any.whl", hash = "sha256:045684fbd2764eac9cdeb824861d1551d113e8b683d8d26e296898d3dd99a12e", size = 103807, upload-time = "2025-06-15T10:08:57.691Z" }, ] [[package]] @@ -28,24 +42,28 @@ dependencies = [ { name = "bottle" }, { name = "gevent-websocket" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/17/8e/a22666b4bb0a6e31de579504077df2b1c2f1438136777c728e6cfabef295/bottle-websocket-0.2.9.tar.gz", hash = "sha256:9887f70dc0c7592ed8d0d11a14aa95dede6cd08d50d83d5b81fd963e5fec738b", size = 2007 } +sdist = { url = "https://files.pythonhosted.org/packages/17/8e/a22666b4bb0a6e31de579504077df2b1c2f1438136777c728e6cfabef295/bottle-websocket-0.2.9.tar.gz", hash = "sha256:9887f70dc0c7592ed8d0d11a14aa95dede6cd08d50d83d5b81fd963e5fec738b", size = 2007, upload-time = "2015-09-21T05:34:24.871Z" } [[package]] -name = "cachetools" -version = "5.5.2" +name = "cattrs" +version = "25.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } +dependencies = [ + { name = "attrs" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, + { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" }, ] [[package]] name = "certifi" -version = "2025.1.31" +version = "2026.1.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" }, ] [[package]] @@ -55,108 +73,121 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pycparser" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2b/a8/050ab4f0c3d4c1b8aaa805f70e26e84d0e27004907c5b8ecc1d31815f92a/cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", size = 508501 } +sdist = { url = "https://files.pythonhosted.org/packages/2b/a8/050ab4f0c3d4c1b8aaa805f70e26e84d0e27004907c5b8ecc1d31815f92a/cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9", size = 508501, upload-time = "2022-06-30T18:18:32.799Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/8b/2e8c2469eaf89f7273ac685164949a7e644cdfe5daf1c036564208c3d26b/cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", size = 179198 }, - { url = "https://files.pythonhosted.org/packages/f9/96/fc9e118c47b7adc45a0676f413b4a47554e5f3b6c99b8607ec9726466ef1/cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", size = 174221 }, - { url = "https://files.pythonhosted.org/packages/10/72/617ee266192223a38b67149c830bd9376b69cf3551e1477abc72ff23ef8e/cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", size = 441694 }, - { url = "https://files.pythonhosted.org/packages/91/bc/b7723c2fe7a22eee71d7edf2102cd43423d5f95ff3932ebaa2f82c7ec8d0/cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", size = 470613 }, - { url = "https://files.pythonhosted.org/packages/5d/4e/4e0bb5579b01fdbfd4388bd1eb9394a989e1336203a4b7f700d887b233c1/cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", size = 472199 }, - { url = "https://files.pythonhosted.org/packages/37/5a/c37631a86be838bdd84cc0259130942bf7e6e32f70f4cab95f479847fb91/cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", size = 462588 }, - { url = "https://files.pythonhosted.org/packages/71/d7/0fe0d91b0bbf610fb7254bb164fa8931596e660d62e90fb6289b7ee27b09/cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", size = 450543 }, - { url = "https://files.pythonhosted.org/packages/d3/56/3e94aa719ae96eeda8b68b3ec6e347e0a23168c6841dc276ccdcdadc9f32/cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", size = 474253 }, - { url = "https://files.pythonhosted.org/packages/87/ee/ddc23981fc0f5e7b5356e98884226bcb899f95ebaefc3e8e8b8742dd7e22/cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", size = 170313 }, - { url = "https://files.pythonhosted.org/packages/43/a0/cc7370ef72b6ee586369bacd3961089ab3d94ae712febf07a244f1448ffd/cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", size = 179001 }, + { url = "https://files.pythonhosted.org/packages/23/8b/2e8c2469eaf89f7273ac685164949a7e644cdfe5daf1c036564208c3d26b/cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac", size = 179198, upload-time = "2022-06-30T18:16:06.056Z" }, + { url = "https://files.pythonhosted.org/packages/f9/96/fc9e118c47b7adc45a0676f413b4a47554e5f3b6c99b8607ec9726466ef1/cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83", size = 174221, upload-time = "2022-06-30T18:16:09.137Z" }, + { url = "https://files.pythonhosted.org/packages/10/72/617ee266192223a38b67149c830bd9376b69cf3551e1477abc72ff23ef8e/cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9", size = 441694, upload-time = "2022-06-30T18:16:12.057Z" }, + { url = "https://files.pythonhosted.org/packages/91/bc/b7723c2fe7a22eee71d7edf2102cd43423d5f95ff3932ebaa2f82c7ec8d0/cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c", size = 470613, upload-time = "2022-06-30T18:16:15.3Z" }, + { url = "https://files.pythonhosted.org/packages/5d/4e/4e0bb5579b01fdbfd4388bd1eb9394a989e1336203a4b7f700d887b233c1/cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325", size = 472199, upload-time = "2022-06-30T18:16:18.027Z" }, + { url = "https://files.pythonhosted.org/packages/37/5a/c37631a86be838bdd84cc0259130942bf7e6e32f70f4cab95f479847fb91/cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c", size = 462588, upload-time = "2022-06-30T18:16:21.148Z" }, + { url = "https://files.pythonhosted.org/packages/71/d7/0fe0d91b0bbf610fb7254bb164fa8931596e660d62e90fb6289b7ee27b09/cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef", size = 450543, upload-time = "2022-06-30T18:16:24.497Z" }, + { url = "https://files.pythonhosted.org/packages/d3/56/3e94aa719ae96eeda8b68b3ec6e347e0a23168c6841dc276ccdcdadc9f32/cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8", size = 474253, upload-time = "2022-06-30T18:16:27.156Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/ddc23981fc0f5e7b5356e98884226bcb899f95ebaefc3e8e8b8742dd7e22/cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d", size = 170313, upload-time = "2022-06-30T18:16:29.939Z" }, + { url = "https://files.pythonhosted.org/packages/43/a0/cc7370ef72b6ee586369bacd3961089ab3d94ae712febf07a244f1448ffd/cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104", size = 179001, upload-time = "2022-06-30T18:16:34.521Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, - { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, - { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, - { url = "https://files.pythonhosted.org/packages/37/ed/be39e5258e198655240db5e19e0b11379163ad7070962d6b0c87ed2c4d39/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", size = 142335 }, - { url = "https://files.pythonhosted.org/packages/88/83/489e9504711fa05d8dde1574996408026bdbdbd938f23be67deebb5eca92/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", size = 143862 }, - { url = "https://files.pythonhosted.org/packages/c6/c7/32da20821cf387b759ad24627a9aca289d2822de929b8a41b6241767b461/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", size = 145673 }, - { url = "https://files.pythonhosted.org/packages/68/85/f4288e96039abdd5aeb5c546fa20a37b50da71b5cf01e75e87f16cd43304/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", size = 140211 }, - { url = "https://files.pythonhosted.org/packages/28/a3/a42e70d03cbdabc18997baf4f0227c73591a08041c149e710045c281f97b/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", size = 148039 }, - { url = "https://files.pythonhosted.org/packages/85/e4/65699e8ab3014ecbe6f5c71d1a55d810fb716bbfd74f6283d5c2aa87febf/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", size = 151939 }, - { url = "https://files.pythonhosted.org/packages/b1/82/8e9fe624cc5374193de6860aba3ea8070f584c8565ee77c168ec13274bd2/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", size = 149075 }, - { url = "https://files.pythonhosted.org/packages/3d/7b/82865ba54c765560c8433f65e8acb9217cb839a9e32b42af4aa8e945870f/charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", size = 144340 }, - { url = "https://files.pythonhosted.org/packages/b5/b6/9674a4b7d4d99a0d2df9b215da766ee682718f88055751e1e5e753c82db0/charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", size = 95205 }, - { url = "https://files.pythonhosted.org/packages/1e/ab/45b180e175de4402dcf7547e4fb617283bae54ce35c27930a6f35b6bef15/charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", size = 102441 }, - { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, - { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, - { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, - { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, - { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, - { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, - { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, - { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, - { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, - { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, - { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, - { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, - { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, ] [[package]] name = "clarabel" -version = "0.10.0" +version = "0.11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "cffi" }, { name = "numpy" }, { name = "scipy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/83/f3e550b56188b9aa363351e87f6608c67cc2c7551feece1006d5ff9eb0c3/clarabel-0.10.0.tar.gz", hash = "sha256:a8a2105058fd7db54718be53c48715a50910500b10ff0b8f5380434e69c10a10", size = 212927 } +sdist = { url = "https://files.pythonhosted.org/packages/81/e2/47f692161779dbd98876015de934943effb667a014e6f79a6d746b3e4c2a/clarabel-0.11.1.tar.gz", hash = "sha256:e7c41c47f0e59aeab99aefff9e58af4a8753ee5269bbeecbd5526fc6f41b9598", size = 253949, upload-time = "2025-06-11T16:49:05.864Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/18/a537f020338349943db49afb305a75e8d41ba9b1383df6341bde8decfde7/clarabel-0.10.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ac0375778b351ed0a6a209a3a671e438181f640e98ea56761acf44681f05f211", size = 1737235 }, - { url = "https://files.pythonhosted.org/packages/15/6c/eb05273543a80f4f9d4196fde5d2e20dc73102b4e5255d965018bd9c9031/clarabel-0.10.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:385c29169918a0fbf7eaece919db381120519241d9806f65b291444ef52deccc", size = 903609 }, - { url = "https://files.pythonhosted.org/packages/44/23/f3bc9dcb9b5e9b821702dcd2a5da03e0583ad96609641c069a31ccab6db9/clarabel-0.10.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325468980cd4495005926d413ccca4bb534e52ecc9e43fbd915f5d0e63859bd8", size = 940233 }, - { url = "https://files.pythonhosted.org/packages/fa/fa/5faf0fcbad99a70ee416443c5194af8c7f1c65d74f6570bfa1560e69af8e/clarabel-0.10.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9be6910e0ae1694996aa0c5f3db9b99ab6b619f6735ad178086b6f1e3eeef5e2", size = 1011382 }, - { url = "https://files.pythonhosted.org/packages/e6/ba/67a0defb70ed910877049e915e6738fc0432edd5e46b97743ce78a1e5b95/clarabel-0.10.0-cp39-abi3-win_amd64.whl", hash = "sha256:7871b6f499ad66f71d4e7fb40754c4d986d4316f242beb62ff4f63a69785a50c", size = 742536 }, + { url = "https://files.pythonhosted.org/packages/34/f7/f82698b6d00a40a80c67e9a32b2628886aadfaf7f7b32daa12a463e44571/clarabel-0.11.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c39160e4222040f051f2a0598691c4f9126b4d17f5b9e7678f76c71d611e12d8", size = 1039511, upload-time = "2025-06-11T16:48:58.525Z" }, + { url = "https://files.pythonhosted.org/packages/b0/8f/13650cfe25762b51175c677330e6471d5d2c5851a6fbd6df77f0681bb34e/clarabel-0.11.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8963687ee250d27310d139eea5a6816f9c3ae31f33691b56579ca4f0f0b64b63", size = 935135, upload-time = "2025-06-11T16:48:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/7af10d2b540b39f1a05d1ebba604fce933cc9bc0e65e88ec3b7a84976425/clarabel-0.11.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4837b9d0db01e98239f04b1e3526a6cf568529d3c19a8b3f591befdc467f9bb", size = 1079226, upload-time = "2025-06-11T16:49:00.987Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a9/c76edf781ca3283186ff4b54a9a4fb51367fd04313a68e2b09f062407439/clarabel-0.11.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8c41aaa6f3f8c0f3bd9d86c3e568dcaee079562c075bd2ec9fb3a80287380ef", size = 1164345, upload-time = "2025-06-11T16:49:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/4eee3062088c221e5a18b054e51c69f616e0bb0dc1b0a1a5e0fe90dfa18e/clarabel-0.11.1-cp39-abi3-win_amd64.whl", hash = "sha256:557d5148a4377ae1980b65d00605ae870a8f34f95f0f6a41e04aa6d3edf67148", size = 887310, upload-time = "2025-06-11T16:49:04.277Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" -version = "7.6.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/2d/da78abbfff98468c91fd63a73cccdfa0e99051676ded8dd36123e3a2d4d5/coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015", size = 208464 }, - { url = "https://files.pythonhosted.org/packages/31/f2/c269f46c470bdabe83a69e860c80a82e5e76840e9f4bbd7f38f8cebbee2f/coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45", size = 208893 }, - { url = "https://files.pythonhosted.org/packages/47/63/5682bf14d2ce20819998a49c0deadb81e608a59eed64d6bc2191bc8046b9/coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702", size = 241545 }, - { url = "https://files.pythonhosted.org/packages/6a/b6/6b6631f1172d437e11067e1c2edfdb7238b65dff965a12bce3b6d1bf2be2/coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0", size = 239230 }, - { url = "https://files.pythonhosted.org/packages/c7/01/9cd06cbb1be53e837e16f1b4309f6357e2dfcbdab0dd7cd3b1a50589e4e1/coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f", size = 241013 }, - { url = "https://files.pythonhosted.org/packages/4b/26/56afefc03c30871326e3d99709a70d327ac1f33da383cba108c79bd71563/coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f", size = 239750 }, - { url = "https://files.pythonhosted.org/packages/dd/ea/88a1ff951ed288f56aa561558ebe380107cf9132facd0b50bced63ba7238/coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d", size = 238462 }, - { url = "https://files.pythonhosted.org/packages/6e/d4/1d9404566f553728889409eff82151d515fbb46dc92cbd13b5337fa0de8c/coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba", size = 239307 }, - { url = "https://files.pythonhosted.org/packages/12/c1/e453d3b794cde1e232ee8ac1d194fde8e2ba329c18bbf1b93f6f5eef606b/coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f", size = 211117 }, - { url = "https://files.pythonhosted.org/packages/d5/db/829185120c1686fa297294f8fcd23e0422f71070bf85ef1cc1a72ecb2930/coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558", size = 212019 }, - { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, - { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, - { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, - { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, - { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, - { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, - { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, - { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, - { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, - { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, - { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, +version = "7.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ad/49/349848445b0e53660e258acbcc9b0d014895b6739237920886672240f84b/coverage-7.13.2.tar.gz", hash = "sha256:044c6951ec37146b72a50cc81ef02217d27d4c3640efd2640311393cbbf143d3", size = 826523, upload-time = "2026-01-25T13:00:04.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/01/abca50583a8975bb6e1c59eff67ed8e48bb127c07dad5c28d9e96ccc09ec/coverage-7.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:060ebf6f2c51aff5ba38e1f43a2095e087389b1c69d559fde6049a4b0001320e", size = 218971, upload-time = "2026-01-25T12:57:36.953Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0e/b6489f344d99cd1e5b4d5e1be52dfd3f8a3dc5112aa6c33948da8cabad4e/coverage-7.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1ea8ca9db5e7469cd364552985e15911548ea5b69c48a17291f0cac70484b2e", size = 219473, upload-time = "2026-01-25T12:57:38.934Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/db2f414915a8e4ec53f60b17956c27f21fb68fcf20f8a455ce7c2ccec638/coverage-7.13.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b780090d15fd58f07cf2011943e25a5f0c1c894384b13a216b6c86c8a8a7c508", size = 249896, upload-time = "2026-01-25T12:57:40.365Z" }, + { url = "https://files.pythonhosted.org/packages/80/06/0823fe93913663c017e508e8810c998c8ebd3ec2a5a85d2c3754297bdede/coverage-7.13.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:88a800258d83acb803c38175b4495d293656d5fac48659c953c18e5f539a274b", size = 251810, upload-time = "2026-01-25T12:57:42.045Z" }, + { url = "https://files.pythonhosted.org/packages/61/dc/b151c3cc41b28cdf7f0166c5fa1271cbc305a8ec0124cce4b04f74791a18/coverage-7.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6326e18e9a553e674d948536a04a80d850a5eeefe2aae2e6d7cf05d54046c01b", size = 253920, upload-time = "2026-01-25T12:57:44.026Z" }, + { url = "https://files.pythonhosted.org/packages/2d/35/e83de0556e54a4729a2b94ea816f74ce08732e81945024adee46851c2264/coverage-7.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59562de3f797979e1ff07c587e2ac36ba60ca59d16c211eceaa579c266c5022f", size = 250025, upload-time = "2026-01-25T12:57:45.624Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/af2eb9c3926ce3ea0d58a0d2516fcbdacf7a9fc9559fe63076beaf3f2596/coverage-7.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:27ba1ed6f66b0e2d61bfa78874dffd4f8c3a12f8e2b5410e515ab345ba7bc9c3", size = 251612, upload-time = "2026-01-25T12:57:47.713Z" }, + { url = "https://files.pythonhosted.org/packages/26/62/5be2e25f3d6c711d23b71296f8b44c978d4c8b4e5b26871abfc164297502/coverage-7.13.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8be48da4d47cc68754ce643ea50b3234557cbefe47c2f120495e7bd0a2756f2b", size = 249670, upload-time = "2026-01-25T12:57:49.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/51/400d1b09a8344199f9b6a6fc1868005d766b7ea95e7882e494fa862ca69c/coverage-7.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2a47a4223d3361b91176aedd9d4e05844ca67d7188456227b6bf5e436630c9a1", size = 249395, upload-time = "2026-01-25T12:57:50.86Z" }, + { url = "https://files.pythonhosted.org/packages/e0/36/f02234bc6e5230e2f0a63fd125d0a2093c73ef20fdf681c7af62a140e4e7/coverage-7.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6f141b468740197d6bd38f2b26ade124363228cc3f9858bd9924ab059e00059", size = 250298, upload-time = "2026-01-25T12:57:52.287Z" }, + { url = "https://files.pythonhosted.org/packages/b0/06/713110d3dd3151b93611c9cbfc65c15b4156b44f927fced49ac0b20b32a4/coverage-7.13.2-cp311-cp311-win32.whl", hash = "sha256:89567798404af067604246e01a49ef907d112edf2b75ef814b1364d5ce267031", size = 221485, upload-time = "2026-01-25T12:57:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/16/0c/3ae6255fa1ebcb7dec19c9a59e85ef5f34566d1265c70af5b2fc981da834/coverage-7.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:21dd57941804ae2ac7e921771a5e21bbf9aabec317a041d164853ad0a96ce31e", size = 222421, upload-time = "2026-01-25T12:57:55.433Z" }, + { url = "https://files.pythonhosted.org/packages/b5/37/fabc3179af4d61d89ea47bd04333fec735cd5e8b59baad44fed9fc4170d7/coverage-7.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:10758e0586c134a0bafa28f2d37dd2cdb5e4a90de25c0fc0c77dabbad46eca28", size = 221088, upload-time = "2026-01-25T12:57:57.41Z" }, + { url = "https://files.pythonhosted.org/packages/46/39/e92a35f7800222d3f7b2cbb7bbc3b65672ae8d501cb31801b2d2bd7acdf1/coverage-7.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f106b2af193f965d0d3234f3f83fc35278c7fb935dfbde56ae2da3dd2c03b84d", size = 219142, upload-time = "2026-01-25T12:58:00.448Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/8bf9e9309c4c996e65c52a7c5a112707ecdd9fbaf49e10b5a705a402bbb4/coverage-7.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f45d21dc4d5d6bd29323f0320089ef7eae16e4bef712dff79d184fa7330af3", size = 219503, upload-time = "2026-01-25T12:58:02.451Z" }, + { url = "https://files.pythonhosted.org/packages/87/93/17661e06b7b37580923f3f12406ac91d78aeed293fb6da0b69cc7957582f/coverage-7.13.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:fae91dfecd816444c74531a9c3d6ded17a504767e97aa674d44f638107265b99", size = 251006, upload-time = "2026-01-25T12:58:04.059Z" }, + { url = "https://files.pythonhosted.org/packages/12/f0/f9e59fb8c310171497f379e25db060abef9fa605e09d63157eebec102676/coverage-7.13.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:264657171406c114787b441484de620e03d8f7202f113d62fcd3d9688baa3e6f", size = 253750, upload-time = "2026-01-25T12:58:05.574Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b1/1935e31add2232663cf7edd8269548b122a7d100047ff93475dbaaae673e/coverage-7.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae47d8dcd3ded0155afbb59c62bd8ab07ea0fd4902e1c40567439e6db9dcaf2f", size = 254862, upload-time = "2026-01-25T12:58:07.647Z" }, + { url = "https://files.pythonhosted.org/packages/af/59/b5e97071ec13df5f45da2b3391b6cdbec78ba20757bc92580a5b3d5fa53c/coverage-7.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a0b33e9fd838220b007ce8f299114d406c1e8edb21336af4c97a26ecfd185aa", size = 251420, upload-time = "2026-01-25T12:58:09.309Z" }, + { url = "https://files.pythonhosted.org/packages/3f/75/9495932f87469d013dc515fb0ce1aac5fa97766f38f6b1a1deb1ee7b7f3a/coverage-7.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b3becbea7f3ce9a2d4d430f223ec15888e4deb31395840a79e916368d6004cce", size = 252786, upload-time = "2026-01-25T12:58:10.909Z" }, + { url = "https://files.pythonhosted.org/packages/6a/59/af550721f0eb62f46f7b8cb7e6f1860592189267b1c411a4e3a057caacee/coverage-7.13.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f819c727a6e6eeb8711e4ce63d78c620f69630a2e9d53bc95ca5379f57b6ba94", size = 250928, upload-time = "2026-01-25T12:58:12.449Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b1/21b4445709aae500be4ab43bbcfb4e53dc0811c3396dcb11bf9f23fd0226/coverage-7.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4f7b71757a3ab19f7ba286e04c181004c1d61be921795ee8ba6970fd0ec91da5", size = 250496, upload-time = "2026-01-25T12:58:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/0f5d89dfe0392990e4f3980adbde3eb34885bc1effb2dc369e0bf385e389/coverage-7.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b7fc50d2afd2e6b4f6f2f403b70103d280a8e0cb35320cbbe6debcda02a1030b", size = 252373, upload-time = "2026-01-25T12:58:15.976Z" }, + { url = "https://files.pythonhosted.org/packages/01/c9/0cf1a6a57a9968cc049a6b896693faa523c638a5314b1fc374eb2b2ac904/coverage-7.13.2-cp312-cp312-win32.whl", hash = "sha256:292250282cf9bcf206b543d7608bda17ca6fc151f4cbae949fc7e115112fbd41", size = 221696, upload-time = "2026-01-25T12:58:17.517Z" }, + { url = "https://files.pythonhosted.org/packages/4d/05/d7540bf983f09d32803911afed135524570f8c47bb394bf6206c1dc3a786/coverage-7.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:eeea10169fac01549a7921d27a3e517194ae254b542102267bef7a93ed38c40e", size = 222504, upload-time = "2026-01-25T12:58:19.115Z" }, + { url = "https://files.pythonhosted.org/packages/15/8b/1a9f037a736ced0a12aacf6330cdaad5008081142a7070bc58b0f7930cbc/coverage-7.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a5b567f0b635b592c917f96b9a9cb3dbd4c320d03f4bf94e9084e494f2e8894", size = 221120, upload-time = "2026-01-25T12:58:21.334Z" }, + { url = "https://files.pythonhosted.org/packages/d2/db/d291e30fdf7ea617a335531e72294e0c723356d7fdde8fba00610a76bda9/coverage-7.13.2-py3-none-any.whl", hash = "sha256:40ce1ea1e25125556d8e76bd0b61500839a07944cc287ac21d5626f3e620cad5", size = 210943, upload-time = "2026-01-25T13:00:02.388Z" }, ] [package.optional-dependencies] @@ -165,68 +196,100 @@ toml = [ ] [[package]] -name = "cvxpy" -version = "1.5.3" +name = "cryptography" +version = "46.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clarabel" }, - { name = "ecos" }, - { name = "numpy" }, - { name = "osqp" }, - { name = "scipy" }, - { name = "scs" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/80/ee/04cd4314db26ffc951c1ea90bde30dd226880ab9343759d7abbecef377ee/cryptography-46.0.0.tar.gz", hash = "sha256:99f64a6d15f19f3afd78720ad2978f6d8d4c68cd4eb600fab82ab1a7c2071dca", size = 749158, upload-time = "2025-09-16T21:07:49.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/e3/0557ffa8e47a7fcd79566c937a941a007c6611d1d51441e3200942cdbd37/cvxpy-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bfa9713c0b378383674c56b82f2b16ea746d0fdb5682a4ee8d25ce21a9a46bb4", size = 1426902 }, - { url = "https://files.pythonhosted.org/packages/76/3a/149d57f680e48615e450b28ebec5fa30eef9c5bdb09d54d072d422000b6a/cvxpy-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec4c8208d125d2c709964ae5783a07fc0fc847caafaaf738719d70f1b122e913", size = 1100511 }, - { url = "https://files.pythonhosted.org/packages/00/17/65842d5c2182122a488bc1b0bf7ee2180ec4cd2d6c4d54a8f8f14eb55bcc/cvxpy-1.5.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45aa9bbff2b695801ee72e4152f8745ef1308304d1137f07f4ac2f3bd92e19a1", size = 1166369 }, - { url = "https://files.pythonhosted.org/packages/d3/39/8da06aa1abdb6375fa607afccbb45d22e5db45a09fad0684193536f72210/cvxpy-1.5.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d202584bda28e47e84034eab41d88b5f42a696854407386a121429c0e687f85", size = 1190386 }, - { url = "https://files.pythonhosted.org/packages/10/c9/f660f90f7b9c1dbfc70a36f253dde48ac96e671b5e996fe99ca92c79e16f/cvxpy-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:031208910db7cb1679e553fda6878760d2b191e07b130365f2cd8a9489677c77", size = 1053051 }, - { url = "https://files.pythonhosted.org/packages/48/2c/f12be05867268b07e03e1212a97873b2b235ac208b86e76c598e3f603e4b/cvxpy-1.5.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ba61d01b4a095eeedf2244feea2bc4225c4f1a38ece716ec338183f0e2c7b72e", size = 1424134 }, - { url = "https://files.pythonhosted.org/packages/e6/98/7179f9efe2d6f6cdf3076986c8e800e663f592ee9e8aad0d43730247d0a0/cvxpy-1.5.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7ee652f36cbe04aae292423f031156390251a0fbc55e53b808a24172bf95677b", size = 1100143 }, - { url = "https://files.pythonhosted.org/packages/fa/48/ed5071259fef1255fddca46a56afab4c188496df9012569068a1edd6afd1/cvxpy-1.5.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62ae140c573462f2007795d7042875848560d7a4fe5e090285b32f268b078f67", size = 1165950 }, - { url = "https://files.pythonhosted.org/packages/9e/0e/f9cff3d578c07cfcb501906cf7469db52c018d83acb345d0e3d96e0b5c83/cvxpy-1.5.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70bcd7207b02afd052fa993c262a0601ad8d39fa2f901b227b34fea7b17dca1a", size = 1191709 }, - { url = "https://files.pythonhosted.org/packages/e1/5a/b69c963c3340032ff1c3e251b60c8cf299d1542e58339983c47e327874ab/cvxpy-1.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:3d8b619c2053d5f127dbd5aa9ceb5a5bdffaa3fa6c5519617364279a183b75bc", size = 1053226 }, + { url = "https://files.pythonhosted.org/packages/04/bd/3e935ca6e87dc4969683f5dd9e49adaf2cb5734253d93317b6b346e0bd33/cryptography-46.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:c9c4121f9a41cc3d02164541d986f59be31548ad355a5c96ac50703003c50fb7", size = 7285468, upload-time = "2025-09-16T21:05:52.026Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ee/dd17f412ce64b347871d7752657c5084940d42af4d9c25b1b91c7ee53362/cryptography-46.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4f70cbade61a16f5e238c4b0eb4e258d177a2fcb59aa0aae1236594f7b0ae338", size = 4308218, upload-time = "2025-09-16T21:05:55.653Z" }, + { url = "https://files.pythonhosted.org/packages/2f/53/f0b865a971e4e8b3e90e648b6f828950dea4c221bb699421e82ef45f0ef9/cryptography-46.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1eccae15d5c28c74b2bea228775c63ac5b6c36eedb574e002440c0bc28750d3", size = 4571982, upload-time = "2025-09-16T21:05:57.322Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/035be5fd63a98284fd74df9e04156f9fed7aa45cef41feceb0d06cbdadd0/cryptography-46.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1b4fba84166d906a22027f0d958e42f3a4dbbb19c28ea71f0fb7812380b04e3c", size = 4307996, upload-time = "2025-09-16T21:05:59.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/dbb6d7d0a48b95984e2d4caf0a4c7d6606cea5d30241d984c0c02b47f1b6/cryptography-46.0.0-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:523153480d7575a169933f083eb47b1edd5fef45d87b026737de74ffeb300f69", size = 4015692, upload-time = "2025-09-16T21:06:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/65/48/aafcffdde716f6061864e56a0a5908f08dcb8523dab436228957c8ebd5df/cryptography-46.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f09a3a108223e319168b7557810596631a8cb864657b0c16ed7a6017f0be9433", size = 4982192, upload-time = "2025-09-16T21:06:03.367Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ab/1e73cfc181afc3054a09e5e8f7753a8fba254592ff50b735d7456d197353/cryptography-46.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c1f6ccd6f2eef3b2eb52837f0463e853501e45a916b3fc42e5d93cf244a4b97b", size = 4603944, upload-time = "2025-09-16T21:06:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/3a/02/d71dac90b77c606c90c366571edf264dc8bd37cf836e7f902253cbf5aa77/cryptography-46.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:80a548a5862d6912a45557a101092cd6c64ae1475b82cef50ee305d14a75f598", size = 4308149, upload-time = "2025-09-16T21:06:07.006Z" }, + { url = "https://files.pythonhosted.org/packages/29/e6/4dcb67fdc6addf4e319a99c4bed25776cb691f3aa6e0c4646474748816c6/cryptography-46.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6c39fd5cd9b7526afa69d64b5e5645a06e1b904f342584b3885254400b63f1b3", size = 4947449, upload-time = "2025-09-16T21:06:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/91e3fad8ee33aa87815c8f25563f176a58da676c2b14757a4d3b19f0253c/cryptography-46.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0", size = 4603549, upload-time = "2025-09-16T21:06:13.268Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6e/caf4efadcc8f593cbaacfbb04778f78b6d0dac287b45cec25e5054de38b7/cryptography-46.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6d8945bc120dcd90ae39aa841afddaeafc5f2e832809dc54fb906e3db829dfdc", size = 4435976, upload-time = "2025-09-16T21:06:16.514Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c0/704710f349db25c5b91965c3662d5a758011b2511408d9451126429b6cd6/cryptography-46.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:88c09da8a94ac27798f6b62de6968ac78bb94805b5d272dbcfd5fdc8c566999f", size = 4709447, upload-time = "2025-09-16T21:06:19.246Z" }, + { url = "https://files.pythonhosted.org/packages/91/5e/ff63bfd27b75adaf75cc2398de28a0b08105f9d7f8193f3b9b071e38e8b9/cryptography-46.0.0-cp311-abi3-win32.whl", hash = "sha256:3738f50215211cee1974193a1809348d33893696ce119968932ea117bcbc9b1d", size = 3058317, upload-time = "2025-09-16T21:06:21.466Z" }, + { url = "https://files.pythonhosted.org/packages/46/47/4caf35014c4551dd0b43aa6c2e250161f7ffcb9c3918c9e075785047d5d2/cryptography-46.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bbaa5eef3c19c66613317dc61e211b48d5f550db009c45e1c28b59d5a9b7812a", size = 3523891, upload-time = "2025-09-16T21:06:23.856Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/6a0cafb3084a854acf808fccf756cbc9b835d1b99fb82c4a15e2e2ffb404/cryptography-46.0.0-cp311-abi3-win_arm64.whl", hash = "sha256:16b5ac72a965ec9d1e34d9417dbce235d45fa04dac28634384e3ce40dfc66495", size = 2932145, upload-time = "2025-09-16T21:06:25.842Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5f/0cf967a1dc1419d5dde111bd0e22872038199f4e4655539ea6f4da5ad7f1/cryptography-46.0.0-cp314-abi3-macosx_10_9_universal2.whl", hash = "sha256:91585fc9e696abd7b3e48a463a20dda1a5c0eeeca4ba60fa4205a79527694390", size = 7203952, upload-time = "2025-09-16T21:06:28.21Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/80e7256a4677c2e9eb762638e8200a51f6dd56d2e3de3e34d0a83c2f5f80/cryptography-46.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:1d2073313324226fd846e6b5fc340ed02d43fd7478f584741bd6b791c33c9fee", size = 7257206, upload-time = "2025-09-16T21:06:59.295Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b8/a5ed987f5c11b242713076121dddfff999d81fb492149c006a579d0e4099/cryptography-46.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83af84ebe7b6e9b6de05050c79f8cc0173c864ce747b53abce6a11e940efdc0d", size = 4301182, upload-time = "2025-09-16T21:07:01.624Z" }, + { url = "https://files.pythonhosted.org/packages/da/94/f1c1f30110c05fa5247bf460b17acfd52fa3f5c77e94ba19cff8957dc5e6/cryptography-46.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c3cd09b1490c1509bf3892bde9cef729795fae4a2fee0621f19be3321beca7e4", size = 4562561, upload-time = "2025-09-16T21:07:03.386Z" }, + { url = "https://files.pythonhosted.org/packages/5d/54/8decbf2f707350bedcd525833d3a0cc0203d8b080d926ad75d5c4de701ba/cryptography-46.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d14eaf1569d6252280516bedaffdd65267428cdbc3a8c2d6de63753cf0863d5e", size = 4301974, upload-time = "2025-09-16T21:07:04.962Z" }, + { url = "https://files.pythonhosted.org/packages/82/63/c34a2f3516c6b05801f129616a5a1c68a8c403b91f23f9db783ee1d4f700/cryptography-46.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ab3a14cecc741c8c03ad0ad46dfbf18de25218551931a23bca2731d46c706d83", size = 4009462, upload-time = "2025-09-16T21:07:06.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c5/92ef920a4cf8ff35fcf9da5a09f008a6977dcb9801c709799ec1bf2873fb/cryptography-46.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8e8b222eb54e3e7d3743a7c2b1f7fa7df7a9add790307bb34327c88ec85fe087", size = 4980769, upload-time = "2025-09-16T21:07:08.269Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8f/1705f7ea3b9468c4a4fef6cce631db14feb6748499870a4772993cbeb729/cryptography-46.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7f3f88df0c9b248dcc2e76124f9140621aca187ccc396b87bc363f890acf3a30", size = 4591812, upload-time = "2025-09-16T21:07:10.288Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/2d797ce9d346b8bac9f570b43e6e14226ff0f625f7f6f2f95d9065e316e3/cryptography-46.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9aa85222f03fdb30defabc7a9e1e3d4ec76eb74ea9fe1504b2800844f9c98440", size = 4301844, upload-time = "2025-09-16T21:07:12.522Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/8efc9712997b46aea2ac8f74adc31f780ac4662e3b107ecad0d5c1a0c7f8/cryptography-46.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f9aaf2a91302e1490c068d2f3af7df4137ac2b36600f5bd26e53d9ec320412d3", size = 4943257, upload-time = "2025-09-16T21:07:14.289Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0c/bc365287a97d28aa7feef8810884831b2a38a8dc4cf0f8d6927ad1568d27/cryptography-46.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:32670ca085150ff36b438c17f2dfc54146fe4a074ebf0a76d72fb1b419a974bc", size = 4591154, upload-time = "2025-09-16T21:07:16.271Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/0b15107277b0c558c02027da615f4e78c892f22c6a04d29c6ad43fcddca6/cryptography-46.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0f58183453032727a65e6605240e7a3824fd1d6a7e75d2b537e280286ab79a52", size = 4428200, upload-time = "2025-09-16T21:07:18.118Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/814d69418247ea2cfc985eec6678239013500d745bc7a0a35a32c2e2f3be/cryptography-46.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bc257c2d5d865ed37d0bd7c500baa71f939a7952c424f28632298d80ccd5ec1", size = 4699862, upload-time = "2025-09-16T21:07:20.219Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1e/665c718e0c45281a4e22454fa8a9bd8835f1ceb667b9ffe807baa41cd681/cryptography-46.0.0-cp38-abi3-win32.whl", hash = "sha256:df932ac70388be034b2e046e34d636245d5eeb8140db24a6b4c2268cd2073270", size = 3043766, upload-time = "2025-09-16T21:07:21.969Z" }, + { url = "https://files.pythonhosted.org/packages/78/7e/12e1e13abff381c702697845d1cf372939957735f49ef66f2061f38da32f/cryptography-46.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:274f8b2eb3616709f437326185eb563eb4e5813d01ebe2029b61bfe7d9995fbb", size = 3517216, upload-time = "2025-09-16T21:07:24.024Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/009497b2ae7375db090b41f9fe7a1a7362f804ddfe17ed9e34f748fcb0e5/cryptography-46.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:249c41f2bbfa026615e7bdca47e4a66135baa81b08509ab240a2e666f6af5966", size = 2923145, upload-time = "2025-09-16T21:07:25.74Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c9/fd0ac99ac18eaa8766800bf7d087e8c011889aa6643006cff9cbd523eadd/cryptography-46.0.0-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:75d2ddde8f1766ab2db48ed7f2aa3797aeb491ea8dfe9b4c074201aec00f5c16", size = 3722472, upload-time = "2025-09-16T21:07:32.619Z" }, + { url = "https://files.pythonhosted.org/packages/f5/69/ff831514209e68a7e32fef655abfd9ef9ee4608d151636fa11eb8d7e589a/cryptography-46.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f9f85d9cf88e3ba2b2b6da3c2310d1cf75bdf04a5bc1a2e972603054f82c4dd5", size = 4249520, upload-time = "2025-09-16T21:07:34.409Z" }, + { url = "https://files.pythonhosted.org/packages/19/4a/19960010da2865f521a5bd657eaf647d6a4368568e96f6d9ec635e47ad55/cryptography-46.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:834af45296083d892e23430e3b11df77e2ac5c042caede1da29c9bf59016f4d2", size = 4528031, upload-time = "2025-09-16T21:07:36.721Z" }, + { url = "https://files.pythonhosted.org/packages/79/92/88970c2b5b270d232213a971e74afa6d0e82d8aeee0964765a78ee1f55c8/cryptography-46.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:c39f0947d50f74b1b3523cec3931315072646286fb462995eb998f8136779319", size = 4249072, upload-time = "2025-09-16T21:07:38.382Z" }, + { url = "https://files.pythonhosted.org/packages/63/50/b0b90a269d64b479602d948f40ef6131f3704546ce003baa11405aa4093b/cryptography-46.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6460866a92143a24e3ed68eaeb6e98d0cedd85d7d9a8ab1fc293ec91850b1b38", size = 4527173, upload-time = "2025-09-16T21:07:40.742Z" }, + { url = "https://files.pythonhosted.org/packages/37/e1/826091488f6402c904e831ccbde41cf1a08672644ee5107e2447ea76a903/cryptography-46.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bf1961037309ee0bdf874ccba9820b1c2f720c2016895c44d8eb2316226c1ad5", size = 3448199, upload-time = "2025-09-16T21:07:42.639Z" }, ] [[package]] -name = "ecos" -version = "2.0.14" +name = "cvxpy" +version = "1.8.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "clarabel" }, + { name = "highspy" }, { name = "numpy" }, + { name = "osqp" }, { name = "scipy" }, + { name = "scs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/5f/17716c533da95ed110815b159efa22b1064c8c41fd5c862f21aff7a7fec0/ecos-2.0.14.tar.gz", hash = "sha256:64b3201c0e0a7f0129050557c4ac50b00031e80a10534506dba1200c8dc1efe4", size = 142430 } +sdist = { url = "https://files.pythonhosted.org/packages/85/cb/45e643e72eef40feecfef4a43c8affdd1804c841dd527e12268810665129/cvxpy-1.8.0.tar.gz", hash = "sha256:6c9f94af5a649e02a418bae565f1566cf731b43221713a558e9991fc0e011bd6", size = 1749642, upload-time = "2026-01-27T21:20:02.363Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/9b/c886a268d4b7adfaa1171244cdbfa3c944e5a599fe7a5e738ee27390ab20/ecos-2.0.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dc90b54eaae16ead128bfdd95e04bf808b73578bdf40ed652c55aa36a6d02e42", size = 92594 }, - { url = "https://files.pythonhosted.org/packages/49/e9/fae34e8ef6a9b78c3098a4428ed0e8f77cdeb334a7dc17c649abb686ed08/ecos-2.0.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8be3b4856838ae351fec40fb3589181d52b41cf75bf4d35342686a508c37a6", size = 220084 }, - { url = "https://files.pythonhosted.org/packages/2f/45/1e52519d6c29dd26bbfaf92ece5b45ca3de3b7c8b2615a818aaeadb7ad63/ecos-2.0.14-cp311-cp311-win_amd64.whl", hash = "sha256:7495b3031ccc2d4cec72cdb40aed8a2d1fdd734fe40519b7e6047aead5e811cf", size = 72199 }, - { url = "https://files.pythonhosted.org/packages/af/c3/84e392f2410f51fa557198937cc52a2e80f887c517ef4e3fb6d46e3bb008/ecos-2.0.14-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4a7e2704a3ef9acfb8146d594deff9942d3a0f0d0399de8fe2e0bd95e8b0855c", size = 92545 }, - { url = "https://files.pythonhosted.org/packages/82/12/42f4d953f9284571726b085f99e13bfa84522bf63bf2e7a81460013b09e6/ecos-2.0.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3cbb1a66ecf10955a1a4bcd6b99db55148000cb79fd176bfac26d98b21a4814", size = 222132 }, - { url = "https://files.pythonhosted.org/packages/56/9a/ca30572f3e3ff3cef6a0ea8aa6cdc12c36f9fefe559f65c7d6265713196a/ecos-2.0.14-cp312-cp312-win_amd64.whl", hash = "sha256:718eb62afb8e45426bcc365ebaf3ca9f610afcbb754de6073ef5f104da8fca1f", size = 72248 }, + { url = "https://files.pythonhosted.org/packages/3a/5e/000bdfea7951c636bdd941e86cfc557d0bf867c36b1a5fa2aa940416d3ce/cvxpy-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c54b299de19fbb306939eeaa2d951527aa153eb890867e4bf0bc184038645b59", size = 1671487, upload-time = "2026-01-27T21:14:07.645Z" }, + { url = "https://files.pythonhosted.org/packages/b9/33/c73ebd442f0c089ad7df3f222d15f35d14aa3f66a77069c9ac69f4237414/cvxpy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:333858c0df3c4167886e96536fcad037b3746d50b6480a337f2ed8b309017b91", size = 1330310, upload-time = "2026-01-27T21:14:09.242Z" }, + { url = "https://files.pythonhosted.org/packages/c3/60/8aa125f14e520066c5d1f8e13878bf1144c5f9fb0f29aadff86e16f74f9a/cvxpy-1.8.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1ec08ab85368ae8dd4e2dded86db4a891aeb37d549322e7f43d83e4be6c32e9", size = 1343072, upload-time = "2026-01-27T21:23:53.223Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a7/847ce8f229fe30f770f752556d885c122bb2f29184667ad4e9cbe5cc3bf0/cvxpy-1.8.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c244a4a80092893cbdda8318ed420c0dc68960cf230ab15dc80903149765f6fb", size = 1372429, upload-time = "2026-01-27T21:23:54.389Z" }, + { url = "https://files.pythonhosted.org/packages/bd/fb/a447dca35f016bf38d1143af3eb25af474831f19893eb5c123d7bd668c04/cvxpy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e04bea5dab8a28c25c47625e380a9094215ef9a5264c9df80674a172bf2d00c", size = 1272177, upload-time = "2026-01-27T21:10:11.56Z" }, + { url = "https://files.pythonhosted.org/packages/86/3f/56a2b0fab089a1b36d23ef4fd09d3532b0c2beac63d25eaae102cda58462/cvxpy-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82e0b8ba765eafd39fe3f9429a95107cb900612d89e2e202186efa5bb3f18fba", size = 1679010, upload-time = "2026-01-27T21:11:49.237Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e5/076f7119c2e294dc5536e9bba791d72f55deb7b97f853d0d298d12403192/cvxpy-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b62f8cb12ee67eea4fe298d3aff8f104cbdd123884d673cc2e18681bd1cf9185", size = 1334745, upload-time = "2026-01-27T21:11:50.588Z" }, + { url = "https://files.pythonhosted.org/packages/81/e0/fa36523cc39e16e4b10e937d52d5db1b2b46b79cc0d2f374c79fafbbf45c/cvxpy-1.8.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a95e2426147410564accc4a180085ac2c3e3c8b6721ba0ffca72598938676db5", size = 1345196, upload-time = "2026-01-27T21:19:58.768Z" }, + { url = "https://files.pythonhosted.org/packages/02/3c/5bdbfa75c88747f7de0e72d3c0d934511895167d11809e65c1eedbeb1f27/cvxpy-1.8.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0571f451ccb98943d89977b25be4205735f90cb2d536c5eadc3f166107ec474f", size = 1374166, upload-time = "2026-01-27T21:20:00.292Z" }, + { url = "https://files.pythonhosted.org/packages/e6/fc/c3afd7299bf46b8239639735a924c35b3c34842f291f5f20af2c4ac547ea/cvxpy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:25dd0f7c64abde257fa20af3b90b1923f77a38aa86f8a5a5f1497fea0bc357fb", size = 1273945, upload-time = "2026-01-27T21:11:27.584Z" }, ] [[package]] name = "eel" -version = "0.17.0" +version = "0.18.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bottle" }, { name = "bottle-websocket" }, { name = "future" }, + { name = "importlib-resources" }, { name = "pyparsing" }, - { name = "whichcraft" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/e7/fc5243361a3f7045634034c1c41a97f938cf3bb4a4cf4ad2d81318bd5d26/eel-0.17.0.tar.gz", hash = "sha256:77f16e24a9f68fe9cc5e2f066c0c2d47ad6f50fffdd842d108d7ac094b94c523", size = 24419 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/e2/425bc21d095f2c51a319d120c83111bb23ff5ee887cb9cbbf840df4a21b4/eel-0.18.2.tar.gz", hash = "sha256:0f70b0f8aa2da57859b35d448ea89ad2102d56b206f8500f130df069e9df2c3a", size = 26849, upload-time = "2025-06-22T18:21:36.535Z" } [[package]] name = "future" version = "1.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490 } +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326 }, + { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, ] [[package]] @@ -239,26 +302,26 @@ dependencies = [ { name = "zope-event" }, { name = "zope-interface" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/24/a3a7b713acfcf1177207f49ec25c665123f8972f42bee641bcc9f32961f4/gevent-24.2.1.tar.gz", hash = "sha256:432fc76f680acf7cf188c2ee0f5d3ab73b63c1f03114c7cd8a34cebbe5aa2056", size = 6147507 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/34/e561fb53ec80e81a83b76667c004c838a292dde8adf80ff289558b4a4df8/gevent-24.2.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:03aa5879acd6b7076f6a2a307410fb1e0d288b84b03cdfd8c74db8b4bc882fc5", size = 3017855 }, - { url = "https://files.pythonhosted.org/packages/4a/db/64295bfd9a51874b715e82ba5ab971f2c298cf283297e4cf5bec37db17d9/gevent-24.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8bb35ce57a63c9a6896c71a285818a3922d8ca05d150fd1fe49a7f57287b836", size = 4877510 }, - { url = "https://files.pythonhosted.org/packages/40/9c/8880eef385b31f694222f5c94b2b487a8b37b99aceeed3e93cb0cb038511/gevent-24.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7f87c2c02e03d99b95cfa6f7a776409083a9e4d468912e18c7680437b29222c", size = 5010258 }, - { url = "https://files.pythonhosted.org/packages/9c/0e/bf924a9998137d51e8ba84bd600ff5de17e405284811b26307748c0e0f9b/gevent-24.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968581d1717bbcf170758580f5f97a2925854943c45a19be4d47299507db2eb7", size = 5066723 }, - { url = "https://files.pythonhosted.org/packages/a1/bc/0f776a3f5a3c57e3f6bbe8abc3d39cc591f58aa03808b50af4f73ae4b238/gevent-24.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7899a38d0ae7e817e99adb217f586d0a4620e315e4de577444ebeeed2c5729be", size = 6700752 }, - { url = "https://files.pythonhosted.org/packages/58/b8/aaf9ff71ba9a7012e04400726b0e0e6986460030dfae3168482069422305/gevent-24.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e8e8d60e18d5f7fd49983f0c4696deeddaf6e608fbab33397671e2fcc6cc91", size = 6679723 }, - { url = "https://files.pythonhosted.org/packages/74/ee/6febc62ddd399b0f060785bea8ae3c994ce47dfe6ec46ece3b1a90cc496b/gevent-24.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682", size = 5434796 }, - { url = "https://files.pythonhosted.org/packages/15/12/7c91964af7112b3b435aa836401d8ca212ba9d43bcfea34c770b73515740/gevent-24.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cdf66977a976d6a3cfb006afdf825d1482f84f7b81179db33941f2fc9673bb1d", size = 6776495 }, - { url = "https://files.pythonhosted.org/packages/18/b1/bbaf6047b13c4b83cd81007298f4f8ddffd8674c130736423e79e7bb8b6a/gevent-24.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1dffb395e500613e0452b9503153f8f7ba587c67dd4a85fc7cd7aa7430cb02cc", size = 1525019 }, - { url = "https://files.pythonhosted.org/packages/50/72/eb98be1cec2a3d0f46d3af49b034deb48a6d6d9a1958ee110bc2e1e600ac/gevent-24.2.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:6c47ae7d1174617b3509f5d884935e788f325eb8f1a7efc95d295c68d83cce40", size = 3007004 }, - { url = "https://files.pythonhosted.org/packages/f7/14/4cc83275fcdfa1977224cc266b710dc71b810d6760f575d259ca3be7b4dd/gevent-24.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7cac622e11b4253ac4536a654fe221249065d9a69feb6cdcd4d9af3503602e0", size = 5142074 }, - { url = "https://files.pythonhosted.org/packages/56/ce/583d29e524c5666f7d66116e818449bee649bba8088d0ac48bec6c006215/gevent-24.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf5b9c72b884c6f0c4ed26ef204ee1f768b9437330422492c319470954bc4cc7", size = 5307651 }, - { url = "https://files.pythonhosted.org/packages/69/e7/072dfbf5c534516dcc91367d5dd5806ec8860b66c1df26b9d603493c1adb/gevent-24.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5de3c676e57177b38857f6e3cdfbe8f38d1cd754b63200c0615eaa31f514b4f", size = 5406093 }, - { url = "https://files.pythonhosted.org/packages/d9/d3/f9d0f62cb6cb0421d0da2cffd10bad13b0f5d641c57ce35927bf8554661e/gevent-24.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4faf846ed132fd7ebfbbf4fde588a62d21faa0faa06e6f468b7faa6f436b661", size = 6730420 }, - { url = "https://files.pythonhosted.org/packages/5b/eb/6b0e902e29283253324fe32317b805df289f05f0ef3e9859a721d403b71e/gevent-24.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:368a277bd9278ddb0fde308e6a43f544222d76ed0c4166e0d9f6b036586819d9", size = 6711332 }, - { url = "https://files.pythonhosted.org/packages/0d/8b/02a07125324e23d64ec342ae7a4cff8dc7271114e787317a5f219027bf1b/gevent-24.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f8a04cf0c5b7139bc6368b461257d4a757ea2fe89b3773e494d235b7dd51119f", size = 5482031 }, - { url = "https://files.pythonhosted.org/packages/5f/fe/288ccd562ac20d5e4ae2624313b699ee35c76be1faa9104b414bfe714a67/gevent-24.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9d8d0642c63d453179058abc4143e30718b19a85cbf58c2744c9a63f06a1d388", size = 6812353 }, - { url = "https://files.pythonhosted.org/packages/2e/90/d9fcdc22864d0cf471630071c264289b9a803892d6f55e895a69c2e3574b/gevent-24.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:94138682e68ec197db42ad7442d3cf9b328069c3ad8e4e5022e6b5cd3e7ffae5", size = 1523715 }, +sdist = { url = "https://files.pythonhosted.org/packages/27/24/a3a7b713acfcf1177207f49ec25c665123f8972f42bee641bcc9f32961f4/gevent-24.2.1.tar.gz", hash = "sha256:432fc76f680acf7cf188c2ee0f5d3ab73b63c1f03114c7cd8a34cebbe5aa2056", size = 6147507, upload-time = "2024-02-14T11:31:10.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/34/e561fb53ec80e81a83b76667c004c838a292dde8adf80ff289558b4a4df8/gevent-24.2.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:03aa5879acd6b7076f6a2a307410fb1e0d288b84b03cdfd8c74db8b4bc882fc5", size = 3017855, upload-time = "2024-02-14T11:26:23.685Z" }, + { url = "https://files.pythonhosted.org/packages/4a/db/64295bfd9a51874b715e82ba5ab971f2c298cf283297e4cf5bec37db17d9/gevent-24.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8bb35ce57a63c9a6896c71a285818a3922d8ca05d150fd1fe49a7f57287b836", size = 4877510, upload-time = "2024-02-14T12:09:43.242Z" }, + { url = "https://files.pythonhosted.org/packages/40/9c/8880eef385b31f694222f5c94b2b487a8b37b99aceeed3e93cb0cb038511/gevent-24.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7f87c2c02e03d99b95cfa6f7a776409083a9e4d468912e18c7680437b29222c", size = 5010258, upload-time = "2024-02-14T12:07:34.016Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0e/bf924a9998137d51e8ba84bd600ff5de17e405284811b26307748c0e0f9b/gevent-24.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968581d1717bbcf170758580f5f97a2925854943c45a19be4d47299507db2eb7", size = 5066723, upload-time = "2024-02-14T12:10:56.261Z" }, + { url = "https://files.pythonhosted.org/packages/a1/bc/0f776a3f5a3c57e3f6bbe8abc3d39cc591f58aa03808b50af4f73ae4b238/gevent-24.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7899a38d0ae7e817e99adb217f586d0a4620e315e4de577444ebeeed2c5729be", size = 6700752, upload-time = "2024-02-14T11:53:59.856Z" }, + { url = "https://files.pythonhosted.org/packages/58/b8/aaf9ff71ba9a7012e04400726b0e0e6986460030dfae3168482069422305/gevent-24.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e8e8d60e18d5f7fd49983f0c4696deeddaf6e608fbab33397671e2fcc6cc91", size = 6679723, upload-time = "2024-02-14T11:59:14.753Z" }, + { url = "https://files.pythonhosted.org/packages/74/ee/6febc62ddd399b0f060785bea8ae3c994ce47dfe6ec46ece3b1a90cc496b/gevent-24.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682", size = 5434796, upload-time = "2024-02-14T12:25:50.016Z" }, + { url = "https://files.pythonhosted.org/packages/15/12/7c91964af7112b3b435aa836401d8ca212ba9d43bcfea34c770b73515740/gevent-24.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cdf66977a976d6a3cfb006afdf825d1482f84f7b81179db33941f2fc9673bb1d", size = 6776495, upload-time = "2024-02-14T12:01:16.975Z" }, + { url = "https://files.pythonhosted.org/packages/18/b1/bbaf6047b13c4b83cd81007298f4f8ddffd8674c130736423e79e7bb8b6a/gevent-24.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1dffb395e500613e0452b9503153f8f7ba587c67dd4a85fc7cd7aa7430cb02cc", size = 1525019, upload-time = "2024-02-14T11:39:23.072Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/eb98be1cec2a3d0f46d3af49b034deb48a6d6d9a1958ee110bc2e1e600ac/gevent-24.2.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:6c47ae7d1174617b3509f5d884935e788f325eb8f1a7efc95d295c68d83cce40", size = 3007004, upload-time = "2024-02-14T11:28:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/f7/14/4cc83275fcdfa1977224cc266b710dc71b810d6760f575d259ca3be7b4dd/gevent-24.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7cac622e11b4253ac4536a654fe221249065d9a69feb6cdcd4d9af3503602e0", size = 5142074, upload-time = "2024-02-14T12:09:45.269Z" }, + { url = "https://files.pythonhosted.org/packages/56/ce/583d29e524c5666f7d66116e818449bee649bba8088d0ac48bec6c006215/gevent-24.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf5b9c72b884c6f0c4ed26ef204ee1f768b9437330422492c319470954bc4cc7", size = 5307651, upload-time = "2024-02-14T12:07:36.645Z" }, + { url = "https://files.pythonhosted.org/packages/69/e7/072dfbf5c534516dcc91367d5dd5806ec8860b66c1df26b9d603493c1adb/gevent-24.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5de3c676e57177b38857f6e3cdfbe8f38d1cd754b63200c0615eaa31f514b4f", size = 5406093, upload-time = "2024-02-14T12:10:58.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d3/f9d0f62cb6cb0421d0da2cffd10bad13b0f5d641c57ce35927bf8554661e/gevent-24.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4faf846ed132fd7ebfbbf4fde588a62d21faa0faa06e6f468b7faa6f436b661", size = 6730420, upload-time = "2024-02-14T11:54:02.399Z" }, + { url = "https://files.pythonhosted.org/packages/5b/eb/6b0e902e29283253324fe32317b805df289f05f0ef3e9859a721d403b71e/gevent-24.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:368a277bd9278ddb0fde308e6a43f544222d76ed0c4166e0d9f6b036586819d9", size = 6711332, upload-time = "2024-02-14T11:59:16.68Z" }, + { url = "https://files.pythonhosted.org/packages/0d/8b/02a07125324e23d64ec342ae7a4cff8dc7271114e787317a5f219027bf1b/gevent-24.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f8a04cf0c5b7139bc6368b461257d4a757ea2fe89b3773e494d235b7dd51119f", size = 5482031, upload-time = "2024-02-14T12:25:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fe/288ccd562ac20d5e4ae2624313b699ee35c76be1faa9104b414bfe714a67/gevent-24.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9d8d0642c63d453179058abc4143e30718b19a85cbf58c2744c9a63f06a1d388", size = 6812353, upload-time = "2024-02-14T12:01:19.819Z" }, + { url = "https://files.pythonhosted.org/packages/2e/90/d9fcdc22864d0cf471630071c264289b9a803892d6f55e895a69c2e3574b/gevent-24.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:94138682e68ec197db42ad7442d3cf9b328069c3ad8e4e5022e6b5cd3e7ffae5", size = 1523715, upload-time = "2024-02-14T11:31:09.195Z" }, ] [[package]] @@ -268,117 +331,208 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "gevent" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/98/d2/6fa19239ff1ab072af40ebf339acd91fb97f34617c2ee625b8e34bf42393/gevent-websocket-0.10.1.tar.gz", hash = "sha256:7eaef32968290c9121f7c35b973e2cc302ffb076d018c9068d2f5ca8b2d85fb0", size = 18366 } +sdist = { url = "https://files.pythonhosted.org/packages/98/d2/6fa19239ff1ab072af40ebf339acd91fb97f34617c2ee625b8e34bf42393/gevent-websocket-0.10.1.tar.gz", hash = "sha256:7eaef32968290c9121f7c35b973e2cc302ffb076d018c9068d2f5ca8b2d85fb0", size = 18366, upload-time = "2017-03-12T22:46:05.68Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/84/2dc373eb6493e00c884cc11e6c059ec97abae2678d42f06bf780570b0193/gevent_websocket-0.10.1-py3-none-any.whl", hash = "sha256:17b67d91282f8f4c973eba0551183fc84f56f1c90c8f6b6b30256f31f66f5242", size = 22987 }, + { url = "https://files.pythonhosted.org/packages/7b/84/2dc373eb6493e00c884cc11e6c059ec97abae2678d42f06bf780570b0193/gevent_websocket-0.10.1-py3-none-any.whl", hash = "sha256:17b67d91282f8f4c973eba0551183fc84f56f1c90c8f6b6b30256f31f66f5242", size = 22987, upload-time = "2017-03-12T22:46:03.611Z" }, ] [[package]] name = "google-auth" -version = "2.38.0" +version = "2.48.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cachetools" }, + { name = "cryptography" }, { name = "pyasn1-modules" }, { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/eb/d504ba1daf190af6b204a9d4714d457462b486043744901a6eeea711f913/google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4", size = 270866 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/47/603554949a37bca5b7f894d51896a9c534b9eab808e2520a748e081669d0/google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a", size = 210770 }, + { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" }, ] [[package]] name = "google-auth-oauthlib" -version = "1.2.1" +version = "1.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "requests-oauthlib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cc/0f/1772edb8d75ecf6280f1c7f51cbcebe274e8b17878b382f63738fd96cee5/google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263", size = 24970 } +sdist = { url = "https://files.pythonhosted.org/packages/90/dd/211f27c1e927e2292c2a71d5df1a2aaf261ce50ba7d50848c6ee24e20970/google_auth_oauthlib-1.2.4.tar.gz", hash = "sha256:3ca93859c6cc9003c8e12b2a0868915209d7953f05a70f4880ab57d57e56ee3e", size = 21185, upload-time = "2026-01-15T22:03:10.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/8e/22a28dfbd218033e4eeaf3a0533b2b54852b6530da0c0fe934f0cc494b29/google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f", size = 24930 }, + { url = "https://files.pythonhosted.org/packages/84/21/fb96db432d187b07756e62971c4d89bdef70259e4cfa76ee32bcc0ac97d1/google_auth_oauthlib-1.2.4-py3-none-any.whl", hash = "sha256:0e922eea5f2baacaf8867febb782e46e7b153236c21592ed76ab3ddb77ffd772", size = 19193, upload-time = "2026-01-15T22:03:09.046Z" }, ] [[package]] name = "greenlet" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479 }, - { url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404 }, - { url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813 }, - { url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517 }, - { url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831 }, - { url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413 }, - { url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619 }, - { url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198 }, - { url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930 }, - { url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 }, - { url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 }, - { url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 }, - { url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 }, - { url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 }, - { url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 }, - { url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 }, - { url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 }, - { url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 }, +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" }, + { url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" }, + { url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" }, + { url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" }, + { url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, + { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, + { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, + { url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" }, ] [[package]] name = "gspread" -version = "6.1.4" +version = "6.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "google-auth" }, { name = "google-auth-oauthlib" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/48/76/d23fcdef5d8137513b64f7447849369983078e4025100d9149d5c8222e5a/gspread-6.1.4.tar.gz", hash = "sha256:b8eec27de7cadb338bb1b9f14a9be168372dee8965c0da32121816b5050ac1de", size = 78251 } +sdist = { url = "https://files.pythonhosted.org/packages/91/83/42d1d813822ed016d77aabadc99b09de3b5bd68532fd6bae23fd62347c41/gspread-6.2.1.tar.gz", hash = "sha256:2c7c99f7c32ebea6ec0d36f2d5cbe8a2be5e8f2a48bde87ad1ea203eff32bd03", size = 82590, upload-time = "2025-05-14T15:56:25.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/76/563fb20dedd0e12794d9a12cfe0198458cc0501fdc7b034eee2166d035d5/gspread-6.2.1-py3-none-any.whl", hash = "sha256:6d4ec9f1c23ae3c704a9219026dac01f2b328ac70b96f1495055d453c4c184db", size = 59977, upload-time = "2025-05-14T15:56:24.014Z" }, +] + +[[package]] +name = "highspy" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/32/cc/dc47527beec1f44274186713a59fca13e6e1c71088ed6d18980dc8b90ee9/highspy-1.12.0.tar.gz", hash = "sha256:91a2da2c090597e34cd2cb57a751816ca6857c8cca8b09ae4d33960fb89ad42c", size = 1399992, upload-time = "2025-10-25T18:09:46.242Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/c7/28ad61476626d635288d1671326f6540145778a5f2fc9f907ad4d929ba90/gspread-6.1.4-py3-none-any.whl", hash = "sha256:c34781c426031a243ad154952b16f21ac56a5af90687885fbee3d1fba5280dcd", size = 57574 }, + { url = "https://files.pythonhosted.org/packages/06/fa/c5e095a868473165de056d8d6821ec842117d4b62304389a948336e27a76/highspy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e9a31864c2921fe625d61df8bfe042ea693748cacf6d81b0ec78e46b3f40c95", size = 2210217, upload-time = "2025-10-25T18:08:30.286Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a1/7d101b5c6a2e3a26571eb55f1e5305f772fabfa6be4c9cc546d5c55d0f58/highspy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2ce9eddf5de0c5301bb0997ad3772df0074f100444f334c3d84d984d5fa1d45", size = 2025200, upload-time = "2025-10-25T18:08:32.378Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d0/a7cb2945ec254cd068c8365cad2043bb8331fafe86f689cfcc014a09ec69/highspy-1.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6011bd9696fe41942d3d854a2eb661289dbb28d2964ff6e3d29282fea2c703ce", size = 2565541, upload-time = "2025-10-25T18:08:34.086Z" }, + { url = "https://files.pythonhosted.org/packages/13/30/478c4cbaf290b622add80846bfd5cb7e30cf5d19ecc5346b5e21933bc45c/highspy-1.12.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dcfe92716545e71ec2c9b23360df7c6bcf0a108d0035d708efbe5d8a9c2e9750", size = 2292945, upload-time = "2025-10-25T18:08:35.893Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/b73cb21ba513fa98621aa88a086453eae9011c672114004f8688ac21a7e4/highspy-1.12.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e60360f2ef725571712ed408594165e50f503d2986552cd94dc0ab6a1b264e7", size = 2498840, upload-time = "2025-10-25T18:08:37.246Z" }, + { url = "https://files.pythonhosted.org/packages/43/44/15b1850d2b50252dfd761b9dfce2c8e8bd7bf6458ee943b87b06c6c6c858/highspy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:38813111ce7f80cd350a396bb43947db4b9d90dddd6af50fa381ac07a7985392", size = 3352376, upload-time = "2025-10-25T18:08:38.589Z" }, + { url = "https://files.pythonhosted.org/packages/90/c2/e496e36f086f4d0a28b8abca2cb4367e122719fd86a2fd0368622c765780/highspy-1.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4e56be011cd493a873b8e478407d2e723fa963edaa46b03df5f0b9b694f35c1d", size = 3914077, upload-time = "2025-10-25T18:08:40.38Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e7/6fa9942ede0373cc087bbd317739fbe0078fc1dcc58e786c291aff9c66df/highspy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbaf2cdc501539c06393df84b13bdedf987c4aa02588e50f57d23ba3739d2c6f", size = 3577646, upload-time = "2025-10-25T18:08:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/b5/52/06321962d19943d72f90a5cbc3b01eab0ce99171039b64e6b66093e5d9cd/highspy-1.12.0-cp311-cp311-win32.whl", hash = "sha256:aff2a46b477593911aa16aefae8518e3fea31bf474a93ba6367f0c7c515e942b", size = 1842778, upload-time = "2025-10-25T18:08:43.565Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6b/dd62b1dba01ac6c4118be7cb5df1bd44a757b3800411b588a43f83539399/highspy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:13be9cd5d17904d6a5ae02da84cd0bb115ce390e43f3c15d433d169d415a8c77", size = 2185224, upload-time = "2025-10-25T18:08:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/69/eacc8742d1a4ac68109a8ce3c808e81cab1c499926815e23dd39106595a3/highspy-1.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8c8b098a57b4035d5bdbffa2a80a96232c99e4d085aa822e283e98c50a1438b8", size = 2212070, upload-time = "2025-10-25T18:08:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/d7/50/25e2adb149b56354aaa31165b44304c85763f66a560e8a1492e09ce6a9a5/highspy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3db2958b934d46c0e112e8f9fdba96bb1fb84cfc6f70ddd4492ea62da890fe56", size = 2029063, upload-time = "2025-10-25T18:08:48.597Z" }, + { url = "https://files.pythonhosted.org/packages/36/0f/9d5b3f27a4bc8ebf4c2a18cd1ef39db77fabd8b007629143416c4d6cd4fd/highspy-1.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ecc5bdee740118e18ce928feaf9e309eb4a4a076f27a6780b8b363f1446801b5", size = 2562998, upload-time = "2025-10-25T18:08:49.968Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b8/901d9702873f6e103a3b9d1a6e8e403b486d4e672e4e925d6bc1a92f8114/highspy-1.12.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6045d1d49627e1dc2f649cda4190c57c823f0cf71bec9d466363f00abbc65a7", size = 2296309, upload-time = "2025-10-25T18:08:51.651Z" }, + { url = "https://files.pythonhosted.org/packages/26/c7/34fbaacecdc363d847ef9f73ff169447db9728a100ca41c4ad1a4d3fb21a/highspy-1.12.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3a72ecea18c0b2cd041e87599d2e0bf6f96cea7133aba838f0128608089e92f", size = 2500486, upload-time = "2025-10-25T18:08:52.981Z" }, + { url = "https://files.pythonhosted.org/packages/ef/69/46910999e0fa30f85b15b3a3d5a7a16ed5af8763323e351f0ddd7fbcfed3/highspy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1df858cfc71fafef789fe87793e331a17768065f9e9505df1ff10239e4fa6a8d", size = 3355868, upload-time = "2025-10-25T18:08:54.308Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d3/9c1c13e09cd2dd1aed77a897272d974dab36be25bcc27a0af1f704153781/highspy-1.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:455b8688d93124b714e93f0787fb17040fcb5b1b90c59684bd44f66b3b947dbc", size = 3918497, upload-time = "2025-10-25T18:08:56.096Z" }, + { url = "https://files.pythonhosted.org/packages/50/f9/6dd6f8694c8f631057199bdbcadbc2078023029021feb9f0dbdbb9b0aed5/highspy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:948db9f948c79a52f5faafeb71cd8990a84b462245bb2c493d3e16938f038782", size = 3581509, upload-time = "2025-10-25T18:08:57.862Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24074f2bdb92b194d2c625a1071241a9bf23f475db850331b4256bda047f/highspy-1.12.0-cp312-cp312-win32.whl", hash = "sha256:869bdcedd7c309647efef2d4968dc05609815b537f8ccd8144ec2b81c0801a8b", size = 1843171, upload-time = "2025-10-25T18:08:59.195Z" }, + { url = "https://files.pythonhosted.org/packages/6c/48/ae627c5b16e2ae0f56537b25e6176e1e7db8ea26ea85fab047518979be2f/highspy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:90c3f9d575a93f0e08490f101b72b8597c7d1ad801d429f31b6ae962a1492a56", size = 2183875, upload-time = "2025-10-25T18:09:00.525Z" }, ] [[package]] name = "httplib2" -version = "0.22.0" +version = "0.31.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyparsing" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/ad/2371116b22d616c194aa25ec410c9c6c37f23599dcd590502b74db197584/httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81", size = 351116 } +sdist = { url = "https://files.pythonhosted.org/packages/c1/1f/e86365613582c027dda5ddb64e1010e57a3d53e99ab8a72093fa13d565ec/httplib2-0.31.2.tar.gz", hash = "sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24", size = 250800, upload-time = "2026-01-23T11:04:44.165Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/6c/d2fbdaaa5959339d53ba38e94c123e4e84b8fbc4b84beb0e70d7c1608486/httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc", size = 96854 }, + { url = "https://files.pythonhosted.org/packages/2f/90/fd509079dfcab01102c0fdd87f3a9506894bc70afcf9e9785ef6b2b3aff6/httplib2-0.31.2-py3-none-any.whl", hash = "sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349", size = 91099, upload-time = "2026-01-23T11:04:42.78Z" }, ] [[package]] name = "idna" -version = "3.10" +version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, ] [[package]] name = "iniconfig" -version = "2.0.0" +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, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, ] [[package]] name = "macholib" -version = "1.16.3" +version = "1.16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altgraph" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/ee/af1a3842bdd5902ce133bd246eb7ffd4375c38642aeb5dc0ae3a0329dfa2/macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30", size = 59309 } +sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/5d/c059c180c84f7962db0aeae7c3b9303ed1d73d76f2bfbc32bc231c8be314/macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c", size = 38094 }, + { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, ] [[package]] @@ -388,37 +542,46 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/84/41ebb2db20fbc199768c317ee8238d50b55fd6ffaa4c17e2cb94f0d03152/mip-1.15.0.tar.gz", hash = "sha256:7f6f0381cfe2c52c1b8640203da2cb56974b26e23950ddfb1a76b37d916f197e", size = 24569566 } +sdist = { url = "https://files.pythonhosted.org/packages/30/84/41ebb2db20fbc199768c317ee8238d50b55fd6ffaa4c17e2cb94f0d03152/mip-1.15.0.tar.gz", hash = "sha256:7f6f0381cfe2c52c1b8640203da2cb56974b26e23950ddfb1a76b37d916f197e", size = 24569566, upload-time = "2023-01-04T13:51:46.934Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/c9/d4ba71d5d73cf57596b8637ab20eda547c6cde296860f6b6192568809e70/mip-1.15.0-py3-none-any.whl", hash = "sha256:9a48c993ddc9a48591a59e4a1221b400eb1e35fc087052085774360330bb9226", size = 15269834 }, + { url = "https://files.pythonhosted.org/packages/49/c9/d4ba71d5d73cf57596b8637ab20eda547c6cde296860f6b6192568809e70/mip-1.15.0-py3-none-any.whl", hash = "sha256:9a48c993ddc9a48591a59e4a1221b400eb1e35fc087052085774360330bb9226", size = 15269834, upload-time = "2023-01-04T13:51:34.596Z" }, ] [[package]] name = "numpy" -version = "2.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/86/453aa3949eab6ff54e2405f9cb0c01f756f031c3dc2a6d60a1d40cba5488/numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8", size = 21237256 }, - { url = "https://files.pythonhosted.org/packages/20/c3/93ecceadf3e155d6a9e4464dd2392d8d80cf436084c714dc8535121c83e8/numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b", size = 14408049 }, - { url = "https://files.pythonhosted.org/packages/8d/29/076999b69bd9264b8df5e56f2be18da2de6b2a2d0e10737e5307592e01de/numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a", size = 5408655 }, - { url = "https://files.pythonhosted.org/packages/e2/a7/b14f0a73eb0fe77cb9bd5b44534c183b23d4229c099e339c522724b02678/numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636", size = 6949996 }, - { url = "https://files.pythonhosted.org/packages/72/2f/8063da0616bb0f414b66dccead503bd96e33e43685c820e78a61a214c098/numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d", size = 14355789 }, - { url = "https://files.pythonhosted.org/packages/e6/d7/3cd47b00b8ea95ab358c376cf5602ad21871410950bc754cf3284771f8b6/numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb", size = 16411356 }, - { url = "https://files.pythonhosted.org/packages/27/c0/a2379e202acbb70b85b41483a422c1e697ff7eee74db642ca478de4ba89f/numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2", size = 15576770 }, - { url = "https://files.pythonhosted.org/packages/bc/63/a13ee650f27b7999e5b9e1964ae942af50bb25606d088df4229283eda779/numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b", size = 18200483 }, - { url = "https://files.pythonhosted.org/packages/4c/87/e71f89935e09e8161ac9c590c82f66d2321eb163893a94af749dfa8a3cf8/numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5", size = 6588415 }, - { url = "https://files.pythonhosted.org/packages/b9/c6/cd4298729826af9979c5f9ab02fcaa344b82621e7c49322cd2d210483d3f/numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f", size = 12929604 }, - { url = "https://files.pythonhosted.org/packages/43/ec/43628dcf98466e087812142eec6d1c1a6c6bdfdad30a0aa07b872dc01f6f/numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d", size = 20929458 }, - { url = "https://files.pythonhosted.org/packages/9b/c0/2f4225073e99a5c12350954949ed19b5d4a738f541d33e6f7439e33e98e4/numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95", size = 14115299 }, - { url = "https://files.pythonhosted.org/packages/ca/fa/d2c5575d9c734a7376cc1592fae50257ec95d061b27ee3dbdb0b3b551eb2/numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea", size = 5145723 }, - { url = "https://files.pythonhosted.org/packages/eb/dc/023dad5b268a7895e58e791f28dc1c60eb7b6c06fcbc2af8538ad069d5f3/numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532", size = 6678797 }, - { url = "https://files.pythonhosted.org/packages/3f/19/bcd641ccf19ac25abb6fb1dcd7744840c11f9d62519d7057b6ab2096eb60/numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e", size = 14067362 }, - { url = "https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe", size = 16116679 }, - { url = "https://files.pythonhosted.org/packages/d0/a1/e90f7aa66512be3150cb9d27f3d9995db330ad1b2046474a13b7040dfd92/numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021", size = 15264272 }, - { url = "https://files.pythonhosted.org/packages/dc/b6/50bd027cca494de4fa1fc7bf1662983d0ba5f256fa0ece2c376b5eb9b3f0/numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8", size = 17880549 }, - { url = "https://files.pythonhosted.org/packages/96/30/f7bf4acb5f8db10a96f73896bdeed7a63373137b131ca18bd3dab889db3b/numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe", size = 6293394 }, - { url = "https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d", size = 12626357 }, +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" }, + { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" }, + { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" }, + { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" }, + { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" }, + { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" }, + { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" }, + { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" }, + { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" }, + { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" }, + { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" }, + { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" }, + { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" }, + { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" }, + { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" }, ] [[package]] @@ -432,103 +595,143 @@ dependencies = [ { name = "rsa" }, { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/7b/17244b1083e8e604bf154cf9b716aecd6388acd656dd01893d0d244c94d9/oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6", size = 155910 } +sdist = { url = "https://files.pythonhosted.org/packages/a6/7b/17244b1083e8e604bf154cf9b716aecd6388acd656dd01893d0d244c94d9/oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6", size = 155910, upload-time = "2018-09-07T21:38:18.036Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/a9/4f25a14d23f0786b64875b91784607c2277eff25d48f915e39ff0cff505a/oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac", size = 98206 }, + { url = "https://files.pythonhosted.org/packages/95/a9/4f25a14d23f0786b64875b91784607c2277eff25d48f915e39ff0cff505a/oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac", size = 98206, upload-time = "2018-09-07T21:38:16.742Z" }, ] [[package]] name = "oauthlib" -version = "3.2.2" +version = "3.3.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, ] [[package]] name = "osqp" -version = "0.6.7.post3" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "jinja2" }, + { name = "joblib" }, { name = "numpy" }, - { name = "qdldl" }, { name = "scipy" }, + { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/35/45d4d1832b31d207f83e0f9734d041be125fb4f0dff49413674bd1b08032/osqp-0.6.7.post3.tar.gz", hash = "sha256:b0c5e0a721f21c9724097a4fd50108304d296468d124e16f34ac67046f7020e1", size = 229274 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/c5/04f5e38b45d08dd22a68bc7e7fff8607f8e23010162f63185f535e34480c/osqp-1.1.0.tar.gz", hash = "sha256:4f81c819346ce8da6eeb105648c110cb00601d379aad8bd41a69296cb6d46464", size = 56972, upload-time = "2026-01-26T21:13:00.502Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/dd/123079f0ad8409d3be9074344a3d45073ce928f701890f010ab506ffee9f/osqp-0.6.7.post3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1a1dcd869fd6ac501e06262c21483a3691b6281e4f3f65af6951330958b89ca", size = 251844 }, - { url = "https://files.pythonhosted.org/packages/cd/6d/0d17e8fa61809c125f97685d86e6cd6f7b1e745e01b8d3f96d783c8de41b/osqp-0.6.7.post3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46b93d1110dc0ad311f6691c4df9ee41cbbde5ffc0d8c8d520d4555bf5d8765b", size = 237567 }, - { url = "https://files.pythonhosted.org/packages/4f/74/d748a9f42426fa48ab0139d0738988296a3c599a6b3c78395258d02e436b/osqp-0.6.7.post3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5209104d6fe3ace4fdbf9ace08caa2cba9de1e7ccd5f56279a346c235917138b", size = 293855 }, - { url = "https://files.pythonhosted.org/packages/55/72/8746c4bc488a31641091ccc50e71f92e0a4211e2ef882e00904940531962/osqp-0.6.7.post3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfefa07740e9fb1c574cdc836e5afe2600b73c0c12089955d4ae6587c55f0eb", size = 298312 }, - { url = "https://files.pythonhosted.org/packages/f8/7b/ec42030f389c1b2a7e5517d4ba4a169f1d8fb6f4beb92c5b457e0cc284e4/osqp-0.6.7.post3-cp311-cp311-win_amd64.whl", hash = "sha256:c48c91dfba02ce11e8b8f5d401ec5b67a316782bfdf4f53ca753e49907f7387f", size = 293043 }, - { url = "https://files.pythonhosted.org/packages/22/26/4cf65e82cf63c4f4ff5186618c006d95a1a5bc9f4f015563ad6d87d75a42/osqp-0.6.7.post3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:023af06764f7aba9c64536ecb7204019906bb7e78237f335f82b404f16623eef", size = 252062 }, - { url = "https://files.pythonhosted.org/packages/ce/bc/ece5348baef40bf355c5ef8000103aaf77973060f4c940da9cce0999e00d/osqp-0.6.7.post3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4cec7cb5bf1615c4129277275dc08e20a037372a874cff35eb891b4b35a463de", size = 237577 }, - { url = "https://files.pythonhosted.org/packages/31/33/f09c305591606e59edc5f09aa5cba3606c0e29e7b0fff42d044585bcc1f4/osqp-0.6.7.post3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb882ab24b97b14843b7c71d2474fb8b415bafc8dd60aa94870c2ef338c20bfb", size = 295407 }, - { url = "https://files.pythonhosted.org/packages/ef/63/356f01888eb0e4cd8603eb8b7711a6865e26bc2d9a1882a1e4562333debd/osqp-0.6.7.post3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:502fde0ae710cef1e6418fb8d26efef9597d1dcba877489a1c2eb9c3eb2ff2e9", size = 300002 }, - { url = "https://files.pythonhosted.org/packages/57/b5/958d4188cb9347e420d3de2d19d8cb1113f691b7a093cdef67f86b598f30/osqp-0.6.7.post3-cp312-cp312-win_amd64.whl", hash = "sha256:468588cfb690becba4d1f460c2a53e75530584e3efcf2caed59f5219032e6888", size = 293164 }, + { url = "https://files.pythonhosted.org/packages/22/37/28dd657c74d28b46c8bb7ea2aeea981bd6f33676dd82ebcc0e761c465e6d/osqp-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0334ca6af9e6cb669c96228f176b4727d189f84cc5e79d1c14176aa7ccb59c08", size = 319433, upload-time = "2026-01-26T21:12:29.496Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/9214e4dc8eaece292c59dc29bf9a8b4e66604036e7a618c47d2d308f9c20/osqp-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:49dba64b2809d4dad8aeab97a7af16cf6eca7b24d54dfc9b4cfce69bdf0264cc", size = 301924, upload-time = "2026-01-26T21:12:30.755Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/f514b53d44d49eba6bcfc25cec5d941e09bea8bfcaab54b7f0900ac5ad48/osqp-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f79a1c3fde43174d2939da662d9e53de5e7ea2b62162b49ca3a6c2cc2b8a069", size = 336550, upload-time = "2026-01-26T21:12:31.721Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/8934416cfb20a37589762a5e374c9f5530ccc89fad3e3347c59e627c3f18/osqp-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:201fa314dc5766c34f5bd6b93c0eaff53f7656f070f2b404057c4ed8bd0d8682", size = 357664, upload-time = "2026-01-26T21:12:32.751Z" }, + { url = "https://files.pythonhosted.org/packages/08/b2/cc5834e4b277582ae831e2b5b2a4dd1f571c6ea1db605222082d74794681/osqp-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f8ca54c90381b70130b5e49125a0d40676ae6230efcb5255eb70e940bfbc25a", size = 310066, upload-time = "2026-01-26T21:12:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/86/a5c4ffd6d71e08a26bd1a076e740c7579e289bf6b54ae481b908e20f954b/osqp-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3674e1fbe8479fa9e275918be2d2672dfd0f4d0b257813cde139a10103d5fcf9", size = 321422, upload-time = "2026-01-26T21:12:35.523Z" }, + { url = "https://files.pythonhosted.org/packages/d1/47/2a5f7d26288b92ca6ae7eb777e61487732317f282054854e38c1cf1abcee/osqp-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b15bc820da970f4b5706e5ad3cbbb9a45c7864df3e613ed76966d81d7e5db1a9", size = 302196, upload-time = "2026-01-26T21:12:36.825Z" }, + { url = "https://files.pythonhosted.org/packages/66/f9/5ba3954a7ecfc7fd42e346a63621ccb04343467db668a4d5477f5c205c52/osqp-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2983851b91143bb12d2acf6061471d7dd6b885c0cfa440219a57a85b7d19c25", size = 335033, upload-time = "2026-01-26T21:12:38.693Z" }, + { url = "https://files.pythonhosted.org/packages/39/36/ca736c5404bc9948bffc0dc464647d24e0b2fd7c69126cd0d67c205894f4/osqp-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:142feeb1b66a309abdd2cd206d5b0effe97da8111c4387d72601453804efd994", size = 357634, upload-time = "2026-01-26T21:12:39.642Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/bee8cc33b517d1b93671828497270b0ef69338cba548271a6b5456f2080b/osqp-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a38956e5cdf7bb2213a906c4dfe9b722c44c24d3741380b2c43c31e5e7b9e2b5", size = 310710, upload-time = "2026-01-26T21:12:40.897Z" }, ] [[package]] name = "packaging" -version = "24.2" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/da/b1dc0481ab8d55d0f46e343cfe67d4551a0e14fcee52bd38ca1bd73258d8/pandas-3.0.0.tar.gz", hash = "sha256:0facf7e87d38f721f0af46fe70d97373a37701b1c09f7ed7aeeb292ade5c050f", size = 4633005, upload-time = "2026-01-21T15:52:04.726Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, + { url = "https://files.pythonhosted.org/packages/46/1e/b184654a856e75e975a6ee95d6577b51c271cd92cb2b020c9378f53e0032/pandas-3.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d64ce01eb9cdca96a15266aa679ae50212ec52757c79204dbc7701a222401850", size = 10313247, upload-time = "2026-01-21T15:50:15.775Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/e04a547ad0f0183bf151fd7c7a477468e3b85ff2ad231c566389e6cc9587/pandas-3.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:613e13426069793aa1ec53bdcc3b86e8d32071daea138bbcf4fa959c9cdaa2e2", size = 9913131, upload-time = "2026-01-21T15:50:18.611Z" }, + { url = "https://files.pythonhosted.org/packages/a2/93/bb77bfa9fc2aba9f7204db807d5d3fb69832ed2854c60ba91b4c65ba9219/pandas-3.0.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0192fee1f1a8e743b464a6607858ee4b071deb0b118eb143d71c2a1d170996d5", size = 10741925, upload-time = "2026-01-21T15:50:21.058Z" }, + { url = "https://files.pythonhosted.org/packages/62/fb/89319812eb1d714bfc04b7f177895caeba8ab4a37ef6712db75ed786e2e0/pandas-3.0.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0b853319dec8d5e0c8b875374c078ef17f2269986a78168d9bd57e49bf650ae", size = 11245979, upload-time = "2026-01-21T15:50:23.413Z" }, + { url = "https://files.pythonhosted.org/packages/a9/63/684120486f541fc88da3862ed31165b3b3e12b6a1c7b93be4597bc84e26c/pandas-3.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:707a9a877a876c326ae2cb640fbdc4ef63b0a7b9e2ef55c6df9942dcee8e2af9", size = 11756337, upload-time = "2026-01-21T15:50:25.932Z" }, + { url = "https://files.pythonhosted.org/packages/39/92/7eb0ad232312b59aec61550c3c81ad0743898d10af5df7f80bc5e5065416/pandas-3.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:afd0aa3d0b5cda6e0b8ffc10dbcca3b09ef3cbcd3fe2b27364f85fdc04e1989d", size = 12325517, upload-time = "2026-01-21T15:50:27.952Z" }, + { url = "https://files.pythonhosted.org/packages/51/27/bf9436dd0a4fc3130acec0828951c7ef96a0631969613a9a35744baf27f6/pandas-3.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:113b4cca2614ff7e5b9fee9b6f066618fe73c5a83e99d721ffc41217b2bf57dd", size = 9881576, upload-time = "2026-01-21T15:50:30.149Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2b/c618b871fce0159fd107516336e82891b404e3f340821853c2fc28c7830f/pandas-3.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c14837eba8e99a8da1527c0280bba29b0eb842f64aa94982c5e21227966e164b", size = 9140807, upload-time = "2026-01-21T15:50:32.308Z" }, + { url = "https://files.pythonhosted.org/packages/0b/38/db33686f4b5fa64d7af40d96361f6a4615b8c6c8f1b3d334eee46ae6160e/pandas-3.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9803b31f5039b3c3b10cc858c5e40054adb4b29b4d81cb2fd789f4121c8efbcd", size = 10334013, upload-time = "2026-01-21T15:50:34.771Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7b/9254310594e9774906bacdd4e732415e1f86ab7dbb4b377ef9ede58cd8ec/pandas-3.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14c2a4099cd38a1d18ff108168ea417909b2dea3bd1ebff2ccf28ddb6a74d740", size = 9874154, upload-time = "2026-01-21T15:50:36.67Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/726c5a67a13bc66643e66d2e9ff115cead482a44fc56991d0c4014f15aaf/pandas-3.0.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d257699b9a9960e6125686098d5714ac59d05222bef7a5e6af7a7fd87c650801", size = 10384433, upload-time = "2026-01-21T15:50:39.132Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2e/9211f09bedb04f9832122942de8b051804b31a39cfbad199a819bb88d9f3/pandas-3.0.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69780c98f286076dcafca38d8b8eee1676adf220199c0a39f0ecbf976b68151a", size = 10864519, upload-time = "2026-01-21T15:50:41.043Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/50858522cdc46ac88b9afdc3015e298959a70a08cd21e008a44e9520180c/pandas-3.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4a66384f017240f3858a4c8a7cf21b0591c3ac885cddb7758a589f0f71e87ebb", size = 11394124, upload-time = "2026-01-21T15:50:43.377Z" }, + { url = "https://files.pythonhosted.org/packages/86/3f/83b2577db02503cd93d8e95b0f794ad9d4be0ba7cb6c8bcdcac964a34a42/pandas-3.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be8c515c9bc33989d97b89db66ea0cececb0f6e3c2a87fcc8b69443a6923e95f", size = 11920444, upload-time = "2026-01-21T15:50:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/64/2d/4f8a2f192ed12c90a0aab47f5557ece0e56b0370c49de9454a09de7381b2/pandas-3.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:a453aad8c4f4e9f166436994a33884442ea62aa8b27d007311e87521b97246e1", size = 9730970, upload-time = "2026-01-21T15:50:47.962Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/ff571be435cf1e643ca98d0945d76732c0b4e9c37191a89c8550b105eed1/pandas-3.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:da768007b5a33057f6d9053563d6b74dd6d029c337d93c6d0d22a763a5c2ecc0", size = 9041950, upload-time = "2026-01-21T15:50:50.422Z" }, ] [[package]] name = "pefile" -version = "2023.2.7" +version = "2024.8.26" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/c5/3b3c62223f72e2360737fd2a57c30e5b2adecd85e70276879609a7403334/pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc", size = 74854 } +sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/26/d0ad8b448476d0a1e8d3ea5622dc77b916db84c6aa3cb1e1c0965af948fc/pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6", size = 71791 }, + { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, ] [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "pyasn1" -version = "0.6.1" +version = "0.6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, + { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" }, ] [[package]] name = "pyasn1-modules" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/67/6afbf0d507f73c32d21084a79946bfcfca5fbc62a72057e9c23797a737c9/pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c", size = 310028 } +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", size = 181537 }, + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] [[package]] name = "pycparser" -version = "2.22" +version = "3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] [[package]] name = "pyinstaller" -version = "6.11.1" +version = "6.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altgraph" }, @@ -539,105 +742,97 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/55/d4/54f5f5c73b803e6256ea97ffc6ba8a305d9a5f57f85f9b00b282512bf18a/pyinstaller-6.11.1.tar.gz", hash = "sha256:491dfb4d9d5d1d9650d9507daec1ff6829527a254d8e396badd60a0affcb72ef", size = 4249772 } +sdist = { url = "https://files.pythonhosted.org/packages/9f/b8/0fe3359920b0a4e7008e0e93ff383003763e3eee3eb31a07c52868722960/pyinstaller-6.18.0.tar.gz", hash = "sha256:cdc507542783511cad4856fce582fdc37e9f29665ca596889c663c83ec8c6ec9", size = 4034976, upload-time = "2026-01-13T03:13:23.886Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/15/b0f1c0985ee32fcd2f6ad9a486ef94e4db3fef9af025a3655e76cb708009/pyinstaller-6.11.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:44e36172de326af6d4e7663b12f71dbd34e2e3e02233e181e457394423daaf03", size = 991780 }, - { url = "https://files.pythonhosted.org/packages/fd/0f/9f54cb18abe2b1d89051bc9214c0cb40d7b5f4049c151c315dacc067f4a2/pyinstaller-6.11.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:6d12c45a29add78039066a53fb05967afaa09a672426072b13816fe7676abfc4", size = 711739 }, - { url = "https://files.pythonhosted.org/packages/32/f7/79d10830780eff8339bfa793eece1df4b2459e35a712fc81983e8536cc29/pyinstaller-6.11.1-py3-none-manylinux2014_i686.whl", hash = "sha256:ddc0fddd75f07f7e423da1f0822e389a42af011f9589e0269b87e0d89aa48c1f", size = 714053 }, - { url = "https://files.pythonhosted.org/packages/25/f7/9961ef02cdbd2dbb1b1a215292656bd0ea72a83aafd8fb6373513849711e/pyinstaller-6.11.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:0d6475559c4939f0735122989611d7f739ed3bf02f666ce31022928f7a7e4fda", size = 719133 }, - { url = "https://files.pythonhosted.org/packages/6f/4d/7f854842a1ce798de762a0b0bc5d5a4fc26ad06164a98575dc3c54abed1f/pyinstaller-6.11.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:e21c7806e34f40181e7606926a14579f848bfb1dc52cbca7eea66eccccbfe977", size = 709591 }, - { url = "https://files.pythonhosted.org/packages/7f/e0/00d29fc90c3ba50620c61554e26ebb4d764569507be7cd1c8794aa696f9a/pyinstaller-6.11.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:32c742a24fe65d0702958fadf4040f76de85859c26bec0008766e5dbabc5b68f", size = 710068 }, - { url = "https://files.pythonhosted.org/packages/3e/57/d14b44a69f068d2caaee49d15e45f9fa0f37c6a2d2ad778c953c1722a1ca/pyinstaller-6.11.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:208c0ef6dab0837a0a273ea32d1a3619a208e3d1fe3fec3785eea71a77fd00ce", size = 714439 }, - { url = "https://files.pythonhosted.org/packages/88/01/256824bb57ca208099c86c2fb289f888ca7732580e91ced48fa14e5903b2/pyinstaller-6.11.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:ad84abf465bcda363c1d54eafa76745d77b6a8a713778348377dc98d12a452f7", size = 710457 }, - { url = "https://files.pythonhosted.org/packages/7c/f0/98c9138f5f0ff17462f1ad6d712dcfa643b9a283d6238d464d8145bc139d/pyinstaller-6.11.1-py3-none-win32.whl", hash = "sha256:2e8365276c5131c9bef98e358fbc305e4022db8bedc9df479629d6414021956a", size = 1280261 }, - { url = "https://files.pythonhosted.org/packages/7d/08/f43080614b3e8bce481d4dfd580e579497c7dcdaf87656d9d2ad912e5796/pyinstaller-6.11.1-py3-none-win_amd64.whl", hash = "sha256:7ac83c0dc0e04357dab98c487e74ad2adb30e7eb186b58157a8faf46f1fa796f", size = 1340482 }, - { url = "https://files.pythonhosted.org/packages/ed/56/953c6594cb66e249563854c9cc04ac5a055c6c99d1614298feeaeaa9b87e/pyinstaller-6.11.1-py3-none-win_arm64.whl", hash = "sha256:35e6b8077d240600bb309ed68bb0b1453fd2b7ab740b66d000db7abae6244423", size = 1267519 }, + { url = "https://files.pythonhosted.org/packages/73/e6/51b0146a1a3eec619e58f5d69fb4e3d0f65a31cbddbeef557c9bb83eeed9/pyinstaller-6.18.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:cb7aa5a71bfa7c0af17a4a4e21855663c89e4bd7c40f1d337c8370636d8847c3", size = 1040056, upload-time = "2026-01-13T03:12:15.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9c/a3634c0ec8e1ed31b373b548848b5c0b39b56edc191cf737e697d484ec23/pyinstaller-6.18.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:07785459b3bf8a48889eac0b4d0667ade84aef8930ce030bc7cbb32f41283b33", size = 734971, upload-time = "2026-01-13T03:12:20.912Z" }, + { url = "https://files.pythonhosted.org/packages/2c/04/6756442078ccfcd552ccce636be1574035e62f827ffa1f5d8a0382682546/pyinstaller-6.18.0-py3-none-manylinux2014_i686.whl", hash = "sha256:f998675b7ccb2dabbb1dc2d6f18af61d55428ad6d38e6c4d700417411b697d37", size = 746637, upload-time = "2026-01-13T03:12:29.302Z" }, + { url = "https://files.pythonhosted.org/packages/54/39/fbc56519000cdbf450f472692a7b9b55d42077ce8529f1be631db7b75a36/pyinstaller-6.18.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:779817a0cf69604cddcdb5be1fd4959dc2ce048d6355c73e5da97884df2f3387", size = 744343, upload-time = "2026-01-13T03:12:33.369Z" }, + { url = "https://files.pythonhosted.org/packages/36/f2/50887badf282fee776e83d1e4feab74c026f50a1ea16e109ed939e32aa28/pyinstaller-6.18.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:31b5d109f8405be0b7cddcede43e7b074792bc9a5bbd54ec000a3e779183c2af", size = 741084, upload-time = "2026-01-13T03:12:37.528Z" }, + { url = "https://files.pythonhosted.org/packages/1c/08/3a1419183e4713ef77d912ecbdd6ef858689ed9deb34d547133f724ca745/pyinstaller-6.18.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:4328c9837f1aef4fe1a127d4ff1b09a12ce53c827ce87c94117628b0e1fd098b", size = 740943, upload-time = "2026-01-13T03:12:41.589Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/309305e36d116f1434b42d91c420ff951fa79b2c398bbd59930c830450be/pyinstaller-6.18.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:3638fc81eb948e5e5eab1d4ad8f216e3fec6d4a350648304f0adb227b746ee5e", size = 740107, upload-time = "2026-01-13T03:12:45.694Z" }, + { url = "https://files.pythonhosted.org/packages/83/0f/a59a95cd1df59ddbc9e74d5a663387551333bcf19a5dd3086f5c81a2e83c/pyinstaller-6.18.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe59da34269e637f97fd3c43024f764586fc319141d245ff1a2e9af1036aa3", size = 739843, upload-time = "2026-01-13T03:12:49.728Z" }, + { url = "https://files.pythonhosted.org/packages/9a/09/e7a870e7205cdbd2f8785010a5d3fe48a9df2591156ee34a8b29b774fa14/pyinstaller-6.18.0-py3-none-win32.whl", hash = "sha256:496205e4fa92ec944f9696eb597962a83aef4d4c3479abfab83d730e1edf016b", size = 1323811, upload-time = "2026-01-13T03:12:55.717Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d5/48eef2002b6d3937ceac2717fe17e9ca3a43a4c9826bafee367dfc75ba85/pyinstaller-6.18.0-py3-none-win_amd64.whl", hash = "sha256:976fabd90ecfbda47571c87055ad73413ec615ff7dea35e12a4304174de78de9", size = 1384389, upload-time = "2026-01-13T03:13:01.993Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/1a88e6e94107de3ea1c842fd59c3aa132d344ad8e52ea458ffa9a748726e/pyinstaller-6.18.0-py3-none-win_arm64.whl", hash = "sha256:dba4b70e3c9ba09aab51152c72a08e58a751851548f77ad35944d32a300c8381", size = 1324869, upload-time = "2026-01-13T03:13:08.192Z" }, ] [[package]] name = "pyinstaller-hooks-contrib" -version = "2025.1" +version = "2026.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2f/1b/dc256d42f4217db99b50d6d32dbbf841a41b9615506cde77d2345d94f4a5/pyinstaller_hooks_contrib-2025.1.tar.gz", hash = "sha256:130818f9e9a0a7f2261f1fd66054966a3a50c99d000981c5d1db11d3ad0c6ab2", size = 147043 } +sdist = { url = "https://files.pythonhosted.org/packages/31/8f/8052ff65067697ee80fde45b9731842e160751c41ac5690ba232c22030e8/pyinstaller_hooks_contrib-2026.0.tar.gz", hash = "sha256:0120893de491a000845470ca9c0b39284731ac6bace26f6849dea9627aaed48e", size = 170311, upload-time = "2026-01-20T00:15:23.922Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/48/833d67a585275e395f351e5787b4b7a8d462d87bca22a8c038f6ffdc2b3c/pyinstaller_hooks_contrib-2025.1-py3-none-any.whl", hash = "sha256:d3c799470cbc0bda60dcc8e6b4ab976777532b77621337f2037f558905e3a8e9", size = 346409 }, + { url = "https://files.pythonhosted.org/packages/d5/b1/9da6ec3e88696018ee7bb9dc4a7310c2cfaebf32923a19598cd342767c10/pyinstaller_hooks_contrib-2026.0-py3-none-any.whl", hash = "sha256:0590db8edeba3e6c30c8474937021f5cd39c0602b4d10f74a064c73911efaca5", size = 452318, upload-time = "2026-01-20T00:15:21.88Z" }, ] [[package]] name = "pyparsing" -version = "3.2.1" +version = "3.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/1a/3544f4f299a47911c2ab3710f534e52fea62a633c96806995da5d25be4b2/pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a", size = 1067694 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1", size = 107716 }, + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] [[package]] name = "pytest" -version = "8.3.4" +version = "9.0.2" 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/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] name = "pytest-cov" -version = "6.0.0" +version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, + { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" }, ] [[package]] -name = "pywin32-ctypes" -version = "0.2.3" +name = "python-dateutil" +version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471 } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] -name = "qdldl" -version = "0.1.7.post5" +name = "pywin32-ctypes" +version = "0.2.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "scipy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/88/9254835c513a381b8c6d52773060844acf76dfa739648c18f61809c8ee04/qdldl-0.1.7.post5.tar.gz", hash = "sha256:0b1399e1c49b5bed5aac8fd63ef08ab708d340c37fb426fe00128bc1f36b286e", size = 73920 } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/15/6c/ce4cab36da9a7c0bff69067377b513ec88ff753de07f33f65959f4141308/qdldl-0.1.7.post5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa22df45e625c763d129b2893b284b7bde16a535a7e900288d588be9dc24fe9f", size = 106139 }, - { url = "https://files.pythonhosted.org/packages/86/cf/641787a0c64019e76eb8bea925930005960323f1a5539361c209613f4747/qdldl-0.1.7.post5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7e196871dafe4febb86c2886713c8a2226d19455226e56e3b9480aa78eb59b5e", size = 103421 }, - { url = "https://files.pythonhosted.org/packages/be/87/91d2f0debdd515b653c701c023b939325c51157d74154336b8495f156659/qdldl-0.1.7.post5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba5ff31a66d1f92b41d0b97d27288d28a8c849dd6db2221a579b1a5a5a6df0f", size = 1179946 }, - { url = "https://files.pythonhosted.org/packages/b8/7e/5fe5a081bd229a2b703a4b93e5ecaf44f51902e9b6a645c8ce4ea325ec0d/qdldl-0.1.7.post5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34872867c2bcac60279034594eac8dee042b9dedd4c45948e55884b8c5c9cd0", size = 1193311 }, - { url = "https://files.pythonhosted.org/packages/53/dc/d6b760217f0fa7007e45c03dc0193c828ee5010f037acb58b79cd0010fbc/qdldl-0.1.7.post5-cp311-cp311-win_amd64.whl", hash = "sha256:b1280e886f734e3d0d67f643e3d76c55d2e23d0e7b06d89b987681dc165892c5", size = 90488 }, - { url = "https://files.pythonhosted.org/packages/14/c1/eba61a848f9dfa0b54e954aa71f18eb35576f8842ef31dc76a3569a50526/qdldl-0.1.7.post5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d67a95d0ba73147a05cf98dc9284103f64150c9e2c214cd35ee0258f06922c5e", size = 106277 }, - { url = "https://files.pythonhosted.org/packages/02/2e/5daa29b8ecf25277c36a220ef3b509d2ec4079ab81ff3adc544bc12cd675/qdldl-0.1.7.post5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e23d684427ce49f5d657e353322363555d1a31605fe72cbe4b965a4e260742c", size = 103179 }, - { url = "https://files.pythonhosted.org/packages/c6/74/5818f5027a0c252d1e8a2eba996359155d1518db90ce545f1becf0dd4a10/qdldl-0.1.7.post5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c4953d4fe61951fb515a6439009248b5a7b73627d74ee929d02b19bea41b19d", size = 1182542 }, - { url = "https://files.pythonhosted.org/packages/ae/55/90ad03c32e673a9b33cfa7cb43f55d1ab0509b60396afbb1031fa1516fd9/qdldl-0.1.7.post5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:520dbe4006a333c773ff474d2dc1e0af928c0dc7d9ca36db5637ba738ee608ba", size = 1201734 }, - { url = "https://files.pythonhosted.org/packages/c1/82/730d0d2c6093c4dc574947eea94e0cddeea836f43823a80fc8b064a82ddf/qdldl-0.1.7.post5-cp312-cp312-win_amd64.whl", hash = "sha256:13dfc0b225a5c180512488fa51f1771e8fa3c06d7fce9fd3c1d018bc03ba0eec", size = 90706 }, + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, ] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -645,9 +840,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] @@ -658,117 +853,166 @@ dependencies = [ { name = "oauthlib" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, ] [[package]] name = "rsa" -version = "4.9" +version = "4.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyasn1" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] [[package]] name = "ruff" -version = "0.9.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/39/8b/a86c300359861b186f18359adf4437ac8e4c52e42daa9eedc731ef9d5b53/ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6", size = 3669813 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/f3/3a1d22973291226df4b4e2ff70196b926b6f910c488479adb0eeb42a0d7f/ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4", size = 11774588 }, - { url = "https://files.pythonhosted.org/packages/8e/c9/b881f4157b9b884f2994fd08ee92ae3663fb24e34b0372ac3af999aa7fc6/ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66", size = 11746848 }, - { url = "https://files.pythonhosted.org/packages/14/89/2f546c133f73886ed50a3d449e6bf4af27d92d2f960a43a93d89353f0945/ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9", size = 11177525 }, - { url = "https://files.pythonhosted.org/packages/d7/93/6b98f2c12bf28ab9def59c50c9c49508519c5b5cfecca6de871cf01237f6/ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903", size = 11996580 }, - { url = "https://files.pythonhosted.org/packages/8e/3f/b3fcaf4f6d875e679ac2b71a72f6691a8128ea3cb7be07cbb249f477c061/ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721", size = 11525674 }, - { url = "https://files.pythonhosted.org/packages/f0/48/33fbf18defb74d624535d5d22adcb09a64c9bbabfa755bc666189a6b2210/ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b", size = 12739151 }, - { url = "https://files.pythonhosted.org/packages/63/b5/7e161080c5e19fa69495cbab7c00975ef8a90f3679caa6164921d7f52f4a/ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22", size = 13416128 }, - { url = "https://files.pythonhosted.org/packages/4e/c8/b5e7d61fb1c1b26f271ac301ff6d9de5e4d9a9a63f67d732fa8f200f0c88/ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49", size = 12870858 }, - { url = "https://files.pythonhosted.org/packages/da/cb/2a1a8e4e291a54d28259f8fc6a674cd5b8833e93852c7ef5de436d6ed729/ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef", size = 14786046 }, - { url = "https://files.pythonhosted.org/packages/ca/6c/c8f8a313be1943f333f376d79724260da5701426c0905762e3ddb389e3f4/ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb", size = 12550834 }, - { url = "https://files.pythonhosted.org/packages/9d/ad/f70cf5e8e7c52a25e166bdc84c082163c9c6f82a073f654c321b4dff9660/ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0", size = 11961307 }, - { url = "https://files.pythonhosted.org/packages/52/d5/4f303ea94a5f4f454daf4d02671b1fbfe2a318b5fcd009f957466f936c50/ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62", size = 11612039 }, - { url = "https://files.pythonhosted.org/packages/eb/c8/bd12a23a75603c704ce86723be0648ba3d4ecc2af07eecd2e9fa112f7e19/ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0", size = 12168177 }, - { url = "https://files.pythonhosted.org/packages/cc/57/d648d4f73400fef047d62d464d1a14591f2e6b3d4a15e93e23a53c20705d/ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606", size = 12610122 }, - { url = "https://files.pythonhosted.org/packages/49/79/acbc1edd03ac0e2a04ae2593555dbc9990b34090a9729a0c4c0cf20fb595/ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d", size = 9988751 }, - { url = "https://files.pythonhosted.org/packages/6d/95/67153a838c6b6ba7a2401241fd8a00cd8c627a8e4a0491b8d853dedeffe0/ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c", size = 11002987 }, - { url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763 }, +version = "0.14.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" }, + { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" }, + { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" }, + { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" }, + { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" }, + { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" }, + { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, + { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, + { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, ] [[package]] name = "scipy" -version = "1.15.2" +version = "1.17.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/1f/bf0a5f338bda7c35c08b4ed0df797e7bafe8a78a97275e9f439aceb46193/scipy-1.15.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:92233b2df6938147be6fa8824b8136f29a18f016ecde986666be5f4d686a91a4", size = 38703651 }, - { url = "https://files.pythonhosted.org/packages/de/54/db126aad3874601048c2c20ae3d8a433dbfd7ba8381551e6f62606d9bd8e/scipy-1.15.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:62ca1ff3eb513e09ed17a5736929429189adf16d2d740f44e53270cc800ecff1", size = 30102038 }, - { url = "https://files.pythonhosted.org/packages/61/d8/84da3fffefb6c7d5a16968fe5b9f24c98606b165bb801bb0b8bc3985200f/scipy-1.15.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c6676490ad76d1c2894d77f976144b41bd1a4052107902238047fb6a473e971", size = 22375518 }, - { url = "https://files.pythonhosted.org/packages/44/78/25535a6e63d3b9c4c90147371aedb5d04c72f3aee3a34451f2dc27c0c07f/scipy-1.15.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8bf5cb4a25046ac61d38f8d3c3426ec11ebc350246a4642f2f315fe95bda655", size = 25142523 }, - { url = "https://files.pythonhosted.org/packages/e0/22/4b4a26fe1cd9ed0bc2b2cb87b17d57e32ab72c346949eaf9288001f8aa8e/scipy-1.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a8e34cf4c188b6dd004654f88586d78f95639e48a25dfae9c5e34a6dc34547e", size = 35491547 }, - { url = "https://files.pythonhosted.org/packages/32/ea/564bacc26b676c06a00266a3f25fdfe91a9d9a2532ccea7ce6dd394541bc/scipy-1.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a0d2c2075946346e4408b211240764759e0fabaeb08d871639b5f3b1aca8a0", size = 37634077 }, - { url = "https://files.pythonhosted.org/packages/43/c2/bfd4e60668897a303b0ffb7191e965a5da4056f0d98acfb6ba529678f0fb/scipy-1.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42dabaaa798e987c425ed76062794e93a243be8f0f20fff6e7a89f4d61cb3d40", size = 37231657 }, - { url = "https://files.pythonhosted.org/packages/4a/75/5f13050bf4f84c931bcab4f4e83c212a36876c3c2244475db34e4b5fe1a6/scipy-1.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f5e296ec63c5da6ba6fa0343ea73fd51b8b3e1a300b0a8cae3ed4b1122c7462", size = 40035857 }, - { url = "https://files.pythonhosted.org/packages/b9/8b/7ec1832b09dbc88f3db411f8cdd47db04505c4b72c99b11c920a8f0479c3/scipy-1.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:597a0c7008b21c035831c39927406c6181bcf8f60a73f36219b69d010aa04737", size = 41217654 }, - { url = "https://files.pythonhosted.org/packages/4b/5d/3c78815cbab499610f26b5bae6aed33e227225a9fa5290008a733a64f6fc/scipy-1.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c4697a10da8f8765bb7c83e24a470da5797e37041edfd77fd95ba3811a47c4fd", size = 38756184 }, - { url = "https://files.pythonhosted.org/packages/37/20/3d04eb066b471b6e171827548b9ddb3c21c6bbea72a4d84fc5989933910b/scipy-1.15.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:869269b767d5ee7ea6991ed7e22b3ca1f22de73ab9a49c44bad338b725603301", size = 30163558 }, - { url = "https://files.pythonhosted.org/packages/a4/98/e5c964526c929ef1f795d4c343b2ff98634ad2051bd2bbadfef9e772e413/scipy-1.15.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bad78d580270a4d32470563ea86c6590b465cb98f83d760ff5b0990cb5518a93", size = 22437211 }, - { url = "https://files.pythonhosted.org/packages/1d/cd/1dc7371e29195ecbf5222f9afeedb210e0a75057d8afbd942aa6cf8c8eca/scipy-1.15.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:b09ae80010f52efddb15551025f9016c910296cf70adbf03ce2a8704f3a5ad20", size = 25232260 }, - { url = "https://files.pythonhosted.org/packages/f0/24/1a181a9e5050090e0b5138c5f496fee33293c342b788d02586bc410c6477/scipy-1.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a6fd6eac1ce74a9f77a7fc724080d507c5812d61e72bd5e4c489b042455865e", size = 35198095 }, - { url = "https://files.pythonhosted.org/packages/c0/53/eaada1a414c026673eb983f8b4a55fe5eb172725d33d62c1b21f63ff6ca4/scipy-1.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b871df1fe1a3ba85d90e22742b93584f8d2b8e6124f8372ab15c71b73e428b8", size = 37297371 }, - { url = "https://files.pythonhosted.org/packages/e9/06/0449b744892ed22b7e7b9a1994a866e64895363572677a316a9042af1fe5/scipy-1.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:03205d57a28e18dfd39f0377d5002725bf1f19a46f444108c29bdb246b6c8a11", size = 36872390 }, - { url = "https://files.pythonhosted.org/packages/6a/6f/a8ac3cfd9505ec695c1bc35edc034d13afbd2fc1882a7c6b473e280397bb/scipy-1.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:601881dfb761311045b03114c5fe718a12634e5608c3b403737ae463c9885d53", size = 39700276 }, - { url = "https://files.pythonhosted.org/packages/f5/6f/e6e5aff77ea2a48dd96808bb51d7450875af154ee7cbe72188afb0b37929/scipy-1.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:e7c68b6a43259ba0aab737237876e5c2c549a031ddb7abc28c7b47f22e202ded", size = 40942317 }, +sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/4b/c89c131aa87cad2b77a54eb0fb94d633a842420fa7e919dc2f922037c3d8/scipy-1.17.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:2abd71643797bd8a106dff97894ff7869eeeb0af0f7a5ce02e4227c6a2e9d6fd", size = 31381316, upload-time = "2026-01-10T21:24:33.42Z" }, + { url = "https://files.pythonhosted.org/packages/5e/5f/a6b38f79a07d74989224d5f11b55267714707582908a5f1ae854cf9a9b84/scipy-1.17.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:ef28d815f4d2686503e5f4f00edc387ae58dfd7a2f42e348bb53359538f01558", size = 27966760, upload-time = "2026-01-10T21:24:38.911Z" }, + { url = "https://files.pythonhosted.org/packages/c1/20/095ad24e031ee8ed3c5975954d816b8e7e2abd731e04f8be573de8740885/scipy-1.17.0-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:272a9f16d6bb4667e8b50d25d71eddcc2158a214df1b566319298de0939d2ab7", size = 20138701, upload-time = "2026-01-10T21:24:43.249Z" }, + { url = "https://files.pythonhosted.org/packages/89/11/4aad2b3858d0337756f3323f8960755704e530b27eb2a94386c970c32cbe/scipy-1.17.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:7204fddcbec2fe6598f1c5fdf027e9f259106d05202a959a9f1aecf036adc9f6", size = 22480574, upload-time = "2026-01-10T21:24:47.266Z" }, + { url = "https://files.pythonhosted.org/packages/85/bd/f5af70c28c6da2227e510875cadf64879855193a687fb19951f0f44cfd6b/scipy-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc02c37a5639ee67d8fb646ffded6d793c06c5622d36b35cfa8fe5ececb8f042", size = 32862414, upload-time = "2026-01-10T21:24:52.566Z" }, + { url = "https://files.pythonhosted.org/packages/ef/df/df1457c4df3826e908879fe3d76bc5b6e60aae45f4ee42539512438cfd5d/scipy-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac97a27520d66c12a34fd90a4fe65f43766c18c0d6e1c0a80f114d2260080e4", size = 35112380, upload-time = "2026-01-10T21:24:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/5f/bb/88e2c16bd1dd4de19d80d7c5e238387182993c2fb13b4b8111e3927ad422/scipy-1.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb7446a39b3ae0fe8f416a9a3fdc6fba3f11c634f680f16a239c5187bc487c0", size = 34922676, upload-time = "2026-01-10T21:25:04.287Z" }, + { url = "https://files.pythonhosted.org/packages/02/ba/5120242cc735f71fc002cff0303d536af4405eb265f7c60742851e7ccfe9/scipy-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:474da16199f6af66601a01546144922ce402cb17362e07d82f5a6cf8f963e449", size = 37507599, upload-time = "2026-01-10T21:25:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/52/c8/08629657ac6c0da198487ce8cd3de78e02cfde42b7f34117d56a3fe249dc/scipy-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:255c0da161bd7b32a6c898e7891509e8a9289f0b1c6c7d96142ee0d2b114c2ea", size = 36380284, upload-time = "2026-01-10T21:25:15.632Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4a/465f96d42c6f33ad324a40049dfd63269891db9324aa66c4a1c108c6f994/scipy-1.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:85b0ac3ad17fa3be50abd7e69d583d98792d7edc08367e01445a1e2076005379", size = 24370427, upload-time = "2026-01-10T21:25:20.514Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" }, + { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" }, + { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" }, ] [[package]] name = "scs" -version = "3.2.7.post2" +version = "3.2.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "scipy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c5/2f/3f15676b0f4cc73879400d94f2c5c64130cad0bbca266aff1365dc643e79/scs-3.2.7.post2.tar.gz", hash = "sha256:4245a4f76328cc73911f20e1414df68d41ead4bcc4a187503a9cd639b644014b", size = 1600725 } +sdist = { url = "https://files.pythonhosted.org/packages/9e/59/5cb7f9612a5a3ff6efd4ab2d899902a536cc5974a7edb589084c5577291c/scs-3.2.11.tar.gz", hash = "sha256:2a5455cf2093d07f84f2f848c199faed52e79cdb3a11fe250b5622b6bbac4913", size = 1691825, upload-time = "2026-01-09T17:53:54.074Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/a8/75e215fb61f65c7dee0a5d2c8e2b9043967fd332b70a4d47478bca45ec10/scs-3.2.7.post2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d551b90d9e2c0497ee17d8c3db325d6fcefa4419057954e68709da8b9184d4f", size = 105857 }, - { url = "https://files.pythonhosted.org/packages/1f/e4/b757b7926cc3355ba41ba747afb5e4d6c553d05be840ea25dc47b47b47b1/scs-3.2.7.post2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c15d035dda04a6626d3cd9b68d3bf814d2e0eb3cb372021775bd358fd8c7405", size = 93594 }, - { url = "https://files.pythonhosted.org/packages/e7/34/bf8e999e13e00946660a1e9009e67d5718356c06b9a2b2905f10829a7c45/scs-3.2.7.post2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6da6add18f039e7e08f0ebc13cb1f853ec4c96ae81d7a578f46e0f9f0e5bf4b5", size = 10443087 }, - { url = "https://files.pythonhosted.org/packages/d3/03/d41749f5c680241345669da533bc7ce6f5f1fac6d88fb255792fdd187e3f/scs-3.2.7.post2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:d6c965f026e56c92b59a9c96744eb90178982c270ab196f58a0260ac392785aa", size = 5066487 }, - { url = "https://files.pythonhosted.org/packages/ac/b1/26db804cdc4009745f4bc4be2a478ac2c29f017672747a3ce64d46bccc7f/scs-3.2.7.post2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0427a5bf9aa43eb2a22083e1a43412db5054a88d695fdaa6018cd6fb3a9f0203", size = 11612089 }, - { url = "https://files.pythonhosted.org/packages/bf/34/a42b90bff9330ac57e1dcc8c35b978cee47d6e9eee14cb71981b801fc7cf/scs-3.2.7.post2-cp311-cp311-win_amd64.whl", hash = "sha256:4d05ec092c891eb842630f343ebc0c46d2ef6047f325a835771b13f9804d6b3b", size = 7432186 }, - { url = "https://files.pythonhosted.org/packages/0b/ef/982d35cadee11137a27c80404155265bb2c4e5899551436ef5e6cc28a0bc/scs-3.2.7.post2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:99e4af2968b046ee55fa0dc89dcd3bfba771f1027d9224cb6efa10008d8bfee1", size = 107289 }, - { url = "https://files.pythonhosted.org/packages/33/2a/f807b0f9dd108c9c75c4d12692803d687be7bd32c91dbfd7213837b3b6ed/scs-3.2.7.post2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc46fef9743d4629337382f034fda92dfce338659e8377afae674517b7d8345f", size = 93544 }, - { url = "https://files.pythonhosted.org/packages/82/0e/f56426e3b3d9ac12dac252c1c4a0e65a530d460b9448e3fc2e20ac8e6bed/scs-3.2.7.post2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f92e925d89004276a449850926a45536f75c03cab701b5e758b1a7efa119ba08", size = 10443128 }, - { url = "https://files.pythonhosted.org/packages/e4/f3/343803e20415bf604e4b237fdce4203f51c35e89707d18eafa7e3fe172d7/scs-3.2.7.post2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:640faf61f85b933fdfc3d33d7ce4f0049b082b245e82d2d6a8c2c54aa0b7f540", size = 5066484 }, - { url = "https://files.pythonhosted.org/packages/33/9a/5b06bc2ba789aa2ce5ba57be503f2563bbc772c0e7b4249e646e44fdcd2b/scs-3.2.7.post2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a520c9bef84eee734df0da3e5e06aa9192d3be34cd5e6d4221cc01f4d09b20c0", size = 11612180 }, - { url = "https://files.pythonhosted.org/packages/30/49/1645fa1219493ac94475ab8f48a2520d2fc27f486327f2b0f167440a8188/scs-3.2.7.post2-cp312-cp312-win_amd64.whl", hash = "sha256:2995d4099943c3fd754b3e39fe178a9c03dcb9c7d84b40f64ac5eb26d8d6085a", size = 7432205 }, + { url = "https://files.pythonhosted.org/packages/13/3d/c76901e5d9b37c90c90810eda2324b54c52976b3171b3c2b35b47b6bd0c2/scs-3.2.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698bdf36c67acc43b7a65f1ffa13d11d09b7050f4da6dd5b9c05080e10d198f7", size = 96304, upload-time = "2026-01-09T17:52:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/2c/da/9fc31759cdccedbe2687da562f3dab61a459171a76bece1e41ae7f7b476a/scs-3.2.11-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aece172705a6e3b04b54b49558d580ab71be02c2fa8fba12b35012e1f386e9c", size = 5071445, upload-time = "2026-01-09T17:52:52.904Z" }, + { url = "https://files.pythonhosted.org/packages/86/b7/4594d24229a763f464e995ad6d00ec5eeee84adfae764f85752e27fdd382/scs-3.2.11-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67a9bf34da4be7baf28eb50c8ea7d2e29ae5f345e4b04f057ba3dbeca42efbba", size = 12079923, upload-time = "2026-01-09T17:52:54.992Z" }, + { url = "https://files.pythonhosted.org/packages/ef/58/608b2efc99ff134964dcae11a91578705eff376bfa46f75956dd7382ad3b/scs-3.2.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9810701691de9da18d98e542e4f8900e197ec501a47b3a4c1c76242cba133453", size = 11973862, upload-time = "2026-01-09T17:52:57.541Z" }, + { url = "https://files.pythonhosted.org/packages/e2/21/bdae36f204f3284ad92493bc09d5e8ae7df0c902b71493ebddbcf1c15e2c/scs-3.2.11-cp311-cp311-win_amd64.whl", hash = "sha256:4bd13200492b9ea334a3c50c17ccbfc9359b206bf7a4f0b022504ebc34e11cda", size = 7478511, upload-time = "2026-01-09T17:53:00.229Z" }, + { url = "https://files.pythonhosted.org/packages/80/74/87a97e5fc2aac7ab3661c2555a25115121734c51eb4ebbabc2127f53bd83/scs-3.2.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad646848375b5cf2d3e45a9ebefd87ccc37a53da9c32f2bf30ea5ad0e84d9e5b", size = 96302, upload-time = "2026-01-09T17:53:01.95Z" }, + { url = "https://files.pythonhosted.org/packages/24/0c/e34764a320249465dc6c11e67a6d34e2e53a9186a64f21759e94dfb043ee/scs-3.2.11-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f30821521a74f6930924b13e731e9455b6bdcfc964f66d5623d3c8d3fdd98126", size = 5071418, upload-time = "2026-01-09T17:53:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/db/3d/dd17a1c1890ce25efd3908f7ab67a56b208e89c5a5d60a2dedaf99394dcb/scs-3.2.11-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a89c71ebacd4790c461d3032a47e59ed4759e11c0f03fa79b5b84086ef9c7bc", size = 12079957, upload-time = "2026-01-09T17:53:05.439Z" }, + { url = "https://files.pythonhosted.org/packages/38/63/6f83bfa17e074c92b17e16a9bd897aedeec64f10f9200c86588d7fc583c2/scs-3.2.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4e37dc60081dd742bdcd63eeb5b260db116b3803162bcf6084eb203ebedcb080", size = 11973936, upload-time = "2026-01-09T17:53:07.926Z" }, + { url = "https://files.pythonhosted.org/packages/1a/fe/5d8f6048a90abc3aa053b5ac2acf3885dc46af94c3baf7d9ccf201a1ce19/scs-3.2.11-cp312-cp312-win_amd64.whl", hash = "sha256:2504266ff8e6a226f7ecb987567c93e6e996534cbf479a60a5a886549446205e", size = 7478461, upload-time = "2026-01-09T17:53:11.899Z" }, ] [[package]] name = "setuptools" -version = "75.8.0" +version = "80.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/92/ec/089608b791d210aec4e7f97488e67ab0d33add3efccb83a056cbafe3a2a6/setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6", size = 1343222 } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3", size = 1228782 }, + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, ] [[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 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sortition-algorithms" +version = "0.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cattrs" }, + { name = "cvxpy" }, + { name = "gspread" }, + { name = "mip" }, + { name = "numpy" }, + { name = "oauth2client" }, + { name = "pandas" }, + { name = "requests" }, + { name = "scikit-learn" }, + { name = "tabulate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/84/0a5ed049807f53a9dd86afc968d59e2b4c64364c4be24ba471aa559cb6e2/sortition_algorithms-0.11.5.tar.gz", hash = "sha256:07cb3091f7002cba42acd613e5337081c02cdea793c4e890a3a62a9b17827702", size = 207184, upload-time = "2026-01-29T11:40:44.817Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/e5/ca/6842dc46cce9c3e62b74dcc2b6269a4addd81f2a137b99b7797234c07267/sortition_algorithms-0.11.5-py3-none-any.whl", hash = "sha256:cb1717bf57fd37237c6dd02c830e06369de615d071548e451ec0b3833ff969a6", size = 84043, upload-time = "2026-01-29T11:40:46.132Z" }, ] [[package]] @@ -776,12 +1020,10 @@ name = "stratification-app" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "cvxpy" }, { name = "eel" }, - { name = "gspread" }, - { name = "mip" }, { name = "oauth2client" }, { name = "pyinstaller" }, + { name = "sortition-algorithms" }, { name = "toml" }, ] @@ -794,13 +1036,11 @@ dev = [ [package.metadata] requires-dist = [ - { name = "cvxpy", specifier = "==1.5.3" }, - { name = "eel", specifier = "==0.17.0" }, - { name = "gspread", specifier = "==6.1.4" }, - { name = "mip", specifier = "==1.15.0" }, - { name = "oauth2client", specifier = "==4.1.3" }, - { name = "pyinstaller", specifier = "==6.11.1" }, - { name = "toml", specifier = "==0.10.2" }, + { name = "eel", specifier = "==0.18.2" }, + { name = "oauth2client", specifier = "<5" }, + { name = "pyinstaller", specifier = "<7" }, + { name = "sortition-algorithms", specifier = "==0.11.5" }, + { name = "toml" }, ] [package.metadata.requires-dev] @@ -810,93 +1050,112 @@ dev = [ { name = "ruff", specifier = ">=0.9.7" }, ] +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + [[package]] name = "toml" version = "0.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253 } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588 }, + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, ] [[package]] name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" }, ] [[package]] -name = "urllib3" -version = "2.3.0" +name = "typing-extensions" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] -name = "whichcraft" -version = "0.6.1" +name = "tzdata" +version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/f5/546c1494f1f8f004de512b5c9c89a8b7afb1d030c9307dd65df48e5772a3/whichcraft-0.6.1.tar.gz", hash = "sha256:acdbb91b63d6a15efbd6430d1d7b2d36e44a71697e93e19b7ded477afd9fce87", size = 6909 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/a2/81887a0dae2e4d2adc70d9a3557fdda969f863ced51cd3c47b587d25bce5/whichcraft-0.6.1-py2.py3-none-any.whl", hash = "sha256:deda9266fbb22b8c64fd3ee45c050d61139cd87419765f588e37c8d23e236dd9", size = 5223 }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] -name = "zope-event" -version = "5.0" +name = "urllib3" +version = "2.6.3" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/c2/427f1867bb96555d1d34342f1dd97f8c420966ab564d58d18469a1db8736/zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd", size = 17350 } + +[[package]] +name = "zope-event" +version = "6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/33/d3eeac228fc14de76615612ee208be2d8a5b5b0fada36bf9b62d6b40600c/zope_event-6.1.tar.gz", hash = "sha256:6052a3e0cb8565d3d4ef1a3a7809336ac519bc4fe38398cb8d466db09adef4f0", size = 18739, upload-time = "2025-11-07T08:05:49.934Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/42/f8dbc2b9ad59e927940325a22d6d3931d630c3644dae7e2369ef5d9ba230/zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26", size = 6824 }, + { url = "https://files.pythonhosted.org/packages/c2/b0/956902e5e1302f8c5d124e219c6bf214e2649f92ad5fce85b05c039a04c9/zope_event-6.1-py3-none-any.whl", hash = "sha256:0ca78b6391b694272b23ec1335c0294cc471065ed10f7f606858fc54566c25a0", size = 6414, upload-time = "2025-11-07T08:05:48.874Z" }, ] [[package]] name = "zope-interface" -version = "7.2" +version = "8.2" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/7d/2e8daf0abea7798d16a58f2f3a2bf7588872eee54ac119f99393fdd47b65/zope.interface-7.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1909f52a00c8c3dcab6c4fad5d13de2285a4b3c7be063b239b8dc15ddfb73bd2", size = 208776 }, - { url = "https://files.pythonhosted.org/packages/a0/2a/0c03c7170fe61d0d371e4c7ea5b62b8cb79b095b3d630ca16719bf8b7b18/zope.interface-7.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:80ecf2451596f19fd607bb09953f426588fc1e79e93f5968ecf3367550396b22", size = 209296 }, - { url = "https://files.pythonhosted.org/packages/49/b4/451f19448772b4a1159519033a5f72672221e623b0a1bd2b896b653943d8/zope.interface-7.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:033b3923b63474800b04cba480b70f6e6243a62208071fc148354f3f89cc01b7", size = 260997 }, - { url = "https://files.pythonhosted.org/packages/65/94/5aa4461c10718062c8f8711161faf3249d6d3679c24a0b81dd6fc8ba1dd3/zope.interface-7.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a102424e28c6b47c67923a1f337ede4a4c2bba3965b01cf707978a801fc7442c", size = 255038 }, - { url = "https://files.pythonhosted.org/packages/9f/aa/1a28c02815fe1ca282b54f6705b9ddba20328fabdc37b8cf73fc06b172f0/zope.interface-7.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25e6a61dcb184453bb00eafa733169ab6d903e46f5c2ace4ad275386f9ab327a", size = 259806 }, - { url = "https://files.pythonhosted.org/packages/a7/2c/82028f121d27c7e68632347fe04f4a6e0466e77bb36e104c8b074f3d7d7b/zope.interface-7.2-cp311-cp311-win_amd64.whl", hash = "sha256:3f6771d1647b1fc543d37640b45c06b34832a943c80d1db214a37c31161a93f1", size = 212305 }, - { url = "https://files.pythonhosted.org/packages/68/0b/c7516bc3bad144c2496f355e35bd699443b82e9437aa02d9867653203b4a/zope.interface-7.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:086ee2f51eaef1e4a52bd7d3111a0404081dadae87f84c0ad4ce2649d4f708b7", size = 208959 }, - { url = "https://files.pythonhosted.org/packages/a2/e9/1463036df1f78ff8c45a02642a7bf6931ae4a38a4acd6a8e07c128e387a7/zope.interface-7.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21328fcc9d5b80768bf051faa35ab98fb979080c18e6f84ab3f27ce703bce465", size = 209357 }, - { url = "https://files.pythonhosted.org/packages/07/a8/106ca4c2add440728e382f1b16c7d886563602487bdd90004788d45eb310/zope.interface-7.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6dd02ec01f4468da0f234da9d9c8545c5412fef80bc590cc51d8dd084138a89", size = 264235 }, - { url = "https://files.pythonhosted.org/packages/fc/ca/57286866285f4b8a4634c12ca1957c24bdac06eae28fd4a3a578e30cf906/zope.interface-7.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e7da17f53e25d1a3bde5da4601e026adc9e8071f9f6f936d0fe3fe84ace6d54", size = 259253 }, - { url = "https://files.pythonhosted.org/packages/96/08/2103587ebc989b455cf05e858e7fbdfeedfc3373358320e9c513428290b1/zope.interface-7.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cab15ff4832580aa440dc9790b8a6128abd0b88b7ee4dd56abacbc52f212209d", size = 264702 }, - { url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466 }, +sdist = { url = "https://files.pythonhosted.org/packages/86/a4/77daa5ba398996d16bb43fc721599d27d03eae68fe3c799de1963c72e228/zope_interface-8.2.tar.gz", hash = "sha256:afb20c371a601d261b4f6edb53c3c418c249db1a9717b0baafc9a9bb39ba1224", size = 254019, upload-time = "2026-01-09T07:51:07.253Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/97/9c2aa8caae79915ed64eb114e18816f178984c917aa9adf2a18345e4f2e5/zope_interface-8.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c65ade7ea85516e428651048489f5e689e695c79188761de8c622594d1e13322", size = 208081, upload-time = "2026-01-09T08:05:06.623Z" }, + { url = "https://files.pythonhosted.org/packages/34/86/4e2fcb01a8f6780ac84923748e450af0805531f47c0956b83065c99ab543/zope_interface-8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1ef4b43659e1348f35f38e7d1a6bbc1682efde239761f335ffc7e31e798b65b", size = 208522, upload-time = "2026-01-09T08:05:07.986Z" }, + { url = "https://files.pythonhosted.org/packages/f6/eb/08e277da32ddcd4014922854096cf6dcb7081fad415892c2da1bedefbf02/zope_interface-8.2-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:dfc4f44e8de2ff4eba20af4f0a3ca42d3c43ab24a08e49ccd8558b7a4185b466", size = 255198, upload-time = "2026-01-09T08:05:09.532Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a1/b32484f3281a5dc83bc713ad61eca52c543735cdf204543172087a074a74/zope_interface-8.2-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8f094bfb49179ec5dc9981cb769af1275702bd64720ef94874d9e34da1390d4c", size = 259970, upload-time = "2026-01-09T08:05:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/bca0e8ae1e487d4093a8a7cfed2118aa2d4758c8cfd66e59d2af09d71f1c/zope_interface-8.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d2bb8e7364e18f083bf6744ccf30433b2a5f236c39c95df8514e3c13007098ce", size = 261153, upload-time = "2026-01-09T08:05:13.402Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/e3ff2a708011e56b10b271b038d4cb650a8ad5b7d24352fe2edf6d6b187a/zope_interface-8.2-cp311-cp311-win_amd64.whl", hash = "sha256:6f4b4dfcfdfaa9177a600bb31cebf711fdb8c8e9ed84f14c61c420c6aa398489", size = 212330, upload-time = "2026-01-09T08:05:15.267Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a0/1e1fabbd2e9c53ef92b69df6d14f4adc94ec25583b1380336905dc37e9a0/zope_interface-8.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:624b6787fc7c3e45fa401984f6add2c736b70a7506518c3b537ffaacc4b29d4c", size = 208785, upload-time = "2026-01-09T08:05:17.348Z" }, + { url = "https://files.pythonhosted.org/packages/c3/2a/88d098a06975c722a192ef1fb7d623d1b57c6a6997cf01a7aabb45ab1970/zope_interface-8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bc9ded9e97a0ed17731d479596ed1071e53b18e6fdb2fc33af1e43f5fd2d3aaa", size = 208976, upload-time = "2026-01-09T08:05:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e8/757398549fdfd2f8c89f32c82ae4d2f0537ae2a5d2f21f4a2f711f5a059f/zope_interface-8.2-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:532367553e4420c80c0fc0cabcc2c74080d495573706f66723edee6eae53361d", size = 259411, upload-time = "2026-01-09T08:05:20.567Z" }, + { url = "https://files.pythonhosted.org/packages/91/af/502601f0395ce84dff622f63cab47488657a04d0065547df42bee3a680ff/zope_interface-8.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2bf9cf275468bafa3c72688aad8cfcbe3d28ee792baf0b228a1b2d93bd1d541a", size = 264859, upload-time = "2026-01-09T08:05:22.234Z" }, + { url = "https://files.pythonhosted.org/packages/89/0c/d2f765b9b4814a368a7c1b0ac23b68823c6789a732112668072fe596945d/zope_interface-8.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0009d2d3c02ea783045d7804da4fd016245e5c5de31a86cebba66dd6914d59a2", size = 264398, upload-time = "2026-01-09T08:05:23.853Z" }, + { url = "https://files.pythonhosted.org/packages/4a/81/2f171fbc4222066957e6b9220c4fb9146792540102c37e6d94e5d14aad97/zope_interface-8.2-cp312-cp312-win_amd64.whl", hash = "sha256:845d14e580220ae4544bd4d7eb800f0b6034fe5585fc2536806e0a26c2ee6640", size = 212444, upload-time = "2026-01-09T08:05:25.148Z" }, ] diff --git a/web/js/main.js b/web/js/main.js index 8d5847a..c65b879 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -1,204 +1,286 @@ -$(function(){ - - - function init_page() { - const categories_file_input = document.getElementById("categories-file"); - categories_file_input.addEventListener("click", clear_file_value, false); - categories_file_input.addEventListener("change", handle_categories_file, false); - const categories_g_sheet_name_input = document.getElementById("categories-g-sheet"); - categories_g_sheet_name_input.addEventListener("input", handle_g_sheet_name, false); - const categories_respondents_tab_name_input = document.getElementById("categories-respondents-tab");//for respondents tab - categories_respondents_tab_name_input.addEventListener("input", handle_respondents_tab_name, false);//for categories tab - const categories_tab_name_input = document.getElementById("categories-tab");//for categories tab - categories_tab_name_input.addEventListener("input", handle_categories_tab_name, false);//for categories tab - const gen_rem_tab_input = document.getElementById("gen-rem-tab");//for generate remaining tab checkbox - gen_rem_tab_input.addEventListener('change', handle_gen_rem_tab, false);// for generate remaining tab checkbox - const number_selections_input = document.getElementById("number-selections");//for categories tab - number_selections_input.addEventListener("input", handle_number_selections, false);//for categories tab - const load_g_sheet_btn = document.getElementById('load-g-sheet-btn'); - load_g_sheet_btn.addEventListener('click', handle_load_g_sheet_btn, false); - const selection_file_input = document.getElementById("selection-file"); - selection_file_input.addEventListener("click", clear_file_value, false); - selection_file_input.addEventListener("change", handle_selection_file, false); - const select_number_people_input = document.getElementById("selection-number"); - select_number_people_input.addEventListener("input", handle_number_people, false); - const run_btn = document.getElementById('run-btn'); - run_btn.addEventListener('click', handle_run_button, false); - const run_test_btn = document.getElementById('run-test-btn'); - run_test_btn.addEventListener('click', handle_run_test_button, false); +$(function () { + + + function init_page() { + // CSV elements + const csv_file_features_input = document.getElementById("csv-file-features"); + csv_file_features_input.addEventListener("click", clear_file_value, false); + csv_file_features_input.addEventListener("change", handle_csv_file_features, false); + const csv_file_people_input = document.getElementById("csv-file-people"); + csv_file_people_input.addEventListener("click", clear_file_value, false); + csv_file_people_input.addEventListener("change", handle_csv_file_people, false); + const csv_run_btn = document.getElementById('csv-run-btn'); + csv_run_btn.addEventListener('click', handle_csv_run_button, false); + const csv_run_test_btn = document.getElementById('csv-run-test-btn'); + csv_run_test_btn.addEventListener('click', handle_csv_run_test_button, false); + const csv_panel_size_input = document.getElementById("csv-panel-size"); + csv_panel_size_input.addEventListener("input", handle_csv_panel_size, false); + + // Google Spreadsheet elements + const g_sheet_name_input = document.getElementById("g-sheet-name"); + g_sheet_name_input.addEventListener("input", handle_g_sheet_name, false); + const g_sheet_respondents_tab_name_input = document.getElementById("g-sheet-respondents-tab");//for respondents tab + g_sheet_respondents_tab_name_input.addEventListener("input", handle_respondents_tab_name, false);//for features/categories tab + const g_sheet_features_tab_name_input = document.getElementById("g-sheet-features-tab");//for features/categories tab + g_sheet_features_tab_name_input.addEventListener("input", handle_features_tab_name, false);//for features/categories tab + const g_sheet_gen_rem_tab_input = document.getElementById("g-sheet-gen-rem-tab");//for generate remaining tab checkbox + g_sheet_gen_rem_tab_input.addEventListener('change', handle_gen_rem_tab, false);// for generate remaining tab checkbox + const load_g_sheet_btn = document.getElementById('load-g-sheet-btn'); + load_g_sheet_btn.addEventListener('click', handle_load_g_sheet_btn, false); + const g_sheet_number_selections_input = document.getElementById("g-sheet-number-selections"); + g_sheet_number_selections_input.addEventListener("input", handle_g_sheet_number_selections, false); + const g_sheet_panel_size_input = document.getElementById("g-sheet-panel-size"); + g_sheet_panel_size_input.addEventListener("input", handle_g_sheet_panel_size, false); + const g_sheet_run_btn = document.getElementById('g-sheet-run-btn'); + g_sheet_run_btn.addEventListener('click', handle_g_sheet_run_button, false); + const g_sheet_run_test_btn = document.getElementById('g-sheet-run-test-btn'); + g_sheet_run_test_btn.addEventListener('click', handle_g_sheet_run_test_button, false); + } + + eel.expose(alert_user); + function alert_user(message, is_error) { + const alerts_div = document.getElementById("user-alerts"); + alerts_div.classList.add("alert"); + if (is_error) { + alerts_div.classList.add("alert-danger"); + } else { + alerts_div.classList.add("alert-info"); } - - eel.expose(alert_user); - function alert_user(message, is_error) { - const alerts_div = document.getElementById("user-alerts"); - alerts_div.classList.add("alert"); - if (is_error) { - alerts_div.classList.add("alert-danger"); - } else { - alerts_div.classList.add("alert-info"); - } - alerts_div.textContent = message; - } - - // this allows repeat upload of a file - // from: https://stackoverflow.com/a/12102992/3189 - function clear_file_value() { - this.value = null; - } - - function handle_categories_file() { - const file_handle = this.files[0]; - const reader = new FileReader(); - reader.onload = categories_file_loaded; - reader.readAsText(file_handle); - } - - function handle_selection_file() { - const file_handle = this.files[0]; - const reader = new FileReader(); - reader.onload = selection_file_loaded; - reader.readAsText(file_handle); - } - - function handle_g_sheet_name() { - eel.update_g_sheet_name(this.value); - } - -//////////////////////////////////////// -//Some functions for Advanced settings// -//////////////////////////////////////// - - function handle_respondents_tab_name() { - eel.update_respondents_tab_name(this.value); - } - - function handle_categories_tab_name() { - eel.update_categories_tab_name(this.value); - } - - function handle_gen_rem_tab() { - if (this.checked == true) { - eel.update_gen_rem_tab(this.value); - } else { - eel.update_gen_rem_tab("off"); - } - } - - function handle_number_selections() { - eel.update_number_selections(this.value); - } -////////////////////////////////////////// -//End of functions for Advanced settings// -////////////////////////////////////////// - - - - function handle_load_g_sheet_btn() { - eel.load_g_sheet(); - } - - eel.expose(enable_load_g_sheet_btn); - function enable_load_g_sheet_btn() { - const load_button = document.getElementById("load-g-sheet-btn"); - load_button.disabled = false; - } - - function categories_file_loaded(e) { - const file_contents = e.target.result; - eel.handle_category_contents(file_contents); - } - - function selection_file_loaded(e) { - const file_contents = e.target.result; - eel.handle_selection_contents(file_contents); + alerts_div.textContent = message; + } + + ////////////////////////////////////////// + // functions for CSV files only - calling from JS to Python + ////////////////////////////////////////// + + // this allows repeat upload of a file + // from: https://stackoverflow.com/a/12102992/3189 + function clear_file_value() { + this.value = null; + } + + function handle_csv_file_features() { + const file_handle = this.files[0]; + const reader = new FileReader(); + reader.onload = csv_file_features_loaded; + reader.readAsText(file_handle); + } + + function handle_csv_file_people() { + const file_handle = this.files[0]; + const reader = new FileReader(); + reader.onload = csv_file_people_loaded; + reader.readAsText(file_handle); + } + + function csv_file_features_loaded(e) { + const file_contents = e.target.result; + eel.handle_csv_file_features_content(file_contents); + } + + function csv_file_people_loaded(e) { + const file_contents = e.target.result; + eel.handle_csv_file_people_content(file_contents); + } + + function enable_download(download_link_id, file_contents, filename) { + let download_link = document.getElementById(download_link_id); + download_link.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(file_contents)); + download_link.setAttribute('download', filename); + download_link.classList.remove("disabled"); + } + + eel.expose(enable_csv_selected_download); + function enable_csv_selected_download(file_contents, filename) { + console.log("in csv_enable_selected_download"); + enable_download("csv-download-selected-btn", file_contents, filename); + } + + eel.expose(enable_csv_remaining_download); + function enable_csv_remaining_download(file_contents, filename) { + console.log("in csv_enable_remaining_download"); + enable_download("csv-download-remaining-btn", file_contents, filename); + } + + function handle_csv_panel_size() { + eel.update_csv_panel_size(this.value); + } + + function handle_csv_run_button() { + eel.csv_run_selection(); + } + + function handle_csv_run_test_button() { + eel.csv_run_test_selection(); + } + + ////////////////////////////////////////// + // functions for CSV files only - calling from Python to JS + ////////////////////////////////////////// + + eel.expose(update_csv_features_output_area); + function update_csv_features_output_area(output_html) { + const output_area = document.getElementById("csv-output-area-features-target-p"); + output_area.innerHTML = output_html; + } + + eel.expose(update_csv_selection_output_area); + function update_csv_selection_output_area(output_html) { + const output_area = document.getElementById("csv-output-area-selection-target-p"); + output_area.innerHTML = output_html; + } + + eel.expose(update_csv_selection_range); + function update_csv_selection_range(min_selection, max_selection) { + const selection_label = document.querySelector("label[for=csv-panel-size]"); + selection_label.textContent = "Step 3: Specify the number of people to select (" + + min_selection + "-" + max_selection + ")"; + const selection_input = document.getElementById("csv-panel-size"); + selection_input.setAttribute("min", min_selection); + selection_input.setAttribute("max", max_selection); + } + + eel.expose(enable_csv_selection_content); + function enable_csv_selection_content() { + var csv_file_people_input = document.getElementById("csv-file-people"); + csv_file_people_input.disabled = false; + } + + eel.expose(set_csv_panel_size); + function set_csv_panel_size(panel_size) { + var csv_panel_size_input = document.getElementById("csv-panel-size"); + csv_panel_size_input.value = panel_size; + } + + eel.expose(enable_csv_run_button); + function enable_csv_run_button() { + var csv_run_btn = document.getElementById('csv-run-btn'); + var csv_run_test_btn = document.getElementById('csv-run-test-btn'); + csv_run_btn.disabled = false; + csv_run_test_btn.disabled = false; + } + + eel.expose(disable_csv_run_button); + function disable_csv_run_button() { + var csv_run_btn = document.getElementById('csv-run-btn'); + var csv_run_test_btn = document.getElementById('csv-run-test-btn'); + csv_run_btn.disabled = true; + csv_run_test_btn.disabled = true; + } + + + ///////////////////////////////////////// + // functions for Google Spreadsheets only - calling from JS to Python + ///////////////////////////////////////// + + function handle_g_sheet_name() { + eel.update_g_sheet_name(this.value); + } + + function handle_respondents_tab_name() { + eel.update_respondents_tab_name(this.value); + } + + function handle_features_tab_name() { + eel.update_features_tab_name(this.value); + } + + function handle_gen_rem_tab() { + if (this.checked == true) { + eel.update_gen_rem_tab(true); + } else { + eel.update_gen_rem_tab(false); } - - eel.expose(update_categories_output_area); - function update_categories_output_area(output_html) { - const output_area = document.getElementById("output-area-categories-target-p"); - output_area.innerHTML = output_html; - } - - eel.expose(update_selection_range); - function update_selection_range(min_selection, max_selection) { - const selection_label = document.querySelector("label[for=selection-number]"); - selection_label.textContent = "Step 3: Specify the number of people to select (" + - min_selection + "-" + max_selection + ")"; - const selection_input = document.getElementById("selection-number"); - selection_input.setAttribute("min", min_selection); - selection_input.setAttribute("max", max_selection); - } - - eel.expose(update_selection_output_area); - function update_selection_output_area(output_html) { - const output_area = document.getElementById("output-area-selection-target-p"); - output_area.innerHTML = output_html; - } - - function handle_number_people() { - eel.update_number_people(this.value); - } - - eel.expose(update_selection_output_messages_area); - function update_selection_output_messages_area(output_html) { - const output_area = document.getElementById("output-area-selection-messages-target-p"); - output_area.innerHTML = output_html; - } - - function handle_run_button() { - eel.run_selection(); - } - - function handle_run_test_button() { - eel.run_test_selection(); - } - - eel.expose(enable_selection_content); - function enable_selection_content() { - const selection_content = document.getElementById("selection-file"); - selection_content.disabled = false; - } - - eel.expose(set_select_number_people); - function set_select_number_people(number_people) { - const select_number_people_input = document.getElementById("selection-number"); - select_number_people_input.value = number_people; - } - - eel.expose(enable_run_button); - function enable_run_button() { - const run_button = document.getElementById("run-btn"); - run_button.disabled = false; - const run_test_button = document.getElementById("run-test-btn"); - run_test_button.disabled = false; - } - - eel.expose(disable_run_button); - function disable_run_button() { - const run_button = document.getElementById("run-btn"); - run_button.disabled = true; - const run_test_button = document.getElementById("run-test-btn"); - run_test_button.disabled = true; - } - - function enable_download(download_link_id, file_contents, filename) { - let download_link = document.getElementById(download_link_id); - download_link.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(file_contents)); - download_link.setAttribute('download', filename); - download_link.classList.remove("disabled"); - } - - eel.expose(enable_selected_download); - function enable_selected_download(file_contents, filename) { - console.log("in enable_selected_download"); - enable_download("download-selected-btn", file_contents, filename); - } - - eel.expose(enable_remaining_download); - function enable_remaining_download(file_contents, filename) { - console.log("in enable_remaining_download"); - enable_download("download-remaining-btn", file_contents, filename); - } - - init_page(); + } + + function handle_load_g_sheet_btn() { + eel.load_g_sheet(); + } + + eel.expose(enable_load_g_sheet_btn); + function enable_load_g_sheet_btn() { + var load_g_sheet_btn = document.getElementById('load-g-sheet-btn'); + load_g_sheet_btn.disabled = false; + } + + function handle_g_sheet_number_selections() { + eel.update_number_selections(this.value); + } + + function handle_g_sheet_panel_size() { + eel.update_g_sheet_panel_size(this.value); + } + + function handle_g_sheet_run_button() { + eel.g_sheet_run_selection(); + } + + function handle_g_sheet_run_test_button() { + eel.g_sheet_run_test_selection(); + } + + ///////////////////////////////////////// + // functions for Google Spreadsheets only - calling from Python to JS + ///////////////////////////////////////// + + eel.expose(update_g_sheet_features_output_area); + function update_g_sheet_features_output_area(output_html) { + const output_area = document.getElementById("g-sheet-output-area-features-target-p"); + output_area.innerHTML = output_html; + } + + eel.expose(update_g_sheet_selection_range); + function update_g_sheet_selection_range(min_selection, max_selection) { + const selection_label = document.querySelector("label[for=g-sheet-panel-size]"); + selection_label.textContent = "Step 3: Specify the number of people to select (" + + min_selection + "-" + max_selection + ")"; + const selection_input = document.getElementById("g-sheet-panel-size"); + selection_input.setAttribute("min", min_selection); + selection_input.setAttribute("max", max_selection); + } + + eel.expose(update_g_sheet_selection_output_area); + function update_g_sheet_selection_output_area(output_html) { + const output_area = document.getElementById("g-sheet-output-area-selection-target-p"); + output_area.innerHTML = output_html; + } + + eel.expose(enable_g_sheet_selection_content); + function enable_g_sheet_selection_content() { + g_sheet_file_people_input.disabled = false; + } + + eel.expose(set_g_sheet_panel_size); + function set_g_sheet_panel_size(panel_size) { + var g_sheet_panel_size_input = document.getElementById("g-sheet-panel-size"); + g_sheet_panel_size_input.value = panel_size; + } + + eel.expose(enable_g_sheet_run_button); + function enable_g_sheet_run_button() { + var g_sheet_run_btn = document.getElementById('g-sheet-run-btn'); + var g_sheet_run_test_btn = document.getElementById('g-sheet-run-test-btn'); + g_sheet_run_btn.disabled = false; + g_sheet_run_test_btn.disabled = false; + } + + eel.expose(disable_g_sheet_run_button); + function disable_g_sheet_run_button() { + var g_sheet_run_btn = document.getElementById('g-sheet-run-btn'); + var g_sheet_run_test_btn = document.getElementById('g-sheet-run-test-btn'); + g_sheet_run_btn.disabled = true; + g_sheet_run_test_btn.disabled = true; + } + + //////////////////////////////////////// + //Some functions for the output area - shared by CSV and GSheets + //////////////////////////////////////// + + eel.expose(update_detailed_log_messages_area); + function update_detailed_log_messages_area(output_html) { + const output_area = document.getElementById("output-area-detailed-log-target-p"); + output_area.innerHTML = output_html; + } + + init_page(); }()); diff --git a/web/main.html b/web/main.html index f80f37c..aa31a07 100644 --- a/web/main.html +++ b/web/main.html @@ -1,208 +1,240 @@ - + - - + + + + - -
-

Sortition Foundation - Stratification & Selection

-

-
-
-
-

- -

-
- -
-
-

-

- - -
-
- - -
-
-

After selection... download output:

- Download Selected - Download Remaining -
-

-
-
-
-

-
-
-
- -
-
-
-
-

-

- - - If you change this, then you must click "Load G-Sheet" afterwards. -
- - - - -
- -
-
-
-

- -

- - - If you change this, then you must click "Load G-Sheet" afterwards. -
-
- - - If you change this, then you must click "Load G-Sheet" afterwards. -
-
- - - If this is checked then the remaining tab will be written to your sheet. If you change the check box, then you must click "Load G-Sheet" afterwards. -
-
- - - If you change this, then you must click "Load G-Sheet" afterwards. -
-

- -
- -
- - -

+ +
+ +

Sortition Foundation - Stratification & Selection

+

+ +
+
+
+
+

+

Option A: CSV file input and output

+ +
+

+
+
+ + +
+
+ + +
+
+

+
+

Category and people information:

+
+

Number of categories: No input yet

+
+
+

Number of people: No input yet

+
+ +

+
+

+
+

Set number of people and run selection

+
+ + +
+
+ + +
+
+

+
+
+

After selection... download output:

+ Download Selected + Download Remaining +
+
+
+
+
+
+
+
+

Option B: Google Sheet input and output

+ +
+

+
+
+ + + If you change this, then you must click "Load G-Sheet" afterwards. +
+
+ +
+
+
+

+ +

+ + + If you change this, then you must click "Load G-Sheet" afterwards. +
+
+ + + If you change this, then you must click "Load G-Sheet" afterwards. +
+
+ + + If this is checked then the remaining tab will be written to your sheet. If you change the check box, then you must click "Load G-Sheet" afterwards. +
+
+ + + If you change this, then you must click "Load G-Sheet" afterwards. +
+

+
+
+
+
+
+ +
+ +
+
+

+
+

Category and people information:

+
+

Number of categories: No input yet

+
+
+

Number of people: No input yet

+
+
+ +

+ +
+

Set number of people and run selection

+
+ + +
+
+ + +
+
+ + + +
+
+
+

+
+

+

+

-
-
-
- - - - -

-
-

Category and people information:

-
-

Number of categories: No input yet

-
-
-

Number of people: No input yet

-
-
- -

- -
-

Set number of people and run selection

-
- - -
-
- - -
-
-

-
- Show/hide detailed log + Show/hide detailed log

Detailed Log

-
-

+
+

@@ -214,8 +246,7 @@

Detailed Log

- - +