diff --git a/poetry.lock b/poetry.lock index 6d6594414..92c9cd4c7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -177,7 +177,7 @@ version = "3.8.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, @@ -732,80 +732,6 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -[[package]] -name = "contourpy" -version = "1.3.1" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab"}, - {file = "contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124"}, - {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1"}, - {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b"}, - {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453"}, - {file = "contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3"}, - {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277"}, - {file = "contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595"}, - {file = "contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697"}, - {file = "contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e"}, - {file = "contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b"}, - {file = "contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc"}, - {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86"}, - {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6"}, - {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85"}, - {file = "contourpy-1.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c"}, - {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291"}, - {file = "contourpy-1.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f"}, - {file = "contourpy-1.3.1-cp311-cp311-win32.whl", hash = "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"}, - {file = "contourpy-1.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9"}, - {file = "contourpy-1.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509"}, - {file = "contourpy-1.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc"}, - {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454"}, - {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80"}, - {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec"}, - {file = "contourpy-1.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9"}, - {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b"}, - {file = "contourpy-1.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d"}, - {file = "contourpy-1.3.1-cp312-cp312-win32.whl", hash = "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e"}, - {file = "contourpy-1.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d"}, - {file = "contourpy-1.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2"}, - {file = "contourpy-1.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5"}, - {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81"}, - {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2"}, - {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7"}, - {file = "contourpy-1.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c"}, - {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3"}, - {file = "contourpy-1.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1"}, - {file = "contourpy-1.3.1-cp313-cp313-win32.whl", hash = "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82"}, - {file = "contourpy-1.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd"}, - {file = "contourpy-1.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30"}, - {file = "contourpy-1.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751"}, - {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342"}, - {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c"}, - {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f"}, - {file = "contourpy-1.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda"}, - {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242"}, - {file = "contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1"}, - {file = "contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1"}, - {file = "contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546"}, - {file = "contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6"}, - {file = "contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750"}, - {file = "contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53"}, - {file = "contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699"}, -] - -[package.dependencies] -numpy = ">=1.23" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] - [[package]] name = "cryptography" version = "43.0.3" @@ -856,22 +782,6 @@ ssh = ["bcrypt (>=3.1.5)"] test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] test-randomorder = ["pytest-randomly"] -[[package]] -name = "cycler" -version = "0.12.1" -description = "Composable style cycles" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, - {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, -] - -[package.extras] -docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] -tests = ["pytest", "pytest-cov", "pytest-xdist"] - [[package]] name = "dataclasses-json" version = "0.6.7" @@ -967,7 +877,7 @@ version = "5.1.5" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "Django-5.1.5-py3-none-any.whl", hash = "sha256:c46eb936111fffe6ec4bc9930035524a8be98ec2f74d8a0ff351226a3e52f459"}, {file = "Django-5.1.5.tar.gz", hash = "sha256:19bbca786df50b9eca23cee79d495facf55c8f5c54c529d9bf1fe7b5ea086af3"}, @@ -1040,7 +950,7 @@ version = "4.4.6" description = "A configurable set of panels that display various debug information about the current request/response." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "django_debug_toolbar-4.4.6-py3-none-any.whl", hash = "sha256:3beb671c9ec44ffb817fad2780667f172bd1c067dbcabad6268ce39a81335f45"}, {file = "django_debug_toolbar-4.4.6.tar.gz", hash = "sha256:36e421cb908c2f0675e07f9f41e3d1d8618dc386392ec82d23bcfcd5d29c7044"}, @@ -1443,80 +1353,6 @@ files = [ {file = "filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb"}, ] -[[package]] -name = "fonttools" -version = "4.55.7" -description = "Tools to manipulate font files" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "fonttools-4.55.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c2680a3e6e2e2d104a7ea81fb89323e1a9122c23b03d6569d0768887d0d76e69"}, - {file = "fonttools-4.55.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a7831d16c95b60866772a15fdcc03772625c4bb6d858e0ad8ef3d6e48709b2ef"}, - {file = "fonttools-4.55.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:833927d089e6585019f2c85e3f8f7d87733e3fe81cd704ebaca7afa27e2e7113"}, - {file = "fonttools-4.55.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7858dc6823296a053d85b831fa8428781c6c6f06fca44582bf7b6b2ff32a9089"}, - {file = "fonttools-4.55.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05568a66b090ed9d79aefdce2ceb180bb64fc856961deaedc29f5ad51355ce2c"}, - {file = "fonttools-4.55.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2dbc08e227fbeb716776905a7bd3c4fc62c8e37c8ef7d481acd10cb5fde12222"}, - {file = "fonttools-4.55.7-cp310-cp310-win32.whl", hash = "sha256:6eb93cbba484a463b5ee83f7dd3211905f27a3871d20d90fb72de84c6c5056e3"}, - {file = "fonttools-4.55.7-cp310-cp310-win_amd64.whl", hash = "sha256:7ff8e606f905048dc91a55a06d994b68065bf35752ae199df54a9bf30013dcaa"}, - {file = "fonttools-4.55.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:916e1d926823b4b3b3815c59fc79f4ed670696fdd5fd9a5e690a0503eef38f79"}, - {file = "fonttools-4.55.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b89da448e0073408d7b2c44935f9fdae4fdc93644899f99f6102ef883ecf083c"}, - {file = "fonttools-4.55.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087ace2d06894ccdb03e6975d05da6bb9cec0c689b2a9983c059880e33a1464a"}, - {file = "fonttools-4.55.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:775ed0700ee6f781436641f18a0c61b1846a8c1aecae6da6b395c4417e2cb567"}, - {file = "fonttools-4.55.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9ec71d0cc0242899f87e4c230ed0b22c7b8681f288fb80e3d81c2c54c5bd2c79"}, - {file = "fonttools-4.55.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d4b1c5939c0521525f45522823508e6fad21175bca978583688ea3b3736e6625"}, - {file = "fonttools-4.55.7-cp311-cp311-win32.whl", hash = "sha256:23df0f1003abaf8a435543f59583fc247e7ae1b047ee2263510e0654a5f207e0"}, - {file = "fonttools-4.55.7-cp311-cp311-win_amd64.whl", hash = "sha256:82163d58b43eff6e2025a25c32905fdb9042a163cc1ff82dab393e7ffc77a7d5"}, - {file = "fonttools-4.55.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:12e81d44f762156d28b5c93a6b65d98ed73678be45b22546de8ed29736c3cb96"}, - {file = "fonttools-4.55.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c26445a7be689f8b70df7d5d2e2c85ec4407bdb769902a23dd45ac44f767575d"}, - {file = "fonttools-4.55.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2cbafedb9462be7cf68c66b6ca1d8309842fe36b729f1b1969595f5d660e5c2"}, - {file = "fonttools-4.55.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4bde87985012adbd7559bc363d802fb335e92a07ff86a76cf02bebb0b8566d1"}, - {file = "fonttools-4.55.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:69ed0660750993150f7c4d966c0c1ffaa0385f23ccef85c2ff108062d80dd7ea"}, - {file = "fonttools-4.55.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3098355e7a7b5ac48d5dc29684a65271187b865b85675033958b57c40364ee34"}, - {file = "fonttools-4.55.7-cp312-cp312-win32.whl", hash = "sha256:ee7aa8bb716318e3d835ef473978e22b7a39c0f1b3b08cc0b0ee1bba6f73bc1e"}, - {file = "fonttools-4.55.7-cp312-cp312-win_amd64.whl", hash = "sha256:e696d6e2baf4cc57ded34bb87e5d3a9e4da9732f3d9e8e2c6db0746e57a6dc0b"}, - {file = "fonttools-4.55.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e10c7fb80cdfdc32244514cbea0906e9f53e3cc80d64d3389da09502fd999b55"}, - {file = "fonttools-4.55.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1101976c703ff4008a928fc3fef42caf06d035bfc4614230d7e797cbe356feb0"}, - {file = "fonttools-4.55.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e6dffe9cbcd163ef617fab1f81682e4d1629b7a5b9c5e598274dc2d03e88bcd"}, - {file = "fonttools-4.55.7-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77e5115a425d53be6e31cd0fe9210f62a488bccf81eb113ab5dd7f4fa88e4d81"}, - {file = "fonttools-4.55.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f0c45eae32d090763820756b18322a70571dada3f1cbe003debc37a9c35bc260"}, - {file = "fonttools-4.55.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4ebc475d43f3de2b26e0cf551eff92c24e22d1aee03dc1b33adb52fc2e6cb2"}, - {file = "fonttools-4.55.7-cp313-cp313-win32.whl", hash = "sha256:371197de1283cc99f5f10eb91496520eb0e2d079312d014fd6cef9e802174c6a"}, - {file = "fonttools-4.55.7-cp313-cp313-win_amd64.whl", hash = "sha256:418ece624fbc04e199f58398ffef3eaad645baba65434871b09eb7350a3a346b"}, - {file = "fonttools-4.55.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3976db357484bf4cb533dfd0d1a444b38ad06062458715ebf21e38c71aff325d"}, - {file = "fonttools-4.55.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:30c3501328363b73a90acc8a722dd199c993f2c4369ea16886128d94e91897ec"}, - {file = "fonttools-4.55.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0899cd23967950e7b902ea75af06cfe5f59ac71eb38e98a774c9e596790e6aa"}, - {file = "fonttools-4.55.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f669910b64d27750398f6c56c651367d4954b05c86ff067af1c9949e109cf1e2"}, - {file = "fonttools-4.55.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:1d4be8354c245c00aecfc90f5d3da8606226f0ac22e1cb0837b39139e4c2df85"}, - {file = "fonttools-4.55.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9074a2848ea5b607377e16998dfcf90cf5eb614d0c388541b9782d5cc038e149"}, - {file = "fonttools-4.55.7-cp38-cp38-win32.whl", hash = "sha256:5ff0daf8b2e0612e5761fed2e4a2f54eff9d9ec0aeb4091c9f3666f9a118325e"}, - {file = "fonttools-4.55.7-cp38-cp38-win_amd64.whl", hash = "sha256:0ed25d7b5fa4ae6a805c2a9cc0e5307d45cbb3b8e155584fe932d0f3b6a997bf"}, - {file = "fonttools-4.55.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ef5ee98fc320c158e4e459a5ee40d1ac3728d4ce11c3c8dfd854aa0aa5c042f"}, - {file = "fonttools-4.55.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09740feed51f9ed816aebf5d82071b7fecf693ac3a7e0fc8ea433f5dc3bd92f5"}, - {file = "fonttools-4.55.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3d19ea483b3cd8833e9e2ee8115f3d2044d55d3743d84f9c23b48b52d7516d8"}, - {file = "fonttools-4.55.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c135c91d47351b84893fb6fcbb8f178eba14f7cb195850264c0675c85e4238b6"}, - {file = "fonttools-4.55.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bee4920ebeb540849bc3555d871e2a8487e39ce8263c281f74d5b6d44d2bf1df"}, - {file = "fonttools-4.55.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f3b63648600dd0081bdd6856a86d014a7f1d2d11c3c974542f866478d832e103"}, - {file = "fonttools-4.55.7-cp39-cp39-win32.whl", hash = "sha256:d4bd27f0fa5120aaa39f76de5768959bc97300e0f59a3160d466b51436a38aea"}, - {file = "fonttools-4.55.7-cp39-cp39-win_amd64.whl", hash = "sha256:c665df9c9d99937a5bf807bace1c0c95bd13f55de8c82aaf9856b868dcbfe5d9"}, - {file = "fonttools-4.55.7-py3-none-any.whl", hash = "sha256:3304dfcf9ca204dd0ef691a287bd851ddd8e8250108658c0677c3fdfec853a20"}, - {file = "fonttools-4.55.7.tar.gz", hash = "sha256:6899e3d97225a8218f525e9754da0376e1c62953a0d57a76c5abaada51e0d140"}, -] - -[package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "pycairo", "scipy"] -lxml = ["lxml (>=4.0)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -ufo = ["fs (>=2.2.0,<3)"] -unicode = ["unicodedata2 (>=15.1.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] - [[package]] name = "frozenlist" version = "1.5.0" @@ -1985,96 +1821,6 @@ files = [ {file = "jsonpath_python-1.0.6-py3-none-any.whl", hash = "sha256:1e3b78df579f5efc23565293612decee04214609208a2335884b3ee3f786b575"}, ] -[[package]] -name = "kiwisolver" -version = "1.4.8" -description = "A fast implementation of the Cassowary constraint solver" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db"}, - {file = "kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b"}, - {file = "kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed"}, - {file = "kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605"}, - {file = "kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e"}, - {file = "kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751"}, - {file = "kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271"}, - {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84"}, - {file = "kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561"}, - {file = "kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6"}, - {file = "kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc"}, - {file = "kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67"}, - {file = "kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34"}, - {file = "kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2"}, - {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502"}, - {file = "kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31"}, - {file = "kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a"}, - {file = "kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d"}, - {file = "kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8"}, - {file = "kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50"}, - {file = "kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476"}, - {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09"}, - {file = "kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1"}, - {file = "kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc"}, - {file = "kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957"}, - {file = "kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb"}, - {file = "kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2"}, - {file = "kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90"}, - {file = "kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b"}, - {file = "kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b"}, - {file = "kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e"}, -] - [[package]] name = "langdetect" version = "1.0.9" @@ -2389,64 +2135,6 @@ dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] docs = ["autodocsumm (==0.2.14)", "furo (==2024.8.6)", "sphinx (==8.1.3)", "sphinx-copybutton (==0.5.2)", "sphinx-issues (==5.0.0)", "sphinxext-opengraph (==0.9.1)"] tests = ["pytest", "simplejson"] -[[package]] -name = "matplotlib" -version = "3.10.0" -description = "Python plotting package" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "matplotlib-3.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2c5829a5a1dd5a71f0e31e6e8bb449bc0ee9dbfb05ad28fc0c6b55101b3a4be6"}, - {file = "matplotlib-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2a43cbefe22d653ab34bb55d42384ed30f611bcbdea1f8d7f431011a2e1c62e"}, - {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:607b16c8a73943df110f99ee2e940b8a1cbf9714b65307c040d422558397dac5"}, - {file = "matplotlib-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01d2b19f13aeec2e759414d3bfe19ddfb16b13a1250add08d46d5ff6f9be83c6"}, - {file = "matplotlib-3.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e6c6461e1fc63df30bf6f80f0b93f5b6784299f721bc28530477acd51bfc3d1"}, - {file = "matplotlib-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:994c07b9d9fe8d25951e3202a68c17900679274dadfc1248738dcfa1bd40d7f3"}, - {file = "matplotlib-3.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:fd44fc75522f58612ec4a33958a7e5552562b7705b42ef1b4f8c0818e304a363"}, - {file = "matplotlib-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c58a9622d5dbeb668f407f35f4e6bfac34bb9ecdcc81680c04d0258169747997"}, - {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:845d96568ec873be63f25fa80e9e7fae4be854a66a7e2f0c8ccc99e94a8bd4ef"}, - {file = "matplotlib-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5439f4c5a3e2e8eab18e2f8c3ef929772fd5641876db71f08127eed95ab64683"}, - {file = "matplotlib-3.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4673ff67a36152c48ddeaf1135e74ce0d4bce1bbf836ae40ed39c29edf7e2765"}, - {file = "matplotlib-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:7e8632baebb058555ac0cde75db885c61f1212e47723d63921879806b40bec6a"}, - {file = "matplotlib-3.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4659665bc7c9b58f8c00317c3c2a299f7f258eeae5a5d56b4c64226fca2f7c59"}, - {file = "matplotlib-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d44cb942af1693cced2604c33a9abcef6205601c445f6d0dc531d813af8a2f5a"}, - {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a994f29e968ca002b50982b27168addfd65f0105610b6be7fa515ca4b5307c95"}, - {file = "matplotlib-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b0558bae37f154fffda54d779a592bc97ca8b4701f1c710055b609a3bac44c8"}, - {file = "matplotlib-3.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:503feb23bd8c8acc75541548a1d709c059b7184cde26314896e10a9f14df5f12"}, - {file = "matplotlib-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:c40ba2eb08b3f5de88152c2333c58cee7edcead0a2a0d60fcafa116b17117adc"}, - {file = "matplotlib-3.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96f2886f5c1e466f21cc41b70c5a0cd47bfa0015eb2d5793c88ebce658600e25"}, - {file = "matplotlib-3.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12eaf48463b472c3c0f8dbacdbf906e573013df81a0ab82f0616ea4b11281908"}, - {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fbbabc82fde51391c4da5006f965e36d86d95f6ee83fb594b279564a4c5d0d2"}, - {file = "matplotlib-3.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad2e15300530c1a94c63cfa546e3b7864bd18ea2901317bae8bbf06a5ade6dcf"}, - {file = "matplotlib-3.10.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3547d153d70233a8496859097ef0312212e2689cdf8d7ed764441c77604095ae"}, - {file = "matplotlib-3.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c55b20591ced744aa04e8c3e4b7543ea4d650b6c3c4b208c08a05b4010e8b442"}, - {file = "matplotlib-3.10.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9ade1003376731a971e398cc4ef38bb83ee8caf0aee46ac6daa4b0506db1fd06"}, - {file = "matplotlib-3.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95b710fea129c76d30be72c3b38f330269363fbc6e570a5dd43580487380b5ff"}, - {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cdbaf909887373c3e094b0318d7ff230b2ad9dcb64da7ade654182872ab2593"}, - {file = "matplotlib-3.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d907fddb39f923d011875452ff1eca29a9e7f21722b873e90db32e5d8ddff12e"}, - {file = "matplotlib-3.10.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3b427392354d10975c1d0f4ee18aa5844640b512d5311ef32efd4dd7db106ede"}, - {file = "matplotlib-3.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5fd41b0ec7ee45cd960a8e71aea7c946a28a0b8a4dcee47d2856b2af051f334c"}, - {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:81713dd0d103b379de4516b861d964b1d789a144103277769238c732229d7f03"}, - {file = "matplotlib-3.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:359f87baedb1f836ce307f0e850d12bb5f1936f70d035561f90d41d305fdacea"}, - {file = "matplotlib-3.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae80dc3a4add4665cf2faa90138384a7ffe2a4e37c58d83e115b54287c4f06ef"}, - {file = "matplotlib-3.10.0.tar.gz", hash = "sha256:b886d02a581b96704c9d1ffe55709e49b4d2d52709ccebc4be42db856e511278"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -kiwisolver = ">=1.3.1" -numpy = ">=1.23" -packaging = ">=20.0" -pillow = ">=8" -pyparsing = ">=2.3.1" -python-dateutil = ">=2.7" - -[package.extras] -dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] - [[package]] name = "msgpack" version = "1.1.0" @@ -3400,21 +3088,6 @@ files = [ {file = "pylibmc-1.6.3.tar.gz", hash = "sha256:eefa46115537abad65fbe2e032acd1b3463d9bf9e335af4b0916df4e4d3206e0"}, ] -[[package]] -name = "pyparsing" -version = "3.2.1" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, - {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pypdf" version = "5.2.0" @@ -4150,7 +3823,7 @@ version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -4324,7 +3997,7 @@ version = "2025.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main"] +groups = ["main", "dev"] markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, @@ -4892,4 +4565,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = "3.11.2" -content-hash = "83360143704fb0869fb646b585bb67739fc657465c08ac1541860d882b048489" +content-hash = "6dd34aa98eb1999ced9cd0f87ba84377480d01e45bb8f9cdb7e19f7dfb600c64" diff --git a/pyproject.toml b/pyproject.toml index be9f1d0fa..fbcff8e00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ django-annoying = "^0.10.7" dj-rest-auth = "^5.0.2" user-agents = "^2.2.0" whitenoise = "^6.8.2" -django-debug-toolbar = "^4.4.6" + pylibmc = "^1.6.1" psycopg2-binary = "^2.9.10" protobuf = "^4.25.3" @@ -54,7 +54,6 @@ faiss-cpu = "^1.8.0" psutil = "^5.9.8" python-bitcoinrpc = "^1.0" sendgrid = "^6.11.0" -matplotlib = "^3.10.0" atproto = "^0.0.55" django-redis = "^5.4.0" uvicorn = "^0.34.0" @@ -73,6 +72,7 @@ pre-commit = "^3.8.0" selenium = "^4.28.1" webdriver-manager = "^4.0.2" chromedriver-autoinstaller = "^0.6.4" +django-debug-toolbar = "^4.4.6" [tool.isort] known_first_party = ["blt"] diff --git a/website/tests/__init__.py b/website/tests/__init__.py new file mode 100644 index 000000000..3ccc7a98a --- /dev/null +++ b/website/tests/__init__.py @@ -0,0 +1 @@ +# This file makes the tests directory a Python package diff --git a/website/tests/test_badge_views.py b/website/tests/test_badge_views.py new file mode 100644 index 000000000..203202ba3 --- /dev/null +++ b/website/tests/test_badge_views.py @@ -0,0 +1,155 @@ +import io +from unittest.mock import patch + +from django.test import Client, TestCase +from django.urls import reverse +from django.utils import timezone +from django.utils.timezone import now +from PIL import Image + +from website.models import IP, Project, Repo + + +class BadgeViewsTest(TestCase): + def setUp(self): + self.client = Client() + # Create a test project + self.project = Project.objects.create(name="Test Project", slug="test-project", project_visit_count=42) + + # Create a test repo + self.repo = Repo.objects.create( + name="Test Repo", + slug="test-repo", + repo_visit_count=42, + project=self.project, + repo_url="https://github.com/test/test", + ) + + # Create IP records for last 7 days + today = now().date() + for i in range(7): + date = today - timezone.timedelta(days=i) + badge_path = reverse("project-badge", kwargs={"slug": self.project.slug}) + IP.objects.create(address=f"192.168.1.{i}", path=badge_path, created=date, count=1) + + def test_project_badge_view(self): + """Test project badge view returns correct chart with visit count""" + url = reverse("project-badge", kwargs={"slug": self.project.slug}) + response = self.client.get(url) + + # Check response basics + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "image/png") + cache_control = "no-store, no-cache, must-revalidate, max-age=0" + self.assertEqual(response["Cache-Control"], cache_control) + self.assertEqual(response["Pragma"], "no-cache") + self.assertEqual(response["Expires"], "0") + + # Verify it's a valid PNG image + image_data = response.content + self.assertTrue(image_data.startswith(b"\x89PNG")) # PNG magic number + + # Verify we can load it as a PIL image + buf = io.BytesIO(image_data) + try: + Image.open(buf) + is_valid_image = True + except Exception: + is_valid_image = False + self.assertTrue(is_valid_image) + + # Check visit count increment + self.project.refresh_from_db() + # Should increment by 1 + self.assertEqual(self.project.project_visit_count, 43) + + def test_repo_badge_view(self): + """Test that repo badge view returns correct chart with visit count""" + url = reverse("repo-badge", kwargs={"slug": self.repo.slug}) + response = self.client.get(url) + + # Check response basics + self.assertEqual(response.status_code, 200) + self.assertEqual(response["Content-Type"], "image/png") + cache_control = "no-store, no-cache, must-revalidate, max-age=0" + self.assertEqual(response["Cache-Control"], cache_control) + self.assertEqual(response["Pragma"], "no-cache") + self.assertEqual(response["Expires"], "0") + + # Verify it's a valid PNG image + image_data = response.content + self.assertTrue(image_data.startswith(b"\x89PNG")) # PNG magic number + + # Verify we can load it as a PIL image + buf = io.BytesIO(image_data) + try: + Image.open(buf) + is_valid_image = True + except Exception: + is_valid_image = False + self.assertTrue(is_valid_image) + + # Check visit count increment + self.repo.refresh_from_db() + # Should increment by 1 + self.assertEqual(self.repo.repo_visit_count, 43) + + def test_badge_view_404(self): + """Test that badge views return 404 for non-existent slugs""" + # Test project badge + url = reverse("project-badge", kwargs={"slug": "non-existent"}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + # Test repo badge + url = reverse("repo-badge", kwargs={"slug": "non-existent"}) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + def test_badge_view_visit_tracking(self): + """Test that badge views properly track unique visits""" + # First visit + url = reverse("project-badge", kwargs={"slug": self.project.slug}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + # Check IP record was created + ip_record = IP.objects.filter(path=url).first() + self.assertIsNotNone(ip_record) + self.assertEqual(ip_record.count, 1) + + # Second visit on same day shouldn't increment + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + ip_record.refresh_from_db() + self.assertEqual(ip_record.count, 1) # Count should stay the same + + # Simulate next day visit + with patch("django.utils.timezone.now") as mock_now: + mock_now.return_value = now() + timezone.timedelta(days=1) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + # Should create new record for new day + self.assertEqual( + IP.objects.filter(path=url).count(), + 9, # 7 from setup + 2 from test + ) + + def test_badge_view_historical_data(self): + """Test that badge view shows historical visit data correctly""" + url = reverse("project-badge", kwargs={"slug": self.project.slug}) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + # Verify response is a PNG image + self.assertEqual(response["Content-Type"], "image/png") + image_data = response.content + self.assertTrue(image_data.startswith(b"\x89PNG")) + + # Check we have the expected number of historical records + seven_days_ago = now().date() - timezone.timedelta(days=7) + visit_count = IP.objects.filter(path=url, created__date__gte=seven_days_ago).count() + # 7 from setup + 1 from this test + self.assertEqual(visit_count, 7) diff --git a/website/views/project.py b/website/views/project.py index bb2138ae5..f0c02a8a9 100644 --- a/website/views/project.py +++ b/website/views/project.py @@ -4,32 +4,32 @@ import socket import time from calendar import monthrange -from datetime import datetime +from datetime import datetime, timedelta from io import BytesIO from pathlib import Path from urllib.parse import urlparse import django_filters -import matplotlib.pyplot as plt import requests +from dateutil.parser import parse as parse_datetime from dateutil.relativedelta import relativedelta from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required, user_passes_test -from django.contrib.humanize.templatetags.humanize import naturaltime from django.core.exceptions import ValidationError from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.core.validators import URLValidator -from django.db.models import F, Q, Sum +from django.db.models import Count, F, Q, Sum +from django.db.models.functions import TruncDate from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone -from django.utils.dateparse import parse_datetime from django.utils.text import slugify -from django.utils.timezone import localtime, now +from django.utils.timezone import now from django.views.decorators.http import require_http_methods from django.views.generic import DetailView from django_filters.views import FilterView +from PIL import Image, ImageDraw, ImageFont from rest_framework.views import APIView from website.bitcoin_utils import create_bacon_token @@ -106,67 +106,107 @@ def get(self, request, slug): project = get_object_or_404(Project, slug=slug) # Get today's date - today = now().date() + current_time = now() + today = current_time.date() # Get the real client IP user_ip = self.get_client_ip(request) - # Continue with existing code but use the new user_ip - visited_data = IP.objects.filter(address=user_ip, path=request.path, created__date=today).last() + # Check if we have a record for today + visited_data = IP.objects.filter(address=user_ip, path=request.path, created__date=today).first() if visited_data: - # If the creation date is today - if visited_data.created.date() == today: - # If the visit count is 1, update the project visit count - if visited_data.count == 1: - project.project_visit_count = F("project_visit_count") + 1 - project.save() - else: - # If the creation date is not today, reset the creation date and count - visited_data.created = now() - visited_data.count = 1 - visited_data.save() - - # Increment the project visit count + # If we have a record for today, only update project visit count if needed + if visited_data.count == 1: project.project_visit_count = F("project_visit_count") + 1 project.save() else: - # If no record exists, create a new one - IP.objects.create(address=user_ip, path=request.path, created=now(), count=1) + # If no record exists for today, create a new one + IP.objects.create(address=user_ip, path=request.path, created=current_time, count=1) # Increment the project's visit count project.project_visit_count = F("project_visit_count") + 1 project.save() + # Get unique visits, grouped by date (last 7 days) + seven_days_ago = today - timedelta(days=7) + visit_counts = ( + IP.objects.filter(path=request.path, created__date__gte=seven_days_ago) + .annotate(date=TruncDate("created")) + .values("date") + .annotate(visit_count=Count("address")) + .order_by("date") + ) + # Refresh project to get the latest visit count project.refresh_from_db() + # Extract dates and counts + dates = [entry["date"] for entry in visit_counts] + counts = [entry["visit_count"] for entry in visit_counts] total_views = project.project_visit_count - fig = plt.figure(figsize=(4, 1)) - plt.bar(0, total_views, color="red", width=0.5) - - plt.title( - f"{total_views}", - loc="left", - x=-0.36, - y=0.3, - fontsize=15, - fontweight="bold", - color="red", - ) + # Create a new image with a white background + width = 600 + height = 200 + img = Image.new("RGB", (width, height), color="white") + draw = ImageDraw.Draw(img) + + # Define colors + bar_color = "#e05d44" + text_color = "#333333" + grid_color = "#eeeeee" + + # Calculate chart dimensions + margin = 40 + chart_width = width - 2 * margin + chart_height = height - 2 * margin + + if counts: + max_count = max(counts) + bar_width = chart_width / (len(counts) * 2) # Leave space between bars + else: + max_count = 1 + bar_width = chart_width / 14 # Default for empty data + + # Draw grid lines + for i in range(5): + y = margin + (chart_height * i) // 4 + draw.line([(margin, y), (width - margin, y)], fill=grid_color) + + # Draw bars + if dates and counts: + for i, count in enumerate(counts): + bar_height = (count / max_count) * chart_height + x1 = margin + (i * 2 * bar_width) + y1 = height - margin - bar_height + x2 = x1 + bar_width + y2 = height - margin + + # Draw bar with a slight gradient effect + for h in range(int(y1), int(y2)): + alpha = int(255 * (1 - (h - y1) / bar_height * 0.2)) + r, g, b = 224, 93, 68 # RGB values for #e05d44 + current_color = f"#{r:02x}{g:02x}{b:02x}" + draw.line([(x1, h), (x2, h)], fill=current_color) + + # Draw total views text + try: + font = ImageFont.truetype("DejaVuSans.ttf", 32) + except OSError: + font = ImageFont.load_default() - plt.gca().set_xticks([]) # Remove x-axis ticks - plt.gca().set_yticks([]) - plt.box(False) + text = f"Total Views: {total_views}" + text_bbox = draw.textbbox((0, 0), text, font=font) + text_width = text_bbox[2] - text_bbox[0] + draw.text(((width - text_width) // 2, margin // 2), text, font=font, fill=bar_color) - # Save the plot to an in-memory file + # Save the image to a buffer buffer = BytesIO() - plt.savefig(buffer, format="png", bbox_inches="tight") - plt.close() + img.save(buffer, format="PNG", quality=95) buffer.seek(0) - # Prepare the HTTP response with the bar graph image + # Return the image with appropriate headers response = HttpResponse(buffer, content_type="image/png") response["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" response["Pragma"] = "no-cache" @@ -844,11 +884,20 @@ def get_context_data(self, **kwargs): # Determine impact level based on score if impact_score > 200: - impact_level = {"class": "bg-green-100 text-green-800", "text": "High Impact"} + impact_level = { + "class": "bg-green-100 text-green-800", + "text": "High Impact", + } elif impact_score > 100: - impact_level = {"class": "bg-yellow-100 text-yellow-800", "text": "Medium Impact"} + impact_level = { + "class": "bg-yellow-100 text-yellow-800", + "text": "Medium Impact", + } else: - impact_level = {"class": "bg-blue-100 text-blue-800", "text": "Growing Impact"} + impact_level = { + "class": "bg-blue-100 text-blue-800", + "text": "Growing Impact", + } processed_stats.append( { @@ -924,12 +973,18 @@ def get_issue_count(full_name, query, headers): # Get GitHub API token github_token = getattr(settings, "GITHUB_TOKEN", None) if not github_token: - return JsonResponse({"status": "error", "message": "GitHub token not configured"}, status=500) + return JsonResponse( + {"status": "error", "message": "GitHub token not configured"}, + status=500, + ) # Extract owner/repo from GitHub URL match = re.match(r"https://github.com/([^/]+)/([^/]+)/?", repo.repo_url) if not match: - return JsonResponse({"status": "error", "message": "Invalid repository URL"}, status=400) + return JsonResponse( + {"status": "error", "message": "Invalid repository URL"}, + status=400, + ) owner, repo_name = match.groups() api_url = f"https://api.github.com/repos/{owner}/{repo_name}" @@ -972,7 +1027,10 @@ def get_issue_count(full_name, query, headers): ) else: return JsonResponse( - {"status": "error", "message": f"GitHub API error: {response.status_code}"}, + { + "status": "error", + "message": f"GitHub API error: {response.status_code}", + }, status=response.status_code, ) @@ -994,7 +1052,10 @@ def get_issue_count(full_name, query, headers): ) except ValueError as e: return JsonResponse( - {"status": "error", "message": "There was an error processing your data."}, + { + "status": "error", + "message": "There was an error processing your data.", + }, status=400, ) @@ -1002,11 +1063,17 @@ def get_issue_count(full_name, query, headers): try: github_token = getattr(settings, "GITHUB_TOKEN", None) if not github_token: - return JsonResponse({"status": "error", "message": "GitHub token not configured"}, status=500) + return JsonResponse( + {"status": "error", "message": "GitHub token not configured"}, + status=500, + ) match = re.match(r"https://github.com/([^/]+)/([^/]+)/?", repo.repo_url) if not match: - return JsonResponse({"status": "error", "message": "Invalid repository URL"}, status=400) + return JsonResponse( + {"status": "error", "message": "Invalid repository URL"}, + status=400, + ) # Extract owner and repo from API call owner, repo_name = match.groups() @@ -1019,7 +1086,10 @@ def get_issue_count(full_name, query, headers): if response.status_code != 200: return JsonResponse( - {"status": "error", "message": "Failed to fetch repository data"}, + { + "status": "error", + "message": "Failed to fetch repository data", + }, status=500, ) @@ -1028,7 +1098,10 @@ def get_issue_count(full_name, query, headers): default_branch = repo_data.get("default_branch") if not full_name: return JsonResponse( - {"status": "error", "message": "Could not get repository full name"}, + { + "status": "error", + "message": "Could not get repository full name", + }, status=500, ) @@ -1092,9 +1165,9 @@ def get_issue_count(full_name, query, headers): "total_issues": repo.total_issues, "open_pull_requests": repo.open_pull_requests, "commit_count": repo.commit_count, - "last_commit_date": repo.last_commit_date.strftime("%b %d, %Y") - if repo.last_commit_date - else "", + "last_commit_date": ( + repo.last_commit_date.strftime("%b %d, %Y") if repo.last_commit_date else "" + ), }, } ) @@ -1117,7 +1190,10 @@ def get_issue_count(full_name, query, headers): ) except ValueError as e: return JsonResponse( - {"status": "error", "message": "There was an error processing your data."}, + { + "status": "error", + "message": "There was an error processing your data.", + }, status=400, ) @@ -1125,11 +1201,17 @@ def get_issue_count(full_name, query, headers): try: github_token = getattr(settings, "GITHUB_TOKEN", None) if not github_token: - return JsonResponse({"status": "error", "message": "GitHub token not configured"}, status=500) + return JsonResponse( + {"status": "error", "message": "GitHub token not configured"}, + status=500, + ) match = re.match(r"https://github.com/([^/]+)/([^/]+)/?", repo.repo_url) if not match: - return JsonResponse({"status": "error", "message": "Invalid repository URL"}, status=400) + return JsonResponse( + {"status": "error", "message": "Invalid repository URL"}, + status=400, + ) owner, repo_name = match.groups() api_url = f"https://api.github.com/repos/{owner}/{repo_name}" @@ -1141,7 +1223,10 @@ def get_issue_count(full_name, query, headers): response = requests.get(api_url, headers=headers) if response.status_code != 200: return JsonResponse( - {"status": "error", "message": "Failed to fetch repository data"}, + { + "status": "error", + "message": "Failed to fetch repository data", + }, status=500, ) @@ -1171,12 +1256,16 @@ def get_issue_count(full_name, query, headers): "size": repo.size, "license": repo.license or "Not specified", "release_name": repo.release_name or "Not available", - "release_date": repo.release_datetime.strftime("%b %d, %Y") - if repo.release_datetime - else "Not available", - "last_commit_date": repo.last_commit_date.strftime("%b %d, %Y") - if repo.last_commit_date - else "Not available", + "release_date": ( + repo.release_datetime.strftime("%b %d, %Y") + if repo.release_datetime + else "Not available" + ), + "last_commit_date": ( + repo.last_commit_date.strftime("%b %d, %Y") + if repo.last_commit_date + else "Not available" + ), }, } ) @@ -1190,7 +1279,10 @@ def get_issue_count(full_name, query, headers): status=503, ) except Exception as e: - return JsonResponse({"status": "error", "message": "An unexpected error occurred."}, status=500) + return JsonResponse( + {"status": "error", "message": "An unexpected error occurred."}, + status=500, + ) elif section == "community": try: @@ -1232,7 +1324,10 @@ def get_issue_count(full_name, query, headers): except ValueError as e: return JsonResponse( - {"status": "error", "message": "There was an error processing your data."}, + { + "status": "error", + "message": "There was an error processing your data.", + }, status=400, ) @@ -1286,67 +1381,102 @@ def get(self, request, slug): repo = get_object_or_404(Repo, slug=slug) # Get today's date - today = now().date() + current_time = now() + today = current_time.date() # Get the real client IP user_ip = self.get_client_ip(request) - # Continue with existing code but use the new user_ip - visited_data = IP.objects.filter(address=user_ip, path=request.path, created__date=today).last() + # Check if we have a record for today + visited_data = IP.objects.filter(address=user_ip, path=request.path, created__date=today).first() if visited_data: - # If the creation date is today - if visited_data.created.date() == today: - # If the visit count is 1, update the repo visit count - if visited_data.count == 1: - repo.repo_visit_count = F("repo_visit_count") + 1 - repo.save() - else: - # If the creation date is not today, reset the creation date and count - visited_data.created = now() - visited_data.count = 1 - visited_data.save() - - # Increment the repo visit count + # If we have a record for today, only update repo visit count if needed + if visited_data.count == 1: repo.repo_visit_count = F("repo_visit_count") + 1 repo.save() else: - # If no record exists, create a new one - IP.objects.create(address=user_ip, path=request.path, created=now(), count=1) + # If no record exists for today, create a new one + IP.objects.create(address=user_ip, path=request.path, created=current_time, count=1) # Increment the repo's visit count repo.repo_visit_count = F("repo_visit_count") + 1 repo.save() - # Refresh project to get the latest visit count + # Get unique visits, grouped by date (last 7 days) + seven_days_ago = today - timedelta(days=7) + visit_counts = ( + IP.objects.filter(path=request.path, created__date__gte=seven_days_ago) + .annotate(date=TruncDate("created")) + .values("date") + .annotate(visit_count=Count("address")) + .order_by("date") + ) + + # Refresh repo to get the latest visit count repo.refresh_from_db() + # Extract dates and counts + dates = [entry["date"] for entry in visit_counts] + counts = [entry["visit_count"] for entry in visit_counts] total_views = repo.repo_visit_count - fig = plt.figure(figsize=(4, 1)) - plt.bar(0, total_views, color="red", width=0.5) - - plt.title( - f"{total_views}", - loc="left", - x=-0.36, - y=0.3, - fontsize=15, - fontweight="bold", - color="red", - ) + # Create a new image with a white background + width = 600 + height = 200 + img = Image.new("RGB", (width, height), color="white") + draw = ImageDraw.Draw(img) + + # Define colors + bar_color = "#e05d44" + text_color = "#333333" + grid_color = "#eeeeee" + + # Calculate chart dimensions + margin = 40 + chart_width = width - 2 * margin + chart_height = height - 2 * margin + + # Draw grid lines + for i in range(5): + y = margin + (chart_height * i) // 4 + draw.line([(margin, y), (width - margin, y)], fill=grid_color) + + # Draw bars + if dates and counts: + max_count = max(counts) + bar_width = chart_width / (len(counts) * 2) # Leave space between bars + for i, count in enumerate(counts): + bar_height = (count / max_count) * chart_height + x1 = margin + (i * 2 * bar_width) + y1 = height - margin - bar_height + x2 = x1 + bar_width + y2 = height - margin + + # Draw bar with a slight gradient effect + for h in range(int(y1), int(y2)): + alpha = int(255 * (1 - (h - y1) / bar_height * 0.2)) + r, g, b = 224, 93, 68 # RGB values for #e05d44 + current_color = f"#{r:02x}{g:02x}{b:02x}" + draw.line([(x1, h), (x2, h)], fill=current_color) + + # Draw total views text + try: + font = ImageFont.truetype("DejaVuSans.ttf", 32) + except OSError: + font = ImageFont.load_default() - plt.gca().set_xticks([]) # Remove x-axis ticks - plt.gca().set_yticks([]) - plt.box(False) + text = f"Total Views: {total_views}" + text_bbox = draw.textbbox((0, 0), text, font=font) + text_width = text_bbox[2] - text_bbox[0] + draw.text(((width - text_width) // 2, margin // 2), text, font=font, fill=bar_color) - # Save the plot to an in-memory file + # Save the image to a buffer buffer = BytesIO() - plt.savefig(buffer, format="png", bbox_inches="tight") - plt.close() + img.save(buffer, format="PNG", quality=95) buffer.seek(0) - # Prepare the HTTP response with the bar graph image + # Return the image with appropriate headers response = HttpResponse(buffer, content_type="image/png") response["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" response["Pragma"] = "no-cache"