2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-05-27 19:39:22 +00:00

feat(backend)!: bump to dj 5.2 lts / py 3.11 (#10730)

* feat(backend): bump to 5.2 lts / python 3.11

This will give us support till 2027-10 (PEP 664)

* bump dependencies

* fix dflt version

* remove 3.9 precaution

* changes for 5.2

* changes for py 3.10

* debug command

* lower crypto again

* another lowering

* fix version string

* lower minimum version to 3.11

* update refs

* fix text

* reaking: remove now unsupported OS

* disable break

* remove temp changes

* fix ruff call

* fix remaining ruff warnings

* remove old arg

* lower allauth reqs

* replace old method

* fix issue with args passing beeing depreceated

* add changelog entries

* bump dependencies a bit further

* fix broken image init for now
might need a refactor

* fix another test

* refactor image name lookup

* mroe refactoring

* ensure str does not cause an issue

* update referenced function

* fix cal sig

* simplify method and add test

* refactor

* ignore wrong typings

* fix deprecated feature

* simplify

* ensure image tests do their job

* simplify

* re-add type check

* fix test

* fix assertations - wonder how long this was broken

* bump to newer versions

* bump deps

* fix assertation
This commit is contained in:
Matthias Mair
2025-11-11 01:45:25 +01:00
committed by GitHub
parent f3e8482469
commit 5d21bf2679
83 changed files with 1292 additions and 1407 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ pool:
strategy:
matrix:
Python39:
PYTHON_VERSION: '3.9'
PYTHON_VERSION: '3.11'
maxParallel: 3
steps:
+1 -1
View File
@@ -9,7 +9,7 @@ on:
- l10
env:
python_version: 3.9
python_version: 3.11
permissions:
contents: read
+3 -3
View File
@@ -9,7 +9,7 @@ on:
branches-ignore: ["l10*"]
env:
python_version: 3.9
python_version: 3.11
node_version: 20
# The OS version must be set per job
server_start_sleep: 60
@@ -334,8 +334,8 @@ jobs:
continue-on-error: true # continue if a step fails so that coverage gets pushed
strategy:
matrix:
python_version: [3.9]
# python_version: [3.9, 3.12] # Disabled due to requirement issues
python_version: [3.11]
# python_version: [3.11, 3.14] # Disabled due to requirement issues
env:
INVENTREE_DB_NAME: ./inventree.sqlite
+1 -1
View File
@@ -7,7 +7,7 @@ on:
permissions:
contents: read
env:
python_version: 3.9
python_version: 3.11
jobs:
stable:
+1 -1
View File
@@ -6,7 +6,7 @@ on:
- master
env:
python_version: 3.9
python_version: 3.11
node_version: 20
permissions:
+3 -5
View File
@@ -20,9 +20,9 @@ before:
- contrib/packager.io/before.sh
dependencies:
- curl
- "python3.9 | python3.10 | python3.11 | python3.12 | python3.13 | python3.14"
- "python3.9-venv | python3.10-venv | python3.11-venv | python3.12-venv | python3.13-venv | python3.14-venv"
- "python3.9-dev | python3.10-dev | python3.11-dev | python3.12-dev | python3.13-dev | python3.14-dev"
- "python3.11 | python3.12 | python3.13 | python3.14"
- "python3.11-venv | python3.12-venv | python3.13-venv | python3.14-venv"
- "python3.11-dev | python3.12-dev | python3.13-dev | python3.14-dev"
- python3-pip
- python3-cffi
- python3-brotli
@@ -35,8 +35,6 @@ dependencies:
- jq
- "libffi7 | libffi8"
targets:
ubuntu-20.04: true
ubuntu-22.04: true
ubuntu-24.04: true
debian-11: true
debian-12: true
+2 -2
View File
@@ -18,11 +18,11 @@ repos:
exclude: mkdocs.yml
- id: mixed-line-ending
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.13
rev: v0.14.3
hooks:
- id: ruff-format
args: [--preview]
- id: ruff
- id: ruff-check
args: [
--fix,
# --unsafe-fixes,
+1 -1
View File
@@ -44,7 +44,7 @@
"name": "InvenTree invoke schema",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/.venv/lib/python3.9/site-packages/invoke/__main__.py",
"program": "${workspaceFolder}/.venv/lib/python3.11/site-packages/invoke/__main__.py",
"cwd": "${workspaceFolder}",
"args": [
"dev.schema","--ignore-warnings"
+1
View File
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
### Removed
- Removed python 3.9 / 3.10 support as part of Django 5.2 upgrade in [#10730](https://github.com/inventree/InvenTree/pull/10730)
## 1.1.0 - 2025-11-02
+1 -1
View File
@@ -17,7 +17,7 @@ gunicorn>=22.0.0
# LDAP required packages
django-auth-ldap # Django integration for ldap auth
python-ldap # LDAP auth support
django<5.0 # Force lower to match main project
django<6.0 # Force lower to match main project
# Upgraded python package installer
uv
+92 -92
View File
@@ -4,9 +4,9 @@ asgiref==3.10.0 \
--hash=sha256:aef8a81283a34d0ab31630c9b7dfe70c812c95eba78171367ca8745e88124734 \
--hash=sha256:d89f2d8cd8b56dada7d52fa7dc8075baa08fb836560710d38c292a7a3f78c04e
# via django
django==4.2.26 \
--hash=sha256:9398e487bcb55e3f142cb56d19fbd9a83e15bb03a97edc31f408361ee76d9d7a \
--hash=sha256:c96e64fc3c359d051a6306871bd26243db1bd02317472a62ffdbe6c3cae14280
django==5.2.8 \
--hash=sha256:23254866a5bb9a2cfa6004e8b809ec6246eba4b58a7589bc2772f1bcc8456c7f \
--hash=sha256:37e687f7bd73ddf043e2b6b97cfe02fcbb11f2dbb3adccc6a2b18c6daa054d7f
# via
# -r contrib/container/requirements.in
# django-auth-ldap
@@ -53,77 +53,77 @@ packaging==25.0 \
# via
# gunicorn
# mariadb
psycopg[binary, pool]==3.2.11 \
--hash=sha256:217231b2b6b72fba88281b94241b2f16043ee67f81def47c52a01b72ff0c086a \
--hash=sha256:398bb484ed44361e041c8f804ed7af3d2fcefbffdace1d905b7446c319321706
psycopg[binary, pool]==3.2.12 \
--hash=sha256:85c08d6f6e2a897b16280e0ff6406bef29b1327c045db06d21f364d7cd5da90b \
--hash=sha256:8a1611a2d4c16ae37eada46438be9029a35bb959bb50b3d0e1e93c0f3d54c9ee
# via -r contrib/container/requirements.in
psycopg-binary==3.2.11 \
--hash=sha256:00221bfeb9594ca6e01207b032c300fa6f889d918bf0de47f4571c1f9f6e1578 \
--hash=sha256:110a2036007230416fcc2c17bfe7aaa2c1fa9b6e9d21e2cd551523e3f6489759 \
--hash=sha256:199f88a05dd22133eab2deb30348ef7a70c23d706c8e63fdc904234163c63517 \
--hash=sha256:1db270e6bdbd183e3908cd9bb832506b99e1f2222a2fc2145f980c3ba1c8c30f \
--hash=sha256:20d41bcd9ac289d44ac1f6151594f7883483b4ad14680a63e04b639dc90c3349 \
--hash=sha256:23c77dbbffe8ba679213877f7204f4599bd545b65d2d69982fd685a3fea35b11 \
--hash=sha256:260738ae222b41dbefd0d84cb2e150a112f90b41688630f57fdac487ab6d6f38 \
--hash=sha256:27eb6367350b75fef882c40cd6f748bfd976db2f8651f7511956f11efc15154f \
--hash=sha256:2a438fad4cc081b018431fde0e791b6d50201526edf39522a85164f606c39ddb \
--hash=sha256:30e2c114d26554ae677088de5d4133cc112344d7a233200fdbf4a2ca5754c7ec \
--hash=sha256:31f1d5630afa673c37a6327f8e3efa1f17d4e4e42972643b3478b52275233529 \
--hash=sha256:32bd319a68420631a320bb450921c8320641621a92556c97b38b1e116010c344 \
--hash=sha256:3bd2c8fb1dec6f93383fbaa561591fa3d676e079f9cb9889af17c3020a19715f \
--hash=sha256:3f32b09fba85d9e239229bdc5b6254420c02054f6954fe7fbd1ecf1ca93009ed \
--hash=sha256:478a68d50f34f6203642d245e2046d266c719ab4e593a1bb94c3be5f82e1aee1 \
--hash=sha256:47f6cf8a1d02d25238bdb8741ac641ff0ec22b1c6ff6a2acd057d0da5c712842 \
--hash=sha256:49d76391b225f72dd63fcab87937ccf307ae0f093b5a382eeacf05f19a57c176 \
--hash=sha256:4cae9bdc482e36e825d5102a9f3010e729f33a4ca83fc8a1f439ba16eb61e1f1 \
--hash=sha256:54a30f00a51b9043048b3e7ee806ffd31fc5fbd02a20f0e69d21306ff33dc473 \
--hash=sha256:566d02a0b85b994e40b4f6276b3423c59e8157f10b73bd2e634f8e0a3dfb1890 \
--hash=sha256:5768a9e7d393b2edd3a28de5a6d5850d054a016ed711f7044a9072f19f5e50d5 \
--hash=sha256:581358e770a4536e546841b78fd0fe318added4a82443bf22d0bbe3109cf9582 \
--hash=sha256:58997db1aa48a1119e26c1c2f893d1c92339bd3be5d1f25334f22eaeaeeca90e \
--hash=sha256:58d8f9f80ae79ba7f2a0509424939236220d7d66a4f8256ae999b882cc58065b \
--hash=sha256:592fb928efe0674a7400af914bcf931eb5267d36237925947aaecf63bd9a91aa \
--hash=sha256:5bc571786a256a2fa2d8f13b5ecf714020b753bc76c2fa6d308e46751946dc31 \
--hash=sha256:5f6f948ff1cd252003ff534d7b50a2b25453b4212b283a7514ff8751bdb68c37 \
--hash=sha256:5fb27dd9c52ae13cb4de90244207155b694f76a75a816115ead2d573f40e1e36 \
--hash=sha256:6688807ed07436c18e9946d01372bc80b9d20b7732cde27de9313e0860910c84 \
--hash=sha256:6b9632c42f76d5349e7dd50025cff02688eb760b258e891ad2c6428e7e4917d5 \
--hash=sha256:720e19ff2d1c425b6be18bd20ba35010c7927e492bcfecbae1085a89caa7db7c \
--hash=sha256:749d23fbfd642a7abfef5fc0f6ca185fa82a2c0f895e6eab42c3f2a5d88f6011 \
--hash=sha256:7608c9fa58b85426093ab8777080e8f134d89915c05c51fa270e7aee317f2b38 \
--hash=sha256:766089fdaa8af1b5f7e2ec9fd7ad190c865e226b4fb0e7b1bd8dbcd62b5b923e \
--hash=sha256:7744b4ed1f3b76fe37de7e9ef98014482fe74b6d3dfe1026cc4cfb4b4404e74f \
--hash=sha256:7b3c5474dbad63bcccb8d14d4d4c7c19f1dc6f8e8c1914cbc771d261cf8eddca \
--hash=sha256:81e57d1f00af9b7414c8d00ac77892b3786ddd69a23c27dee47cae8fd3543b07 \
--hash=sha256:82fe30afbdd66fbdad583b02baad5c15930a3dc8a3756d2ae15fc874e9be8ec8 \
--hash=sha256:8792e502a16a0b28d9fd23571fe492271a00c4b2b55f6c0b8377e47032758cd3 \
--hash=sha256:91268f04380964a5e767f8102d05f1e23312ddbe848de1a9514b08b3fc57d354 \
--hash=sha256:9b4b0fc4e774063ae64c92cc57e2b10160150de68c96d71743218159d953869d \
--hash=sha256:9bdc762600fcc8e4ad3224734a4e70cc226207fd8f2de47c36b115efeed01782 \
--hash=sha256:9ea3ebe1706fd78d6ac0dd1cf692a77cfacd5ba967c82128f9863a5e48f63c47 \
--hash=sha256:9f12a34bddaeffa7840a61163595ec0d70a9db855896865dcfbb731510014484 \
--hash=sha256:a3a59d404e1fb8ec47116f66f5adf48a8993a8aac0dad0395a923155fd55ee38 \
--hash=sha256:b051aa1e67f0d03ccdb4503d716f22da56229896526f0aa721e5a199baa9e5d4 \
--hash=sha256:b2fa94ce40bc4b408149d83a6204fc5e53c3e9d3cd5b749de2e7e9671a049cf7 \
--hash=sha256:c45f61202e5691090a697e599997eaffa3ec298209743caa4fd346145acabafe \
--hash=sha256:c594c199869099c59c85b9f4423370b6212491fb929e7fcda0da1768761a2c2c \
--hash=sha256:d27f51b8ce291da4af749ef850adb4520bfe52c2ff4677402c719ff35af03f00 \
--hash=sha256:d59db908d9baaa057a43dd5aa8352f3e3de4b8c57f295172d5fe521e97d6c39d \
--hash=sha256:d7e490848d7bedf6c1d2180233a33d31d554a1b0823f80a0236ebb7d3b6caf12 \
--hash=sha256:e3b6328bc2f3ca233f9a5f08d266089b96a534eca9ee4e45cb92d0a8d4629d9c \
--hash=sha256:e3f5887019dfb094c60e7026968ca3a964ca16305807ba5e43f9a78483767d5f \
--hash=sha256:e7575ca710277cc3e9257ff803a3e0e3cb7cc1b7851639cb783a7cd55ebfc815 \
--hash=sha256:e7f4dff472a529c9027f294c8842ab535bbed7e2928fe1f4fc28b27f2463d6d5 \
--hash=sha256:eab6959fade522e586b8ec37d3fe337ce10861965edef3292f52e66e36dc375d \
--hash=sha256:f5e7415b5d0f58edf2708842c66605092df67f3821161d861b09695fc326c4de \
--hash=sha256:f72146ad5b69ea177c2707578e5a4a9422b79e50d5a80992dabc5619b0929771 \
--hash=sha256:fa2aa5094dc962967ca0978c035b3ef90329b802501ef12a088d3bac6a55598e \
--hash=sha256:fe5e3648e855df4fba1d70c18aef18c9880ea8d123fdfae754c18787c8cb37b3 \
--hash=sha256:ff64883cff66fe797cd958c0ff7f53fc36a28239b9e0dc80189ce1c03ce47153
psycopg-binary==3.2.12 \
--hash=sha256:095ccda59042a1239ac2fefe693a336cb5cecf8944a8d9e98b07f07e94e2b78d \
--hash=sha256:0afb71a99871a41dd677d207c6a988d978edde5d6a018bafaed4f9da45357055 \
--hash=sha256:100fdfee763d701f6da694bde711e264aca4c2bc84fb81e1669fb491ce11d219 \
--hash=sha256:13cd057f406d2c8063ae8b489395b089a7f23c39aff223b5ea39f0c4dd640550 \
--hash=sha256:15e226f0d8af85cc8b2435b2e9bc6f0d40febc79eef76cf20fceac4d902a6a7b \
--hash=sha256:16db2549a31ccd4887bef05570d95036813ce25fd9810b523ba1c16b0f6cfd90 \
--hash=sha256:1c1dbeb8e97d00a33dfa9987776ce3d1c1e4cc251dfbd663b8f9e173f5c89d17 \
--hash=sha256:1d7cedecbe0bb60a2e72b1613fba4072a184a6472d6cc9aa99e540217f544e3e \
--hash=sha256:2598d0e4f2f258da13df0560187b3f1dfc9b8688c46b9d90176360ae5212c3fc \
--hash=sha256:26b5927b5880b396231ab6190ee5c8fb47ed3f459b53504ed5419faaf16d3bfb \
--hash=sha256:294f08b014f08dfd3c9b72408f5e1a0fd187bd86d7a85ead651e32dbd47aa038 \
--hash=sha256:2aa80ca8d17266507bef853cecefa7d632ffd087883ee7ca92b8a7ea14a1e581 \
--hash=sha256:2d55009eeddbef54c711093c986daaf361d2c4210aaa1ee905075a3b97a62441 \
--hash=sha256:310c95a68a9b948b89d6d187622757d57b6c26cece3c3f7c2cbb645ee36531b2 \
--hash=sha256:32b3e12d9441508f9c4e1424f4478b1a518a90a087cd54be3754e74954934194 \
--hash=sha256:356b4266e5cde7b5bbcf232f549dedf7fbed4983daa556042bdec397780e044d \
--hash=sha256:385c7b5cfffac115f413b8e32c941c85ea0960e0b94a6ef43bb260f774c54893 \
--hash=sha256:3c1e38b1eda54910628f68448598139a9818973755abf77950057372c1fe89a6 \
--hash=sha256:3e9c9e64fb7cda688e9488402611c0be2c81083664117edcc709d15f37faa30f \
--hash=sha256:442f20153415f374ae5753ca618637611a41a3c58c56d16ce55f845d76a3cf7b \
--hash=sha256:489b154891f1c995355adeb1077ee3479e9c9bada721b93270c20243bbad6542 \
--hash=sha256:48a8e29f3e38fcf8d393b8fe460d83e39c107ad7e5e61cd3858a7569e0554a39 \
--hash=sha256:49582c3b6d578bdaab2932b59f70b1bd93351ed4d594b2c97cea1611633c9de1 \
--hash=sha256:58ed30d33c25d7dc8d2f06285e88493147c2a660cc94713e4b563a99efb80a1f \
--hash=sha256:5b6e505618cb376a7a7d6af86833a8f289833fe4cc97541d7100745081dc31bd \
--hash=sha256:66a031f22e4418016990446d3e38143826f03ad811b9f78f58e2afbc1d343f7a \
--hash=sha256:6a898717ab560db393355c6ecf39b8c534f252afc3131480db1251e061090d3a \
--hash=sha256:7130effd0517881f3a852eff98729d51034128f0737f64f0d1c7ea8343d77bd7 \
--hash=sha256:72fd979e410ba7805462817ef8ed6f37dd75f9f4ae109bdb8503e013ccecb80b \
--hash=sha256:77690f0bf08356ca00fc357f50a5980c7a25f076c2c1f37d9d775a278234fefd \
--hash=sha256:79de3cc5adbf51677009a8fda35ac9e9e3686d5595ab4b0c43ec7099ece6aeb5 \
--hash=sha256:7b9a99ded7d19b24d3b6fa632b58e52bbdecde7e1f866c3b23d0c27b092af4e3 \
--hash=sha256:802bd01fb18a0acb0dea491f69a9a2da6034f33329a62876ab5b558a1fb66b45 \
--hash=sha256:8335d989a4e94df2ccd8a1acbba9d03c4157ea8d73b65b79d447c6dc10b001d8 \
--hash=sha256:89b3c5201ca616d69ca0c3c0003ca18f7170a679c445c7e386ebfb4f29aa738e \
--hash=sha256:8ffe75fe6be902dadd439adf4228c98138a992088e073ede6dd34e7235f4e03e \
--hash=sha256:909de94de7dd4d6086098a5755562207114c9638ec42c52d84c8a440c45fe084 \
--hash=sha256:940ac69ef6e89c17b3d30f3297a2ad03efdd06a4b1857f81bc533a9108a90eb9 \
--hash=sha256:95f2806097a49bfd57e0c6a178f77b99487c53c157d9d507aee9c40dd58efdb4 \
--hash=sha256:9c674887d1e0d4384c06c822bc7fcfede4952742e232ec1e76b5a6ae39a3ddd4 \
--hash=sha256:9fdf3a0c24822401c60c93640da69b3dfd4d9f29c3a8d797244fe22bfe592823 \
--hash=sha256:ab02b7d138768fd6ac4230e45b073f7b9fd688d88c04f24c34df4a250a94d066 \
--hash=sha256:acb1811219a4144539f0baee224a11a2aa323a739c349799cf52f191eb87bc52 \
--hash=sha256:bfd632f7038c76b0921f6d5621f5ba9ecabfad3042fa40e5875db11771d2a5de \
--hash=sha256:ce68839da386f137bc8d814fdbeede8f89916b8605e3593a85b504a859243af9 \
--hash=sha256:d369e79ad9647fc8217cbb51bbbf11f9a1ffca450be31d005340157ffe8e91b3 \
--hash=sha256:dc68094e00a5a7e8c20de1d3a0d5e404a27f522e18f8eb62bbbc9f865c3c81ef \
--hash=sha256:deeb06b7141f3a577c3aa8562307e2747580ae43d705a0482603a2c1f110d046 \
--hash=sha256:e0b5ccd03ca4749b8f66f38608ccbcb415cbd130d02de5eda80d042b83bee90e \
--hash=sha256:ea049c8d33c4f4e6b030d5a68123c0ccd2ffb77d4035f073db97187b49b6422f \
--hash=sha256:ea9751310b840186379c949ede5a5129b31439acdb929f3003a8685372117ed8 \
--hash=sha256:ec82fa5134517af44e28a30c38f34384773a0422ffd545fd298433ea9f2cc5a9 \
--hash=sha256:eedc410f82007038030650aa58f620f9fe0009b9d6b04c3dc71cbd3bae5b2675 \
--hash=sha256:ef40601b959cc1440deaf4d53472ab54fa51036c37189cf3fe5500559ac25347 \
--hash=sha256:ef92d5ba6213de060d1390b1f71f5c3b2fbb00b4d55edee39f3b07234538b64a \
--hash=sha256:efab679a2c7d1bf7d0ec0e1ecb47fe764945eff75bb4321f2e699b30a12db9b3 \
--hash=sha256:f33c9e12ed05e579b7fb3c8fdb10a165f41459394b8eb113e7c377b2bd027f61 \
--hash=sha256:f3bae4be7f6781bf6c9576eedcd5e1bb74468126fa6de991e47cdb1a8ea3a42a \
--hash=sha256:f6ba1fe35fd215813dac4544a5ffc90f13713b29dd26e9e5be97ba53482bf6d6 \
--hash=sha256:f7c81bc60560be9eb3c23601237765069ebfa9881097ce19ca6b5ea17c5faa8f \
--hash=sha256:f8107968a9eadb451cfa6cf86036006fdde32a83cd39c26c9ca46765e653b547 \
--hash=sha256:f821e0c8a8fdfddfa71acb4f462d7a4c5aae1655f3f5e078970dbe9f19027386
# via psycopg
psycopg-pool==3.2.6 \
--hash=sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5 \
--hash=sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7
psycopg-pool==3.2.7 \
--hash=sha256:4b47bb59d887ef5da522eb63746b9f70e2faf967d34aac4f56ffc65e9606728f \
--hash=sha256:a77d531bfca238e49e5fb5832d65b98e69f2c62bfda3d2d4d833696bdc9ca54b
# via psycopg
pyasn1==0.6.1 \
--hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
@@ -229,26 +229,26 @@ typing-extensions==4.15.0 \
# via
# psycopg
# psycopg-pool
uv==0.9.7 \
--hash=sha256:0fdbfad5b367e7a3968264af6da5bbfffd4944a90319042f166e8df1a2d9de09 \
--hash=sha256:134e0daac56f9e399ccdfc9e4635bc0a13c234cad9224994c67bae462e07399a \
--hash=sha256:1aaf79b4234400e9e2fbf5b50b091726ccbb0b6d4d032edd3dfd4c9673d89dca \
--hash=sha256:34fe0af83fcafb9e2b786f4bd633a06c878d548a7c479594ffb5607db8778471 \
--hash=sha256:555ee72146b8782c73d755e4a21c9885c6bfc81db0ffca2220d52dddae007eb7 \
--hash=sha256:56a440ccde7624a7bc070e1c2492b358c67aea9b8f17bc243ea27c5871c8d02c \
--hash=sha256:62b315f62669899076a1953fba6baf50bd2b57f66f656280491331dcedd7e6c6 \
--hash=sha256:635e82c2d0d8b001618af82e4f2724350f15814f6462a71b3ebd44adec21f03c \
--hash=sha256:7019f4416925f4091b9d28c1cf3e8444cf910c4ede76bdf1f6b9a56ca5f97985 \
--hash=sha256:777bb1de174319245a35e4f805d3b4484d006ebedae71d3546f95e7c28a5f436 \
--hash=sha256:89697fa0d7384ba047daf75df844ee7800235105e41d08e0c876861a2b4aa90e \
--hash=sha256:8cf6bc2482d1293cc630f66b862b494c09acda9b7faff7307ef52667a2b3ad49 \
--hash=sha256:b5f1fb8203a77853db176000e8f30d5815ab175dc46199db059f97a72fc51110 \
--hash=sha256:bb8bfcc2897f7653522abc2cae80233af756ad857bfbbbbe176f79460cbba417 \
--hash=sha256:bcf878528bd079fe8ae15928b5dfa232fac8b0e1854a2102da6ae1a833c31276 \
--hash=sha256:c9810ee8173dce129c49b338d5e97f3d7c7e9435f73e0b9b26c2f37743d3bb9e \
--hash=sha256:d13da6521d4e841b1e0a9fda82e793dcf8458a323a9e8955f50903479d0bfa97 \
--hash=sha256:d6e5fe28ca05a4b576c0e8da5f69251dc187a67054829cfc4afb2bfa1767114b \
--hash=sha256:edd768f6730bba06aa10fdbd80ee064569f7236806f636bf65b68136a430aad0
uv==0.9.8 \
--hash=sha256:0f03bc413c933dbf850ad0dc2dba3df6b80c860a5c65cd767add49da19dadef0 \
--hash=sha256:14670bf55ecb5cfd0f3654fbf51c58a21dec3ad8ab531079b3ed8599271dc77b \
--hash=sha256:1b8b5bdcda3e10ea70b618d0609acddc5c725cb58d4caf933030ddedd7c2e98f \
--hash=sha256:40253d00c1e900a0a61b132b1e0dd4aa83575cfd5302d3671899b6de29b1ef67 \
--hash=sha256:50d130c46d97d7f10675ebea8608b7b4722c84b5745cd1bb0c8ae6d7984c05d5 \
--hash=sha256:543693def38fa41b9706aba391111fe8d9dd6be86899d76f9581faf045ac1cb6 \
--hash=sha256:5af28f1645eb3c50fd34a78508792db2d0799816f4eb5f55e1e6e2c724dfb125 \
--hash=sha256:6a01d7cd41953ffac583139b10ad1df004a67c0246a6b694eb5bcdbc8c99deaf \
--hash=sha256:6df2e16f6df32018047c60bab2c0284868ad5c309addba9183ea2eeb71746bf0 \
--hash=sha256:7038a552159f2291dd0d1f4f66a36261b5f3ed5fcd92e2869186f8e910b2c935 \
--hash=sha256:75671150d6eb9d5ee829e1fdb8cf86b8e44a66d27cbb996fe807e986c4107b5d \
--hash=sha256:87c3b65b6d5fcbdeab199d54c74fbf75de19cb534a690c936c5616478a038576 \
--hash=sha256:99b18bfe92c33c3862b65d74677697e799763e669e0064685f405e7e27517f25 \
--hash=sha256:9f2f3576c4518ff4f15e48dbca70585a513523c4738bc8cc2e48b20fd1190ce3 \
--hash=sha256:a4010b3fdabbb3c4f2cf2f7aa3bf6002d00049dcbc54ce0ee5ada32a933b2290 \
--hash=sha256:bb0f8e83c2a2fc5a802e930cc8a7b71ab068180300a3f27ba38037f9fcb3d430 \
--hash=sha256:cdbfadca9522422ab9820f5ada071c9c5c869bcd6fee719d20d91d5ec85b2a7d \
--hash=sha256:d93a2227d23e81ab3a16c30363559afc483e8aca40ea9343b3f326a9a41718c9 \
--hash=sha256:f52c6a99197028a314d4c1825f7ccb696eb9a88b822d2e2f17046266c75e543e
# via -r contrib/container/requirements.in
wheel==0.45.1 \
--hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \
+4 -4
View File
@@ -4,8 +4,8 @@
#
Color_Off='\033[0m'
On_Red='\033[41m'
PYTHON_FROM=9
PYTHON_TO=14
PYTHON_FROM=11
PYTHON_TO=15
function detect_docker() {
if [ -n "$(grep docker </proc/1/cgroup)" ]; then
@@ -61,7 +61,7 @@ function detect_python() {
echo "# POI07| No python environment found - using environment variable: ${SETUP_PYTHON}"
fi
# Try to detect a python between 3.9 and 3.12 in reverse order
# Try to detect a python between lowest and highest supported in reverse order
if [ -z "$(which ${SETUP_PYTHON})" ]; then
echo "# POI07| Trying to detecting python3.${PYTHON_FROM} to python3.${PYTHON_TO} - using newest version"
for i in $(seq $PYTHON_TO -1 $PYTHON_FROM); do
@@ -79,7 +79,7 @@ function detect_python() {
echo "${On_Red}"
echo "# POI07| Python ${SETUP_PYTHON} not found - aborting!"
echo "# POI07| Please ensure python can be executed with the command '$SETUP_PYTHON' by the current user '$USER'."
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.10'."
echo "# POI07| If you are using a different python version, please set the environment variable SETUP_PYTHON to the correct command - eg. 'python3.11'."
echo "${Color_Off}"
exit 1
fi
+1 -1
View File
@@ -26,7 +26,7 @@ export DATA_DIR=${APP_HOME}/data
export SETUP_NGINX_FILE=${SETUP_NGINX_FILE:-/etc/nginx/sites-enabled/inventree.conf}
export SETUP_ADMIN_PASSWORD_FILE=${CONF_DIR}/admin_password.txt
export SETUP_NO_CALLS=${SETUP_NO_CALLS:-false}
export SETUP_PYTHON=${SETUP_PYTHON:-python3.9}
export SETUP_PYTHON=${SETUP_PYTHON:-python3.11}
export SETUP_ADMIN_NOCREATION=${SETUP_ADMIN_NOCREATION:-false}
# SETUP_DEBUG can be set to get debug info
# SETUP_EXTRA_PIP can be set to install extra pip packages
+2 -2
View File
@@ -357,9 +357,9 @@ extra:
# provider: google
# property: UA-143467500-1
min_python_version: 3.9
min_python_version: 3.10
min_invoke_version: 2.0.0
django_version: 4.2
django_version: 5.2
docker_postgres_version: 16
version:
-19
View File
@@ -152,10 +152,6 @@ essentials-openapi==1.2.1 \
--hash=sha256:70b06d80a8d9cb7634b702903cfe8fcfc48e6fc054e744eab514bb7bc37f00c9 \
--hash=sha256:d0085cde3ceaa25e6fc4983d8372ac0185909e3fa2791f498280379bb3c86c62
# via neoteroi-mkdocs
exceptiongroup==1.3.0 \
--hash=sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10 \
--hash=sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88
# via anyio
ghp-import==2.1.0 \
--hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
--hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343
@@ -197,14 +193,6 @@ idna==3.10 \
# anyio
# httpx
# requests
importlib-metadata==8.7.0 \
--hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \
--hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd
# via
# markdown
# mkdocs
# mkdocs-get-deps
# mkdocstrings
jinja2==3.1.6 \
--hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
--hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
@@ -551,9 +539,6 @@ typing-extensions==4.14.1 \
# via
# anyio
# beautifulsoup4
# exceptiongroup
# gitpython
# mkdocstrings-python
urllib3==2.5.0 \
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
@@ -594,7 +579,3 @@ wcmatch==10.1 \
--hash=sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a \
--hash=sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af
# via mkdocs-include-markdown-plugin
zipp==3.23.0 \
--hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \
--hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166
# via importlib-metadata
+1 -1
View File
@@ -97,7 +97,7 @@ skip-magic-trailing-comma = true
line-ending = "auto"
[tool.uv.pip]
python-version = "3.9.2"
python-version = "3.11.0"
no-strip-extras=true
generate-hashes=true
+1 -1
View File
@@ -11,7 +11,7 @@ python:
build:
os: "ubuntu-22.04"
tools:
python: "3.9"
python: "3.11"
jobs:
post_install:
- pip install -U invoke
+1 -1
View File
@@ -53,7 +53,7 @@ def read_license_file(path: Path) -> list:
return []
try:
data = json.loads(path.read_text())
data = json.loads(path.read_text(encoding='utf-8'))
except Exception as e:
logger.exception("Failed to parse license file '%s': %s", path, e)
return []
@@ -1,11 +1,14 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 423
INVENTREE_API_VERSION = 424
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v424 -> 2025-11-05 : https://github.com/inventree/InvenTree/pull/10730
- Adds more lower / upper bounds to integer fields in the API schema due to bump to Django 5.2- no functional changes
v423 -> 2025-11-05 : https://github.com/inventree/InvenTree/pull/10772
- Adds "category_detail" field to BomItem API endpoints
- Adds "category_detail" field to BuildLine API endpoints
+1 -1
View File
@@ -277,7 +277,7 @@ class InvenTreeConfig(AppConfig):
new_user = user.objects.create_superuser(
add_user, add_email, add_password
)
logger.info('User %s was created!', str(new_user))
logger.info('User %s was created!', new_user)
except IntegrityError:
logger.warning('The user "%s" could not be created', add_user)
+1 -2
View File
@@ -2,7 +2,6 @@
import datetime
import time
from typing import Union
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -109,7 +108,7 @@ class InvenTreeMailLoggingBackend(BaseEmailBackend):
return self.backend.open()
def send_messages(
self, email_messages: list[Union[EmailMessage, EmailMultiAlternatives]]
self, email_messages: list[EmailMessage | EmailMultiAlternatives]
) -> int:
"""Send email messages and log them to the database.
+4 -4
View File
@@ -8,7 +8,7 @@ import random
import shutil
import string
from pathlib import Path
from typing import Optional, Union
from typing import Optional
logger = logging.getLogger('inventree')
CONFIG_DATA = None
@@ -177,7 +177,7 @@ def get_config_file(create=True) -> Path:
return cfg_filename
def load_config_data(set_cache: bool = False) -> Union[map, None]:
def load_config_data(set_cache: bool = False) -> map | None:
"""Load configuration data from the config file.
Arguments:
@@ -392,7 +392,7 @@ def get_plugin_dir():
return get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
def get_secret_key(return_path: bool = False) -> Union[str, Path]:
def get_secret_key(return_path: bool = False) -> str | Path:
"""Return the secret key value which will be used by django.
Following options are tested, in descending order of preference:
@@ -436,7 +436,7 @@ def get_secret_key(return_path: bool = False) -> Union[str, Path]:
return secret_key_file.read_text().strip()
def get_oidc_private_key(return_path: bool = False) -> Union[str, Path]:
def get_oidc_private_key(return_path: bool = False) -> str | Path:
"""Return the private key for OIDC authentication.
Following options are tested, in descending order of preference:
+1 -1
View File
@@ -22,7 +22,7 @@ class InvenTreeDateFilter(rest_filters.DateFilter):
if settings.USE_TZ and value is not None:
tz = timezone.get_current_timezone()
value = datetime(value.year, value.month, value.day)
value = make_aware(value, timezone=tz, is_dst=True)
value = make_aware(value, timezone=tz)
return super().filter(qs, value)
+44 -6
View File
@@ -5,11 +5,10 @@ import hashlib
import inspect
import io
import json
import os
import os.path
import re
from decimal import Decimal, InvalidOperation
from typing import Optional, TypeVar, Union
from typing import Optional, TypeVar
from wsgiref.util import FileWrapper
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
@@ -17,6 +16,7 @@ from django.conf import settings
from django.contrib.staticfiles.storage import StaticFilesStorage
from django.core.exceptions import FieldError, ValidationError
from django.core.files.storage import default_storage
from django.db.models.fields.files import ImageFieldFile
from django.http import StreamingHttpResponse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@@ -28,6 +28,7 @@ import structlog
from bleach import clean
from djmoney.money import Money
from PIL import Image
from stdimage.models import StdImageField, StdImageFieldFile
from common.currency import currency_code_default
@@ -127,7 +128,7 @@ def extract_int(
return ref_int
def generateTestKey(test_name: Union[str, None]) -> str:
def generateTestKey(test_name: str | None) -> str:
"""Generate a test 'key' for a given test name. This must not have illegal chars as it will be used for dict lookup in a template.
Tests must be named such that they will have unique keys.
@@ -175,11 +176,48 @@ def constructPathString(path: list[str], max_chars: int = 250) -> str:
return pathstring
def getMediaUrl(filename):
def getMediaUrl(file: StdImageFieldFile | ImageFieldFile, name: str | None = None):
"""Return the qualified access path for the given file, under the media directory."""
if not isinstance(file, StdImageFieldFile):
raise ValueError('file_obj must be an instance of StdImageFieldFile')
if name is not None:
file = regenerate_imagefile(file, name)
if settings.STORAGE_TARGET == StorageBackends.S3:
return str(filename)
return os.path.join(MEDIA_URL, str(filename))
return str(file.url)
return os.path.join(MEDIA_URL, str(file.url))
def regenerate_imagefile(_file, _name: str):
"""Regenerate a file object for a given variation name.
Arguments:
_file: Original file object
_name: Name of the variation (e.g. 'thumbnail', 'preview')
"""
name = _file.field.attr_class.get_variation_name(_file.name, _name)
return ImageFieldFile(_file.instance, _file, name) # type: ignore
def image2name(img_obj: StdImageField, do_preview: bool, do_thumbnail: bool):
"""Convert an image object to a filename string.
Arguments:
img_obj: Image object
do_preview: Return preview image name
do_thumbnail: Return thumbnail image name
"""
def extract(ref: str):
return None if not hasattr(img_obj, ref) else getattr(img_obj, ref).name
if not img_obj:
return None
elif do_preview:
return extract('preview')
elif do_thumbnail:
return extract('thumbnail')
else:
return img_obj.name
def getStaticUrl(filename):
@@ -1,6 +1,6 @@
"""Code for managing email functionality in InvenTree."""
from typing import Optional, Union
from typing import Optional
from django.conf import settings
@@ -8,7 +8,6 @@ import structlog
from allauth.account.models import EmailAddress
import InvenTree.ready
import InvenTree.tasks
from common.models import Priority, issue_mail
logger = structlog.get_logger('inventree')
@@ -64,7 +63,7 @@ def is_email_configured() -> bool:
def send_email(
subject: str,
body: str,
recipients: Union[str, list],
recipients: str | list,
from_email: Optional[str] = None,
html_message=None,
prio: Priority = Priority.NORMAL,
@@ -1,8 +1,9 @@
"""Provides helper mixins that are used throughout the InvenTree project."""
import inspect
from collections.abc import Callable
from pathlib import Path
from typing import Any, Callable
from typing import Any
from django.conf import settings
from django.core.cache import cache
@@ -35,10 +35,6 @@ def get_type_str(type_obj):
if type_obj.__module__ == 'builtins':
return type_obj.__name__
# in python3.9, typing.Union has no __name__
if not hasattr(type_obj, '__module__') or not hasattr(type_obj, '__name__'):
return str(type_obj)
return f'{type_obj.__module__}.{type_obj.__name__}'
@@ -1,7 +1,7 @@
"""Extended schema generator."""
from pathlib import Path
from typing import TypeVar, Union
from typing import TypeVar
from django.conf import settings
@@ -26,7 +26,7 @@ def prep_name(ref):
return f'{dja_ref_prefix}.{ref}'
def sub_component_name(name: T) -> Union[T, str]:
def sub_component_name(name: T) -> T | str:
"""Clean up component references."""
if not isinstance(name, str):
return name
+61 -3
View File
@@ -1,8 +1,9 @@
"""Generic models which provide extra functionality over base Django model types."""
from collections.abc import Callable
from datetime import datetime
from string import Formatter
from typing import Optional
from typing import Any, Optional
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
@@ -19,6 +20,7 @@ from django_q.models import Task
from error_report.models import Error
from mptt.exceptions import InvalidMove
from mptt.models import MPTTModel, TreeForeignKey
from stdimage.models import StdImageField
import common.settings
import InvenTree.exceptions
@@ -164,10 +166,14 @@ class MetadataMixin(models.Model):
abstract = True
def save(self, *args, **kwargs):
def save(self, force_insert=False, force_update=False, *args, **kwargs):
"""Save the model instance, and perform validation on the metadata field."""
self.validate_metadata()
super().save(*args, **kwargs)
if len(args) > 0:
raise TypeError(
'save() takes no positional arguments anymore'
) # pragma: no cover
super().save(force_insert=force_insert, force_update=force_update, **kwargs)
def clean(self, *args, **kwargs):
"""Perform model validation on the metadata field."""
@@ -1293,3 +1299,55 @@ def after_error_logged(sender, instance: Error, created: bool, **kwargs):
'link': url,
},
)
class InvenTreeImageMixin(models.Model):
"""A mixin class for adding image functionality to a model class.
The following fields are added to any model which implements this mixin:
- image : An image field for storing an image
"""
IMAGE_RENAME: Callable | None = None
class Meta:
"""Metaclass options for this mixin.
Note: abstract must be true, as this is only a mixin, not a separate table
"""
abstract = True
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Custom init method for InvenTreeImageMixin to ensure IMAGE_RENAME is implemented."""
if self.IMAGE_RENAME is None:
raise NotImplementedError(
'IMAGE_RENAME must be implemented in the model class'
)
super().__init__(*args, **kwargs)
def rename_image(self, filename):
"""Rename the uploaded image file using the IMAGE_RENAME function."""
return self.IMAGE_RENAME(filename) # type: ignore
image = StdImageField(
upload_to=rename_image,
null=True,
blank=True,
variations={'thumbnail': (128, 128), 'preview': (256, 256)},
delete_orphans=False,
verbose_name=_('Image'),
)
def get_image_url(self):
"""Return the URL of the image for this object."""
if self.image:
return InvenTree.helpers.getMediaUrl(self.image)
return InvenTree.helpers.getBlankImage()
def get_thumbnail_url(self) -> str:
"""Return the URL of the image thumbnail for this object."""
if self.image:
return InvenTree.helpers.getMediaUrl(self.image, 'thumbnail')
return InvenTree.helpers.getBlankThumbnail()
+4 -6
View File
@@ -695,7 +695,7 @@ Ref: https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-OPTIONS
# connecting to the database server (such as a replica failover) don't sit and
# wait for possibly an hour or more, just tell the client something went wrong
# and let the client retry when they want to.
db_options = db_config.get('OPTIONS', db_config.get('options', None))
db_options = db_config.get('OPTIONS', db_config.get('options'))
if db_options is None:
db_options = {}
@@ -1428,9 +1428,7 @@ if SITE_URL:
GLOBAL_SETTINGS_OVERRIDES['INVENTREE_BASE_URL'] = SITE_URL
if len(GLOBAL_SETTINGS_OVERRIDES) > 0:
logger.info(
'INVE-I1: Global settings overrides: %s', str(GLOBAL_SETTINGS_OVERRIDES)
)
logger.info('INVE-I1: Global settings overrides: %s', GLOBAL_SETTINGS_OVERRIDES)
for key in GLOBAL_SETTINGS_OVERRIDES:
# Set the global setting
logger.debug('- Override value for %s = ********', key)
@@ -1469,9 +1467,9 @@ FLAGS = {
CUSTOM_FLAGS = get_setting('INVENTREE_FLAGS', 'flags', None, typecast=dict)
if CUSTOM_FLAGS: # pragma: no cover
if not isinstance(CUSTOM_FLAGS, dict):
logger.error('Invalid custom flags, must be valid dict: %s', str(CUSTOM_FLAGS))
logger.error('Invalid custom flags, must be valid dict: %s', CUSTOM_FLAGS)
else:
logger.info('Custom flags: %s', str(CUSTOM_FLAGS))
logger.info('Custom flags: %s', CUSTOM_FLAGS)
FLAGS.update(CUSTOM_FLAGS)
# Magic login django-sesame
+4 -3
View File
@@ -4,9 +4,10 @@ import json
import os
import re
import warnings
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Callable, Optional
from typing import Optional
from django.conf import settings
from django.contrib.auth import get_user_model
@@ -626,7 +627,7 @@ def update_exchange_rates(force: bool = False):
except (AppRegistryNotReady, OperationalError, ProgrammingError):
logger.warning('Could not update exchange rates - database not ready')
except Exception as e: # pragma: no cover
logger.exception('Error updating exchange rates: %s', str(type(e)))
logger.exception('Error updating exchange rates: %s', type(e))
@tracer.start_as_current_span('run_backup')
@@ -716,7 +717,7 @@ def check_for_migrations(force: bool = False, reload_registry: bool = True) -> b
# Log open migrations
for migration in plan:
logger.info('- %s', str(migration[0]))
logger.info('- %s', migration[0])
# Set the application to maintenance mode - no access from now on.
set_maintenance_mode(True)
+1 -1
View File
@@ -576,7 +576,7 @@ class GeneralApiTests(InvenTreeAPITestCase):
with TemporaryDirectory() as tmp: # type: ignore[no-matching-overload]
sample_file = Path(tmp, 'temp.txt')
sample_file.write_text('abc')
sample_file.write_text('abc', 'utf-8')
# File is not a json
with self.assertLogs(logger='inventree', level='ERROR') as log:
@@ -173,7 +173,7 @@ class MiddlewareTests(InvenTreeTestCase):
SITE_URL='https://testserver', CSRF_TRUSTED_ORIGINS=['https://testserver']
):
response = self.client.get(
reverse('web'), HTTP_HOST='otherhost.example.com'
reverse('web'), headers={'host': 'otherhost.example.com'}
)
self.assertContains(response, 'INVE-E7: The visited path', status_code=500)
@@ -199,7 +199,9 @@ class MiddlewareTests(InvenTreeTestCase):
SITE_URL='https://testserver:8000',
CSRF_TRUSTED_ORIGINS=['https://testserver:8000'],
):
response = self.client.get(reverse('web'), HTTP_HOST='testserver:8008')
response = self.client.get(
reverse('web'), headers={'host': 'testserver:8008'}
)
self.do_positive_test(response)
# Try again with strict protocol check
@@ -208,7 +210,9 @@ class MiddlewareTests(InvenTreeTestCase):
CSRF_TRUSTED_ORIGINS=['https://testserver:8000'],
SITE_LAX_PROTOCOL_CHECK=False,
):
response = self.client.get(reverse('web'), HTTP_HOST='testserver:8008')
response = self.client.get(
reverse('web'), headers={'host': 'testserver:8008'}
)
self.assertContains(response, 'INVE-E7: The visited path', status_code=500)
def test_site_url_checks_multi(self):
+15 -3
View File
@@ -23,6 +23,7 @@ from djmoney.contrib.exchange.models import Rate, convert_money
from djmoney.money import Money
from maintenance_mode.core import get_maintenance_mode, set_maintenance_mode
from sesame.utils import get_user
from stdimage.models import StdImageFieldFile
import InvenTree.conversion
import InvenTree.format
@@ -700,7 +701,16 @@ class TestHelpers(TestCase):
def testMediaUrl(self):
"""Test getMediaUrl."""
self.assertEqual(helpers.getMediaUrl('xx/yy.png'), '/media/xx/yy.png')
# Str should not work
with self.assertRaises(ValueError):
helpers.getMediaUrl('xx/yy.png') # type: ignore
# Correct usage
part = Part().image
self.assertEqual(
helpers.getMediaUrl(StdImageFieldFile(part, part, 'xx/yy.png')), # type: ignore
'/media/xx/yy.png',
)
def testDecimal2String(self):
"""Test decimal2string."""
@@ -994,12 +1004,14 @@ class TestSerialNumberExtraction(TestCase):
# Extract a range of values with a smaller range
with self.assertRaises(ValidationError) as exc:
e('11-50', 10, 1)
self.assertIn('Range quantity exceeds 10', str(exc))
self.assertIn(
'Group range 11-50 exceeds allowed quantity (10)', str(exc.exception)
)
# Test groups are not interpolated with alpha characters
with self.assertRaises(ValidationError) as exc:
e('1, A-2, 3+', 5, 1)
self.assertIn('Invalid group range: A-2', str(exc))
self.assertIn('Invalid group: A-2', str(exc.exception))
def test_combinations(self):
"""Test complex serial number combinations."""
+3 -2
View File
@@ -6,9 +6,10 @@ import json
import os
import re
import time
from collections.abc import Callable
from contextlib import contextmanager
from pathlib import Path
from typing import Callable, Optional, Union
from typing import Optional
from unittest import mock
from django.contrib.auth import get_user_model
@@ -731,7 +732,7 @@ class InvenTreeAPITestCase(
def run_output_test(
self,
url: str,
test_cases: list[Union[tuple[str, str], str]],
test_cases: list[tuple[str, str] | str],
additional_params: Optional[dict] = None,
assert_subset: bool = False,
assert_fnc: Optional[Callable] = None,
@@ -5,7 +5,6 @@ from django.core import validators
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
import pint
import pint.errors
from moneyed import CURRENCIES
+4 -4
View File
@@ -59,20 +59,20 @@ except Exception as exc:
def checkMinPythonVersion():
"""Check that the Python version is at least 3.9."""
"""Check that the Python version is at least 3.11."""
version = sys.version.split(' ')[0]
docs = 'https://docs.inventree.org/en/stable/start/intro/#python-requirements'
msg = f"""
InvenTree requires Python 3.9 or above - you are running version {version}.
InvenTree requires Python 3.11 or above - you are running version {version}.
- Refer to the InvenTree documentation for more information:
- {docs}
"""
if sys.version_info.major < 3:
if sys.version_info.major < 3: # noqa: UP036
raise RuntimeError(msg)
if sys.version_info.major == 3 and sys.version_info.minor < 9:
if sys.version_info.major == 3 and sys.version_info.minor < 11:
raise RuntimeError(msg)
print(f'Python version {version} - {sys.executable}')
+2 -4
View File
@@ -2,8 +2,6 @@
from __future__ import annotations
from typing import Optional
from django.contrib.auth.models import User
from django.db.models import F, Q
from django.urls import include, path
@@ -629,7 +627,7 @@ class BuildLineList(
'bom_item__reference',
]
def get_source_build(self) -> Optional[Build]:
def get_source_build(self) -> Build | None:
"""Return the target build for the BuildLine queryset."""
source_build = None
@@ -648,7 +646,7 @@ class BuildLineDetail(BuildLineMixin, OutputOptionsMixin, RetrieveUpdateDestroyA
output_options = BuildLineOutputOptions
def get_source_build(self) -> Optional[Build]:
def get_source_build(self) -> Build | None:
"""Return the target source location for the BuildLine queryset."""
return None
+3 -1
View File
@@ -1640,7 +1640,9 @@ class BuildConsumeTest(BuildAPITest):
data = {
'items': [
{'build_line': line.pk, 'stock_item': si.pk, 'quantity': 100}
for line, si in zip(self.build.build_lines.all(), self.stock_items)
for line, si in zip(
self.build.build_lines.all(), self.stock_items, strict=False
)
]
}
+14 -20
View File
@@ -14,7 +14,7 @@ from email.utils import make_msgid
from enum import Enum
from io import BytesIO
from secrets import compare_digest
from typing import Any, Optional, Union
from typing import Any, Optional
from django.apps import apps
from django.conf import settings as django_settings
@@ -67,7 +67,7 @@ from InvenTree.version import inventree_identifier
logger = structlog.get_logger('inventree')
class RenderMeta(enums.ChoicesMeta):
class RenderMeta(enums.ChoicesType):
"""Metaclass for rendering choices."""
choice_fnc = None
@@ -239,9 +239,7 @@ class BaseInvenTreeSetting(models.Model):
missing_keys = set(settings_keys) - set(existing_keys)
if len(missing_keys) > 0:
logger.info(
'Building %s default values for %s', len(missing_keys), str(cls)
)
logger.info('Building %s default values for %s', len(missing_keys), cls)
cls.objects.bulk_create([
cls(key=key, value=cls.get_setting_default(key), **kwargs)
for key in missing_keys
@@ -249,7 +247,7 @@ class BaseInvenTreeSetting(models.Model):
])
except Exception as exc:
logger.exception(
'Failed to build default values for %s (%s)', str(cls), str(type(exc))
'Failed to build default values for %s (%s)', cls, type(exc)
)
def _call_settings_function(self, reference: str, args, kwargs):
@@ -330,7 +328,7 @@ class BaseInvenTreeSetting(models.Model):
cls,
*,
exclude_hidden=False,
settings_definition: Union[dict[str, SettingsKeyType], None] = None,
settings_definition: dict[str, SettingsKeyType] | None = None,
**kwargs,
):
"""Return a list of "all" defined settings.
@@ -392,7 +390,7 @@ class BaseInvenTreeSetting(models.Model):
cls,
*,
exclude_hidden=False,
settings_definition: Union[dict[str, SettingsKeyType], None] = None,
settings_definition: dict[str, SettingsKeyType] | None = None,
**kwargs,
):
"""Return a dict of "all" defined global settings.
@@ -419,7 +417,7 @@ class BaseInvenTreeSetting(models.Model):
cls,
*,
exclude_hidden=False,
settings_definition: Union[dict[str, SettingsKeyType], None] = None,
settings_definition: dict[str, SettingsKeyType] | None = None,
**kwargs,
):
"""Check if all required settings are set by definition.
@@ -647,9 +645,7 @@ class BaseInvenTreeSetting(models.Model):
and not key.startswith('_')
):
logger.warning(
"get_setting: Setting key '%s' is not defined for class %s",
key,
str(cls),
"get_setting: Setting key '%s' is not defined for class %s", key, cls
)
# If no backup value is specified, attempt to retrieve a "default" value
@@ -693,9 +689,7 @@ class BaseInvenTreeSetting(models.Model):
and not key.startswith('_')
):
logger.warning(
"set_setting: Setting key '%s' is not defined for class %s",
key,
str(cls),
"set_setting: Setting key '%s' is not defined for class %s", key, cls
)
if change_user is not None and not change_user.is_staff:
@@ -734,7 +728,7 @@ class BaseInvenTreeSetting(models.Model):
return
except Exception as exc: # pragma: no cover
logger.exception(
"Error setting setting '%s' for %s: %s", key, str(cls), str(type(exc))
"Error setting setting '%s' for %s: %s", key, cls, type(exc)
)
return
@@ -753,7 +747,7 @@ class BaseInvenTreeSetting(models.Model):
if attempts > 0:
# Try again
logger.info(
"Duplicate setting key '%s' for %s - trying again", key, str(cls)
"Duplicate setting key '%s' for %s - trying again", key, cls
)
cls.set_setting(
key,
@@ -770,7 +764,7 @@ class BaseInvenTreeSetting(models.Model):
except Exception as exc: # pragma: no cover
# Some other error
logger.exception(
"Error setting setting '%s' for %s: %s", key, str(cls), str(type(exc))
"Error setting setting '%s' for %s: %s", key, cls, type(exc)
)
key = models.CharField(
@@ -2659,7 +2653,7 @@ class EmailMessage(models.Model):
direction = models.CharField(
max_length=50, blank=True, null=True, choices=EmailDirection.choices
)
priority = models.IntegerField(verbose_name=_('Prioriy'), choices=Priority.choices)
priority = models.IntegerField(verbose_name=_('Prioriy'), choices=Priority)
delivery_options = models.JSONField(
blank=True,
null=True,
@@ -2741,7 +2735,7 @@ def issue_mail(
subject: str,
body: str,
from_email: str,
recipients: Union[str, list],
recipients: str | list,
fail_silently: bool = False,
html_message=None,
prio: Priority = Priority.NORMAL,
@@ -132,7 +132,7 @@ def trigger_notification(obj: Model, category: str = '', obj_ref: str = 'pk', **
logger.info(
"Notification '%s' has recently been sent for '%s' - SKIPPING",
category,
str(obj),
obj,
)
return
@@ -15,7 +15,6 @@ from jinja2 import Template
import build.validators
import common.currency
import common.models
import common.validators
import order.validators
import report.helpers
+5 -4
View File
@@ -1,7 +1,8 @@
"""Types for settings."""
import sys
from typing import Any, Callable, TypedDict, Union
from collections.abc import Callable
from typing import Any, TypedDict
if sys.version_info >= (3, 11):
from typing import NotRequired # pragma: no cover
@@ -36,9 +37,9 @@ class SettingsKeyType(TypedDict, total=False):
name: str
description: str
units: str
validator: Union[Callable, list[Callable], tuple[Callable]]
default: Union[Callable, Any]
choices: Union[list[tuple[str, str]], Callable[[], list[tuple[str, str]]]]
validator: Callable | list[Callable] | tuple[Callable]
default: Callable | Any
choices: list[tuple[str, str]] | Callable[[], list[tuple[str, str]]]
hidden: bool
before_save: Callable[..., None]
after_save: Callable[..., None]
+4 -4
View File
@@ -1105,7 +1105,7 @@ class WebhookMessageTests(TestCase):
def test_bad_token(self):
"""Test that a wrong token is not working."""
response = self.client.post(
self.url, content_type=CONTENT_TYPE_JSON, HTTP_TOKEN='1234567fghj'
self.url, content_type=CONTENT_TYPE_JSON, headers={'token': '1234567fghj'}
)
assert response.status_code == HTTPStatus.FORBIDDEN
@@ -1128,7 +1128,7 @@ class WebhookMessageTests(TestCase):
self.url,
data="{'this': 123}",
content_type=CONTENT_TYPE_JSON,
HTTP_TOKEN=str(self.endpoint_def.token),
headers={'token': str(self.endpoint_def.token)},
)
assert response.status_code == HTTPStatus.NOT_ACCEPTABLE
@@ -1176,7 +1176,7 @@ class WebhookMessageTests(TestCase):
response = self.client.post(
self.url,
content_type=CONTENT_TYPE_JSON,
HTTP_TOKEN='68MXtc/OiXdA5e2Nq9hATEVrZFpLb3Zb0oau7n8s31I=',
headers={'token': '68MXtc/OiXdA5e2Nq9hATEVrZFpLb3Zb0oau7n8s31I='},
)
assert response.status_code == HTTPStatus.OK
@@ -1191,7 +1191,7 @@ class WebhookMessageTests(TestCase):
self.url,
data={'this': 'is a message'},
content_type=CONTENT_TYPE_JSON,
HTTP_TOKEN=str(self.endpoint_def.token),
headers={'token': str(self.endpoint_def.token)},
)
assert response.status_code == HTTPStatus.OK
+1 -2
View File
@@ -1,7 +1,6 @@
"""Validation helpers for common models."""
import re
from typing import Union
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@@ -107,7 +106,7 @@ def validate_email_domains(setting):
raise ValidationError(_(f'Invalid domain name: {domain}'))
def validate_icon(name: Union[str, None]):
def validate_icon(name: str | None):
"""Validate the provided icon name, and ignore if empty."""
if name == '' or name is None:
return
@@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-11-04 20:59
import InvenTree.models
import stdimage.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('company', '0075_company_tax_id'),
]
operations = [
migrations.AlterField(
model_name='company',
name='image',
field=stdimage.models.StdImageField(blank=True, force_min_size=False, null=True, upload_to=InvenTree.models.InvenTreeImageMixin.rename_image, variations={'preview': (256, 256), 'thumbnail': (128, 128)}, verbose_name='Image'),
),
]
+3 -22
View File
@@ -16,7 +16,6 @@ from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext_lazy as __
from moneyed import CURRENCIES
from stdimage.models import StdImageField
from taggit.managers import TaggableManager
import common.currency
@@ -80,6 +79,7 @@ class Company(
InvenTree.models.InvenTreeAttachmentMixin,
InvenTree.models.InvenTreeNotesMixin,
report.mixins.InvenTreeReportMixin,
InvenTree.models.InvenTreeImageMixin,
InvenTree.models.InvenTreeMetadataModel,
):
"""A Company object represents an external company.
@@ -109,6 +109,8 @@ class Company(
tax_id: Tax ID for the company
"""
IMAGE_RENAME = rename_company_image
class Meta:
"""Metaclass defines extra model options."""
@@ -185,15 +187,6 @@ class Company(
max_length=2000,
)
image = StdImageField(
upload_to=rename_company_image,
null=True,
blank=True,
variations={'thumbnail': (128, 128), 'preview': (256, 256)},
delete_orphans=True,
verbose_name=_('Image'),
)
active = models.BooleanField(
default=True, verbose_name=_('Active'), help_text=_('Is this company active?')
)
@@ -269,18 +262,6 @@ class Company(
"""Get the web URL for the detail view for this Company."""
return InvenTree.helpers.pui_url(f'/purchasing/manufacturer/{self.id}')
def get_image_url(self):
"""Return the URL of the image for this company."""
if self.image:
return InvenTree.helpers.getMediaUrl(self.image.url)
return InvenTree.helpers.getBlankImage()
def get_thumbnail_url(self):
"""Return the URL for the thumbnail image for this Company."""
if self.image:
return InvenTree.helpers.getMediaUrl(self.image.thumbnail.url)
return InvenTree.helpers.getBlankThumbnail()
@property
def parts(self):
"""Return SupplierPart objects which are supplied or manufactured by this company."""
@@ -1,6 +1,6 @@
"""Classes and functions for plugin controlled object state transitions."""
from typing import Callable
from collections.abc import Callable
from django.db.models import Model
+1 -1
View File
@@ -297,7 +297,7 @@ class DataImportSession(models.Model):
# Iterate through each "row" in the data file, and create a new DataImportRow object
for idx, row in enumerate(df):
row_data = dict(zip(headers, row))
row_data = dict(zip(headers, row, strict=False))
# Skip completely empty rows
if not any(row_data.values()):
-2
View File
@@ -16,8 +16,6 @@ def import_data(session_id: int):
Attempt to load data from the provided file, and potentially handle any errors.
"""
import importer.models
import importer.operations
import importer.status_codes
try:
session = importer.models.DataImportSession.objects.get(pk=session_id)
@@ -1,6 +1,6 @@
"""Base machine type/base driver."""
from typing import TYPE_CHECKING, Any, Literal, TypedDict, Union
from typing import TYPE_CHECKING, Any, Literal, TypedDict
from generic.states import StatusCode
from InvenTree.helpers_mixin import (
@@ -63,10 +63,10 @@ class MachineProperty(TypedDict, total=False):
"""
key: str
value: Union[str, bool, int, float]
value: str | bool | int | float
group: str
type: MachinePropertyType
max_progress: Union[int, None]
max_progress: int | None
class BaseDriver(
@@ -160,7 +160,7 @@ class BaseDriver(
return registry.get_machines(driver=self, **kwargs)
def handle_error(self, error: Union[Exception, str]):
def handle_error(self, error: Exception | str):
"""Handle driver error.
Arguments:
@@ -171,7 +171,7 @@ class BaseDriver(
# --- state getters/setters
@property
def errors(self) -> list[Union[str, Exception]]:
def errors(self) -> list[str | Exception]:
"""List of driver errors."""
return self.get_shared_state('errors', [])
@@ -343,7 +343,7 @@ class BaseMachineType(
self.handle_error(e)
# --- helper functions
def handle_error(self, error: Union[Exception, str]):
def handle_error(self, error: Exception | str):
"""Helper function for capturing errors with the machine.
Arguments:
@@ -475,7 +475,7 @@ class BaseMachineType(
self.set_shared_state('restart_required', value)
@property
def errors(self) -> list[Union[str, Exception]]:
def errors(self) -> list[str | Exception]:
"""List of machine errors."""
return self.get_shared_state('errors', [])
@@ -1,6 +1,6 @@
"""Label printing machine type."""
from typing import Union, cast
from typing import cast
from django.contrib.auth.models import AnonymousUser
from django.db import models
@@ -59,7 +59,7 @@ class LabelPrinterBaseDriver(BaseDriver):
label: LabelTemplate,
items: QuerySet[models.Model],
**kwargs,
) -> Union[JsonResponse, None]:
) -> JsonResponse | None:
"""Print one or more labels with the provided template and items.
Arguments:
@@ -155,7 +155,7 @@ class LabelPrinterBaseDriver(BaseDriver):
def render_to_png(
self, label: LabelTemplate, item: models.Model, **kwargs
) -> Union[Image, None]:
) -> Image | None:
"""Helper method to render a label to PNG format for a specific item.
Arguments:
+8 -8
View File
@@ -1,7 +1,7 @@
"""Machine registry."""
import functools
from typing import Any, Optional, Union, cast
from typing import Any, Optional, cast
from uuid import UUID
from django.db.utils import IntegrityError, OperationalError, ProgrammingError
@@ -114,16 +114,16 @@ class MachineRegistry(
self._hash = None
@property
def errors(self) -> list[Union[str, Exception]]:
def errors(self) -> list[str | Exception]:
"""List of registry errors."""
return cast(list[Union[str, Exception]], self.get_shared_state('errors', []))
return cast(list[str | Exception], self.get_shared_state('errors', []))
@property
def is_ready(self) -> bool:
"""Check if the machine registry is ready."""
return self.ready
def handle_error(self, error: Union[Exception, str]):
def handle_error(self, error: Exception | str):
"""Helper function for capturing errors with the machine registry."""
if error not in self.errors:
self.set_shared_state('errors', [*self.errors, error])
@@ -384,7 +384,7 @@ class MachineRegistry(
return list(self.machine_types.values())
@machine_registry_entrypoint()
def get_machine(self, pk: Union[str, UUID]) -> Optional[BaseMachineType]:
def get_machine(self, pk: str | UUID) -> Optional[BaseMachineType]:
"""Get machine from registry by pk."""
return self.machines.get(str(pk), None)
@@ -454,7 +454,7 @@ class MachineRegistry(
try:
reg_hash = get_global_setting('_MACHINE_REGISTRY_HASH', '', create=False)
except Exception as exc:
logger.exception('Failed to get machine registry hash: %s', str(exc))
logger.exception('Failed to get machine registry hash: %s', exc)
return False
if reg_hash and reg_hash != self._hash:
@@ -480,12 +480,12 @@ class MachineRegistry(
if old_hash != self._hash:
try:
logger.info('Updating machine registry hash: %s', str(self._hash))
logger.info('Updating machine registry hash: %s', self._hash)
set_global_setting('_MACHINE_REGISTRY_HASH', self._hash)
except (IntegrityError, OperationalError, ProgrammingError):
pass
except Exception as exc:
logger.exception('Failed to update machine registry hash: %s', str(exc))
logger.exception('Failed to update machine registry hash: %s', exc)
@machine_registry_entrypoint()
def call_machine_function(
+2 -4
View File
@@ -1,7 +1,5 @@
"""Serializers for the machine app."""
from typing import Union
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -96,7 +94,7 @@ class MachineConfigSerializer(serializers.ModelSerializer):
return status.value
return -1
def get_status_model(self, obj: MachineConfig) -> Union[str, None]:
def get_status_model(self, obj: MachineConfig) -> str | None:
"""Textual machine status name if available, else None."""
if obj.machine and obj.machine.MACHINE_STATUS:
return obj.machine.MACHINE_STATUS.__name__
@@ -171,7 +169,7 @@ class BaseMachineClassSerializer(serializers.Serializer):
"""File that contains the class definition."""
return obj.get_provider_file()
def get_provider_plugin(self, obj: ClassProviderMixin) -> Union[dict, None]:
def get_provider_plugin(self, obj: ClassProviderMixin) -> dict | None:
"""Plugin(s) that contain(s) the class definition."""
plugin = obj.get_provider_plugin()
if plugin:
+2 -3
View File
@@ -1,7 +1,6 @@
"""Background tasks for the 'order' app."""
from datetime import datetime, timedelta
from typing import Union
from django.contrib.auth.models import Group, User
from django.db import transaction
@@ -105,7 +104,7 @@ def check_overdue_purchase_orders():
@tracer.start_as_current_span('notify_overdue_sales_order')
def notify_overdue_sales_order(so: order.models.SalesOrder) -> None:
"""Notify appropriate users that a SalesOrder has just become 'overdue'."""
targets: list[Union[User, Group, Owner]] = []
targets: list[User | Group | Owner] = []
if so.created_by:
targets.append(so.created_by)
@@ -172,7 +171,7 @@ def check_overdue_sales_orders():
@tracer.start_as_current_span('notify_overdue_return_order')
def notify_overdue_return_order(ro: order.models.ReturnOrder) -> None:
"""Notify appropriate users that a ReturnOrder has just become 'overdue'."""
targets: list[Union[User, Group, Owner]] = []
targets: list[User | Group | Owner] = []
if ro.created_by:
targets.append(ro.created_by)
@@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-11-04 20:59
import InvenTree.models
import stdimage.models
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('part', '0142_remove_part_last_stocktake_remove_partstocktake_note_and_more'),
]
operations = [
migrations.AlterField(
model_name='part',
name='image',
field=stdimage.models.StdImageField(blank=True, force_min_size=False, null=True, upload_to=InvenTree.models.InvenTreeImageMixin.rename_image, variations={'preview': (256, 256), 'thumbnail': (128, 128)}, verbose_name='Image'),
),
]
+6 -27
View File
@@ -10,7 +10,7 @@ import os
import re
from datetime import timedelta
from decimal import Decimal, InvalidOperation
from typing import Optional, cast
from typing import cast
from django.conf import settings
from django.contrib.auth.models import User
@@ -36,12 +36,10 @@ from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money
from mptt.managers import TreeManager
from mptt.models import TreeForeignKey
from stdimage.models import StdImageField
from taggit.managers import TaggableManager
import common.currency
import common.models
import common.settings
import InvenTree.conversion
import InvenTree.fields
import InvenTree.helpers
@@ -394,15 +392,15 @@ class PartReportContext(report.mixins.BaseReportContext):
"""
bom_items: report.mixins.QuerySet[BomItem]
category: Optional[PartCategory]
category: PartCategory | None
description: str
IPN: Optional[str]
IPN: str | None
name: str
parameters: dict[str, str]
part: Part
qr_data: str
qr_url: str
revision: Optional[str]
revision: str | None
test_template_list: report.mixins.QuerySet[PartTestTemplate]
test_templates: dict[str, PartTestTemplate]
@@ -414,6 +412,7 @@ class Part(
InvenTree.models.InvenTreeBarcodeMixin,
InvenTree.models.InvenTreeNotesMixin,
report.mixins.InvenTreeReportMixin,
InvenTree.models.InvenTreeImageMixin,
InvenTree.models.MetadataMixin,
InvenTree.models.InvenTreeTree,
):
@@ -461,6 +460,7 @@ class Part(
"""
NODE_PARENT_KEY = 'variant_of'
IMAGE_RENAME = rename_part_image
objects = PartManager()
@@ -945,18 +945,6 @@ class Part(
"""Return the web URL for viewing this part."""
return helpers.pui_url(f'/part/{self.id}')
def get_image_url(self):
"""Return the URL of the image for this part."""
if self.image:
return helpers.getMediaUrl(self.image.url)
return helpers.getBlankImage()
def get_thumbnail_url(self) -> str:
"""Return the URL of the image thumbnail for this part."""
if self.image:
return helpers.getMediaUrl(self.image.thumbnail.url)
return helpers.getBlankThumbnail()
def validate_unique(self, exclude=None):
"""Validate that this Part instance is 'unique'.
@@ -1127,15 +1115,6 @@ class Part(
max_length=2000,
)
image = StdImageField(
upload_to=rename_part_image,
null=True,
blank=True,
variations={'thumbnail': (128, 128), 'preview': (256, 256)},
delete_orphans=False,
verbose_name=_('Image'),
)
default_location = TreeForeignKey(
'stock.StockLocation',
on_delete=models.SET_NULL,
+1 -1
View File
@@ -217,7 +217,7 @@ def check_stale_stock():
logger.error(
'Error scheduling stale stock notification for user %s: %s',
user.username,
str(e),
e,
)
logger.info(
+2 -4
View File
@@ -7,8 +7,6 @@ from djmoney.contrib.exchange.models import convert_money
from djmoney.money import Money
import common.currency
import common.models
import common.settings
import company.models
import order.models
import part.models
@@ -463,11 +461,11 @@ class PartPricingTests(InvenTreeTestCase):
p.delete()
# Check that the PartPricing object has been deleted
self.assertFalse(part.models.PartPricing.objects.filter(part=p).exists())
self.assertFalse(part.models.PartPricing.objects.filter(part_id=p.pk).exists())
# Try to update pricing (should fail gracefully as the Part has been deleted)
p.schedule_pricing_update(create=False)
self.assertFalse(part.models.PartPricing.objects.filter(part=p).exists())
self.assertFalse(part.models.PartPricing.objects.filter(part_id=p.pk).exists())
@override_settings(TESTING_PRICING=True)
def test_multi_level_bom(self):
@@ -2,8 +2,6 @@
from __future__ import annotations
from typing import Optional
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
@@ -115,7 +113,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
return fields.get(key, backup_value)
def get_part(self) -> Optional[Part]:
def get_part(self) -> Part | None:
"""Extract the Part object from the barcode fields."""
# TODO: Implement this
return None
@@ -130,7 +128,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
"""Return the supplier part number from the barcode fields."""
return self.get_field_value(self.SUPPLIER_PART_NUMBER)
def get_supplier_part(self) -> Optional[SupplierPart]:
def get_supplier_part(self) -> SupplierPart | None:
"""Return the SupplierPart object for the scanned barcode.
Returns:
@@ -174,7 +172,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
"""Return the manufacturer part number from the barcode fields."""
return self.get_field_value(self.MANUFACTURER_PART_NUMBER)
def get_manufacturer_part(self) -> Optional[ManufacturerPart]:
def get_manufacturer_part(self) -> ManufacturerPart | None:
"""Return the ManufacturerPart object for the scanned barcode.
Returns:
@@ -215,7 +213,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
"""Return the supplier order number from the barcode fields."""
return self.get_field_value(self.SUPPLIER_ORDER_NUMBER)
def get_purchase_order(self) -> Optional[PurchaseOrder]:
def get_purchase_order(self) -> PurchaseOrder | None:
"""Extract the PurchaseOrder object from the barcode fields.
Inspect the customer_order_number and supplier_order_number fields,
@@ -262,7 +260,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
'extract_barcode_fields must be implemented by each plugin'
)
def scan(self, barcode_data: str) -> Optional[dict]:
def scan(self, barcode_data: str) -> dict | None:
"""Perform a generic 'scan' operation on a supplier barcode.
The supplier barcode may provide sufficient information to match against
@@ -321,7 +319,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
location=None,
auto_allocate: bool = True,
**kwargs,
) -> Optional[dict]:
) -> dict | None:
"""Attempt to receive an item against a PurchaseOrder via barcode scanning.
Arguments:
@@ -432,7 +430,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
return response
def get_supplier(self, cache: bool = False) -> Optional[Company]:
def get_supplier(self, cache: bool = False) -> Company | None:
"""Get the supplier for the SUPPLIER_ID set in the plugin settings.
If it's not defined, try to guess it and set it if possible.
@@ -1,7 +1,7 @@
"""Plugin class for custom data exporting."""
from collections import OrderedDict
from typing import Optional, Union
from typing import Optional
from django.contrib.auth.models import User
from django.db.models import QuerySet
@@ -109,9 +109,7 @@ class DataExportMixin:
# The default implementation simply serializes the queryset
return serializer_class(queryset, many=True, exporting=True).data
def get_export_options_serializer(
self, **kwargs
) -> Union[serializers.Serializer, None]:
def get_export_options_serializer(self, **kwargs) -> serializers.Serializer | None:
"""Return a serializer class with dynamic export options for this plugin.
Returns:
@@ -1,7 +1,5 @@
"""Plugin mixin classes for label plugins."""
from typing import Union
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
@@ -223,7 +221,7 @@ class LabelPrintingMixin:
def get_printing_options_serializer(
self, request: Request, *args, **kwargs
) -> Union[serializers.Serializer, None]:
) -> serializers.Serializer | None:
"""Return a serializer class instance with dynamic printing options.
Arguments:
@@ -1,6 +1,6 @@
"""Functions for processing emails."""
from typing import Optional, Union
from typing import Optional
from django.core.mail.message import EmailMessage, EmailMultiAlternatives
@@ -12,7 +12,7 @@ from plugin.registry import registry
def process_mail_out(
email_messages: list[Union[EmailMessage, EmailMultiAlternatives]],
email_messages: list[EmailMessage | EmailMultiAlternatives],
) -> bool:
"""Process email messages with plugins.
@@ -99,7 +99,7 @@ class AutoIssueOrdersPlugin(ScheduleMixin, SettingsMixin, InvenTreePlugin):
try:
getattr(order, func_name)()
except Exception as e:
logger.error('Failed to issue order %s: %s', order.pk, str(e))
logger.error('Failed to issue order %s: %s', order.pk, e)
def auto_issue_build_orders(self):
"""Automatically issue build orders on the assigned target date."""
@@ -4,12 +4,8 @@ from django.utils.translation import gettext_lazy as _
import structlog
import common.models
import common.notifications
import InvenTree.helpers
import InvenTree.helpers_email
import InvenTree.helpers_model
import InvenTree.tasks
from part.models import Part
from plugin import InvenTreePlugin
from plugin.mixins import EventMixin, SettingsMixin
@@ -219,7 +219,7 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
)
html += cell
except Exception as exc:
logger.exception('Error rendering label: %s', str(exc))
logger.exception('Error rendering label: %s', exc)
html += """
<div class='label-sheet-cell-error'></div>
"""
+6 -6
View File
@@ -99,7 +99,7 @@ def get_install_info(packagename: str) -> dict:
output = error.output.decode('utf-8')
info['error'] = output
logger.exception('Plugin lookup failed: %s', str(output))
logger.exception('Plugin lookup failed: %s', output)
except Exception:
log_error('get_install_info', scope='pip')
@@ -131,7 +131,7 @@ def install_plugins_file():
pf = settings.PLUGIN_FILE
if not pf or not pf.exists():
logger.warning('Plugin file %s does not exist', str(pf))
logger.warning('Plugin file %s does not exist', pf)
return
cmd = ['install', '--disable-pip-version-check', '-U', '-r', str(pf)]
@@ -140,7 +140,7 @@ def install_plugins_file():
pip_command(*cmd)
except subprocess.CalledProcessError as error:
output = error.output.decode('utf-8')
logger.exception('Plugin file installation failed: %s', str(output))
logger.exception('Plugin file installation failed: %s', output)
log_error('install_plugins_file', scope='pip')
return False
except Exception as exc:
@@ -174,7 +174,7 @@ def update_plugins_file(install_name, full_package=None, version=None, remove=Fa
pf = settings.PLUGIN_FILE
if not pf or not pf.exists():
logger.warning('Plugin file %s does not exist', str(pf))
logger.warning('Plugin file %s does not exist', pf)
return
def compare_line(line: str):
@@ -186,7 +186,7 @@ def update_plugins_file(install_name, full_package=None, version=None, remove=Fa
with pf.open(mode='r') as f:
lines = f.readlines()
except Exception as exc:
logger.exception('Failed to read plugins file: %s', str(exc))
logger.exception('Failed to read plugins file: %s', exc)
log_error('update_plugins_file', scope='plugins')
return
@@ -223,7 +223,7 @@ def update_plugins_file(install_name, full_package=None, version=None, remove=Fa
if not line.endswith('\n'):
f.write('\n')
except Exception as exc:
logger.exception('Failed to add plugin to plugins file: %s', str(exc))
logger.exception('Failed to add plugin to plugins file: %s', exc)
log_error('update_plugins_file', scope='plugins')
+5 -5
View File
@@ -6,7 +6,7 @@ from datetime import datetime
from distutils.sysconfig import get_python_lib # type: ignore[import]
from importlib.metadata import PackageNotFoundError, metadata
from pathlib import Path
from typing import Optional, Union
from typing import Optional
from django.conf import settings
from django.utils.text import slugify
@@ -483,7 +483,7 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
@classmethod
@mark_final
def check_package_install_name(cls) -> Union[str, None]:
def check_package_install_name(cls) -> str | None:
"""Installable package name of the plugin.
e.g. if this plugin was installed via 'pip install <x>',
@@ -496,7 +496,7 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
@property
@mark_final
def package_install_name(self) -> Union[str, None]:
def package_install_name(self) -> str | None:
"""Installable package name of the plugin.
e.g. if this plugin was installed via 'pip install <x>',
@@ -610,7 +610,7 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
return url
def get_admin_source(self) -> Union[str, None]:
def get_admin_source(self) -> str | None:
"""Return a path to a JavaScript file which contains custom UI settings.
The frontend code expects that this file provides a function named 'renderPluginSettings'.
@@ -620,7 +620,7 @@ class InvenTreePlugin(VersionMixin, MixinBase, MetaBase):
return self.plugin_static_file(self.ADMIN_SOURCE)
def get_admin_context(self) -> Union[dict, None]:
def get_admin_context(self) -> dict | None:
"""Return a context dictionary for the admin panel settings.
This is an optional method which can be overridden by the plugin.
+7 -8
View File
@@ -14,7 +14,7 @@ from collections import OrderedDict
from importlib.machinery import SourceFileLoader
from pathlib import Path
from threading import Lock
from typing import Any, Optional, Union
from typing import Any, Optional
from django.apps import apps
from django.conf import settings
@@ -212,7 +212,7 @@ class PluginsRegistry:
return plg
def get_plugin_config(self, slug: str, name: Union[str, None] = None):
def get_plugin_config(self, slug: str, name: str | None = None):
"""Return the matching PluginConfig instance for a given plugin.
Arguments:
@@ -820,7 +820,7 @@ class PluginsRegistry:
logger.exception(
'[PLUGIN] Encountered an error with %s:\n%s',
getattr(error, 'path', None),
str(error),
error,
)
logger.debug('Finished plugin initialization')
@@ -980,9 +980,8 @@ class PluginsRegistry:
if old_hash != self.registry_hash:
try:
logger.info(
'Updating plugin registry hash: %s', str(self.registry_hash)
)
logger.info('Updating plugin registry hash: %s', self.registry_hash)
set_global_setting(
'_PLUGIN_REGISTRY_HASH', self.registry_hash, change_user=None
)
@@ -991,7 +990,7 @@ class PluginsRegistry:
pass
except Exception as exc:
# Some other exception, we want to know about it
logger.exception('Failed to update plugin registry hash: %s', str(exc))
logger.exception('Failed to update plugin registry hash: %s', exc)
def plugin_settings_keys(self):
"""A list of keys which are used to store plugin settings."""
@@ -1064,7 +1063,7 @@ class PluginsRegistry:
try:
reg_hash = get_global_setting('_PLUGIN_REGISTRY_HASH', '', create=False)
except Exception as exc:
logger.exception('Failed to retrieve plugin registry hash: %s', str(exc))
logger.exception('Failed to retrieve plugin registry hash: %s', exc)
return False
if reg_hash and reg_hash != self.registry_hash:
+1 -1
View File
@@ -111,7 +111,7 @@ def copy_plugin_static_files(slug, check_reload=True):
with item.open('rb') as src:
staticfiles_storage.save(destination_path, src)
logger.debug('- copied %s to %s', str(item), str(destination_path))
logger.debug('- copied %s to %s', item, destination_path)
copied += 1
if copied > 0:
+1 -1
View File
@@ -72,7 +72,7 @@ def report_page_size_default():
try:
page_size = get_global_setting('REPORT_DEFAULT_PAGE_SIZE', 'A4', create=False)
except Exception as exc:
logger.exception('Error getting default page size: %s', str(exc))
logger.exception('Error getting default page size: %s', exc)
page_size = 'A4'
return page_size
@@ -4,7 +4,6 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
import plugin.models
import plugin.serializers
import report.helpers
import report.models
@@ -5,7 +5,7 @@ import logging
import os
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from typing import Any, Optional, Union
from typing import Any, Optional
from django import template
from django.apps.registry import apps
@@ -323,18 +323,9 @@ def part_image(part: Part, preview: bool = False, thumbnail: bool = False, **kwa
"""
if type(part) is not Part:
raise TypeError(_('part_image tag requires a Part instance'))
part_img = part.image
if not part_img:
img = None
elif preview:
img = None if not hasattr(part.image, 'preview') else part_img.preview.name
elif thumbnail:
img = None if not hasattr(part.image, 'thumbnail') else part_img.thumbnail.name
else:
img = part.image.name
return uploaded_image(img, **kwargs)
return uploaded_image(
InvenTree.helpers.image2name(part.image, preview, thumbnail), **kwargs
)
@register.simple_tag()
@@ -369,18 +360,9 @@ def company_image(
"""
if type(company) is not Company:
raise TypeError(_('company_image tag requires a Company instance'))
cmp_img = company.image
if not cmp_img:
img = None
elif preview:
img = cmp_img.preview.name
elif thumbnail:
img = cmp_img.thumbnail.name
else:
img = cmp_img.name
return uploaded_image(img, **kwargs)
return uploaded_image(
InvenTree.helpers.image2name(company.image, preview, thumbnail), **kwargs
)
@register.simple_tag()
@@ -603,7 +585,7 @@ def render_currency(money, **kwargs):
@register.simple_tag
def create_currency(
amount: Union[str, int, float, Decimal], currency: Optional[str] = None, **kwargs
amount: str | int | float | Decimal, currency: Optional[str] = None, **kwargs
):
"""Create a Money object, with the provided amount and currency.
@@ -694,9 +676,9 @@ def render_html_text(text: str, **kwargs):
@register.simple_tag
def format_number(
number: Union[int, float, Decimal],
number: int | float | Decimal,
decimal_places: Optional[int] = None,
multiplier: Optional[Union[int, float, Decimal]] = None,
multiplier: Optional[int | float | Decimal] = None,
integer: bool = False,
leading: int = 0,
separator: Optional[str] = None,
@@ -154,6 +154,7 @@ class ReportTagTest(PartImageTestMixin, InvenTreeTestCase):
obj = Part.objects.create(name='test', description='test')
self.create_test_image()
obj.refresh_from_db()
report_tags.part_image(obj, preview=True)
report_tags.part_image(obj, thumbnail=True)
+1 -3
View File
@@ -1557,9 +1557,7 @@ class StockTrackingList(
deltas = item['deltas'] or {}
if key in deltas:
item['deltas'][f'{key}_detail'] = related_data.get(
deltas[key], None
)
item['deltas'][f'{key}_detail'] = related_data.get(deltas[key])
if page is not None:
return self.get_paginated_response(data)
+9 -10
View File
@@ -5,7 +5,6 @@ from __future__ import annotations
import os
from datetime import timedelta
from decimal import Decimal, InvalidOperation
from typing import Optional
from django.conf import settings
from django.contrib.auth.models import User
@@ -129,7 +128,7 @@ class StockLocationReportContext(report.mixins.BaseReportContext):
location: StockLocation
qr_data: str
parent: Optional[StockLocation]
parent: StockLocation | None
stock_location: StockLocation
stock_items: report.mixins.QuerySet[StockItem]
@@ -392,7 +391,7 @@ class StockItemReportContext(report.mixins.BaseReportContext):
barcode_hash: str
batch: str
child_items: report.mixins.QuerySet[StockItem]
ipn: Optional[str]
ipn: str | None
installed_items: set[StockItem]
item: StockItem
name: str
@@ -403,7 +402,7 @@ class StockItemReportContext(report.mixins.BaseReportContext):
quantity: Decimal
result_list: list[StockItemTestResult]
results: dict[str, StockItemTestResult]
serial: Optional[str]
serial: str | None
stock_item: StockItem
tests: dict[str, StockItemTestResult]
test_keys: list[str]
@@ -667,7 +666,7 @@ class StockItem(
return items
@staticmethod
def convert_serial_to_int(serial: str) -> Optional[int]:
def convert_serial_to_int(serial: str) -> int | None:
"""Convert the provided serial number to an integer value.
This function hooks into the plugin system to allow for custom serial number conversion.
@@ -1783,7 +1782,7 @@ class StockItem(
self,
entry_type: int,
user: User,
deltas: Optional[dict] = None,
deltas: dict | None = None,
notes: str = '',
commit: bool = True,
**kwargs,
@@ -1842,9 +1841,9 @@ class StockItem(
self,
quantity: int,
serials: list[str],
user: Optional[User] = None,
notes: Optional[str] = '',
location: Optional[StockLocation] = None,
user: User | None = None,
notes: str | None = '',
location: StockLocation | None = None,
):
"""Split this stock item into unique serial numbers.
@@ -1951,7 +1950,7 @@ class StockItem(
item.save()
@transaction.atomic
def copyTestResultsFrom(self, other: StockItem, filters: Optional[dict] = None):
def copyTestResultsFrom(self, other: StockItem, filters: dict | None = None):
"""Copy all test results from another StockItem."""
# Set default - see B006
+18 -1
View File
@@ -1,5 +1,8 @@
"""DRF API serializers for the 'users' app."""
import secrets
import string
from django.contrib.auth.models import Group, Permission, User
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
@@ -382,6 +385,20 @@ class MeUserSerializer(ExtendedUserSerializer):
profile = UserProfileSerializer(many=False, read_only=True)
def make_random_password(length=14):
"""Generate a random password of given length."""
alphabet = string.ascii_letters + string.digits
while True:
password = ''.join(secrets.choice(alphabet) for i in range(length))
if (
any(c.islower() for c in password)
and any(c.isupper() for c in password)
and sum(c.isdigit() for c in password) >= 3
):
break
return password
class UserCreateSerializer(ExtendedUserSerializer):
"""Serializer for creating a new User."""
@@ -402,7 +419,7 @@ class UserCreateSerializer(ExtendedUserSerializer):
)
# Generate a random password
password = User.objects.make_random_password(length=14)
password = make_random_password(length=14)
attrs.update({'password': password})
return super().validate(attrs)
@@ -3,7 +3,6 @@
import json
import json.decoder
from pathlib import Path
from typing import Union
from django import template
from django.conf import settings
@@ -18,7 +17,7 @@ FRONTEND_SETTINGS = json.dumps(settings.FRONTEND_SETTINGS)
@register.simple_tag
def spa_bundle(manifest_path: Union[str, Path] = '', app: str = 'web'):
def spa_bundle(manifest_path: str | Path = '', app: str = 'web'):
"""Render SPA bundle."""
def get_url(file: str) -> str:
+188 -204
View File
@@ -6,7 +6,6 @@ asgiref==3.10.0 \
# via
# -c src/backend/requirements.txt
# django
# django-stubs
build==1.3.0 \
--hash=sha256:698edd0ea270bde950f53aed21f3a0135672206f3911e0176261a31e0e07b397 \
--hash=sha256:7145f0b5061ba90a1500d60bd1b13ca0a8a4cebdd0cc16ed8adf1c0e739f43b4
@@ -227,154 +226,159 @@ charset-normalizer==3.4.4 \
# -c src/backend/requirements.txt
# pdfminer-six
# requests
click==8.1.8 \
--hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \
--hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a
click==8.3.0 \
--hash=sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc \
--hash=sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4
# via pip-tools
coverage[toml]==7.10.7 \
--hash=sha256:03ffc58aacdf65d2a82bbeb1ffe4d01ead4017a21bfd0454983b88ca73af94b9 \
--hash=sha256:097c1591f5af4496226d5783d036bf6fd6cd0cbc132e071b33861de756efb880 \
--hash=sha256:0b944ee8459f515f28b851728ad224fa2d068f1513ef6b7ff1efafeb2185f999 \
--hash=sha256:0ebbaddb2c19b71912c6f2518e791aa8b9f054985a0769bdb3a53ebbc765c6a1 \
--hash=sha256:10b24412692df990dbc34f8fb1b6b13d236ace9dfdd68df5b28c2e39cafbba13 \
--hash=sha256:10b6ba00ab1132a0ce4428ff68cf50a25efd6840a42cdf4239c9b99aad83be8b \
--hash=sha256:121da30abb574f6ce6ae09840dae322bef734480ceafe410117627aa54f76d82 \
--hash=sha256:18afb24843cbc175687225cab1138c95d262337f5473512010e46831aa0c2973 \
--hash=sha256:1b4fd784344d4e52647fd7857b2af5b3fbe6c239b0b5fa63e94eb67320770e0f \
--hash=sha256:1ca6db7c8807fb9e755d0379ccc39017ce0a84dcd26d14b5a03b78563776f681 \
--hash=sha256:1ef2319dd15a0b009667301a3f84452a4dc6fddfd06b0c5c53ea472d3989fbf0 \
--hash=sha256:2120043f147bebb41c85b97ac45dd173595ff14f2a584f2963891cbcc3091541 \
--hash=sha256:212f8f2e0612778f09c55dd4872cb1f64a1f2b074393d139278ce902064d5b32 \
--hash=sha256:240af60539987ced2c399809bd34f7c78e8abe0736af91c3d7d0e795df633d17 \
--hash=sha256:2a78cd46550081a7909b3329e2266204d584866e8d97b898cd7fb5ac8d888b1a \
--hash=sha256:2af88deffcc8a4d5974cf2d502251bc3b2db8461f0b66d80a449c33757aa9f40 \
--hash=sha256:2c8b9a0636f94c43cd3576811e05b89aa9bc2d0a85137affc544ae5cb0e4bfbd \
--hash=sha256:2fafd773231dd0378fdba66d339f84904a8e57a262f583530f4f156ab83863e6 \
--hash=sha256:314f2c326ded3f4b09be11bc282eb2fc861184bc95748ae67b360ac962770be7 \
--hash=sha256:33a5e6396ab684cb43dc7befa386258acb2d7fae7f67330ebb85ba4ea27938eb \
--hash=sha256:3445258bcded7d4aa630ab8296dea4d3f15a255588dd535f980c193ab6b95f3f \
--hash=sha256:35f5e3f9e455bb17831876048355dca0f758b6df22f49258cb5a91da23ef437d \
--hash=sha256:39508ffda4f343c35f3236fe8d1a6634a51f4581226a1262769d7f970e73bffe \
--hash=sha256:399a0b6347bcd3822be369392932884b8216d0944049ae22925631a9b3d4ba4c \
--hash=sha256:3a622ac801b17198020f09af3eaf45666b344a0d69fc2a6ffe2ea83aeef1d807 \
--hash=sha256:4376538f36b533b46f8971d3a3e63464f2c7905c9800db97361c43a2b14792ab \
--hash=sha256:4b583b97ab2e3efe1b3e75248a9b333bd3f8b0b1b8e5b45578e05e5850dfb2c2 \
--hash=sha256:4b6f236edf6e2f9ae8fcd1332da4e791c1b6ba0dc16a2dc94590ceccb482e546 \
--hash=sha256:4da86b6d62a496e908ac2898243920c7992499c1712ff7c2b6d837cc69d9467e \
--hash=sha256:50aa94fb1fb9a397eaa19c0d5ec15a5edd03a47bf1a3a6111a16b36e190cff65 \
--hash=sha256:567f5c155eda8df1d3d439d40a45a6a5f029b429b06648235f1e7e51b522b396 \
--hash=sha256:5a02d5a850e2979b0a014c412573953995174743a3f7fa4ea5a6e9a3c5617431 \
--hash=sha256:5e1e9802121405ede4b0133aa4340ad8186a1d2526de5b7c3eca519db7bb89fb \
--hash=sha256:5f33166f0dfcce728191f520bd2692914ec70fac2713f6bf3ce59c3deacb4699 \
--hash=sha256:606cc265adc9aaedcc84f1f064f0e8736bc45814f15a357e30fca7ecc01504e0 \
--hash=sha256:635adb9a4507c9fd2ed65f39693fa31c9a3ee3a8e6dc64df033e8fdf52a7003f \
--hash=sha256:65646bb0359386e07639c367a22cf9b5bf6304e8630b565d0626e2bdf329227a \
--hash=sha256:67f8c5cbcd3deb7a60b3345dffc89a961a484ed0af1f6f73de91705cc6e31235 \
--hash=sha256:69212fbccdbd5b0e39eac4067e20a4a5256609e209547d86f740d68ad4f04911 \
--hash=sha256:6b8b09c1fad947c84bbbc95eca841350fad9cbfa5a2d7ca88ac9f8d836c92e23 \
--hash=sha256:6be8ed3039ae7f7ac5ce058c308484787c86e8437e72b30bf5e88b8ea10f3c87 \
--hash=sha256:6e16e07d85ca0cf8bafe5f5d23a0b850064e8e945d5677492b06bbe6f09cc699 \
--hash=sha256:736f227fb490f03c6488f9b6d45855f8e0fd749c007f9303ad30efab0e73c05a \
--hash=sha256:73ab1601f84dc804f7812dc297e93cd99381162da39c47040a827d4e8dafe63b \
--hash=sha256:77eb4c747061a6af8d0f7bdb31f1e108d172762ef579166ec84542f711d90256 \
--hash=sha256:78a384e49f46b80fb4c901d52d92abe098e78768ed829c673fbb53c498bef73a \
--hash=sha256:7bb3b9ddb87ef7725056572368040c32775036472d5a033679d1fa6c8dc08417 \
--hash=sha256:7ea7c6c9d0d286d04ed3541747e6597cbe4971f22648b68248f7ddcd329207f0 \
--hash=sha256:7fe650342addd8524ca63d77b2362b02345e5f1a093266787d210c70a50b471a \
--hash=sha256:813922f35bd800dca9994c5971883cbc0d291128a5de6b167c7aa697fcf59360 \
--hash=sha256:83082a57783239717ceb0ad584de3c69cf581b2a95ed6bf81ea66034f00401c0 \
--hash=sha256:8421e088bc051361b01c4b3a50fd39a4b9133079a2229978d9d30511fd05231b \
--hash=sha256:86b0e7308289ddde73d863b7683f596d8d21c7d8664ce1dee061d0bcf3fbb4bb \
--hash=sha256:88127d40df529336a9836870436fc2751c339fbaed3a836d42c93f3e4bd1d0a2 \
--hash=sha256:8fb190658865565c549b6b4706856d6a7b09302c797eb2cf8e7fe9dabb043f0d \
--hash=sha256:912e6ebc7a6e4adfdbb1aec371ad04c68854cd3bf3608b3514e7ff9062931d8a \
--hash=sha256:925a1edf3d810537c5a3abe78ec5530160c5f9a26b1f4270b40e62cc79304a1e \
--hash=sha256:93c1b03552081b2a4423091d6fb3787265b8f86af404cff98d1b5342713bdd69 \
--hash=sha256:972b9e3a4094b053a4e46832b4bc829fc8a8d347160eb39d03f1690316a99c14 \
--hash=sha256:981a651f543f2854abd3b5fcb3263aac581b18209be49863ba575de6edf4c14d \
--hash=sha256:99e4aa63097ab1118e75a848a28e40d68b08a5e19ce587891ab7fd04475e780f \
--hash=sha256:9fa6e4dd51fe15d8738708a973470f67a855ca50002294852e9571cdbd9433f2 \
--hash=sha256:a0ec07fd264d0745ee396b666d47cef20875f4ff2375d7c4f58235886cc1ef0c \
--hash=sha256:a2d9a3b260cc1d1dbdb1c582e63ddcf5363426a1a68faa0f5da28d8ee3c722a0 \
--hash=sha256:a3cc8638b2480865eaa3926d192e64ce6c51e3d29c849e09d5b4ad95efae5399 \
--hash=sha256:a609f9c93113be646f44c2a0256d6ea375ad047005d7f57a5c15f614dc1b2f59 \
--hash=sha256:a62c6ef0d50e6de320c270ff91d9dd0a05e7250cac2a800b7784bae474506e63 \
--hash=sha256:a6442c59a8ac8b85812ce33bc4d05bde3fb22321fa8294e2a5b487c3505f611b \
--hash=sha256:a7b55a944a7f43892e28ad4bc0561dfd5f0d73e605d1aa5c3c976b52aea121d2 \
--hash=sha256:a8b6f03672aa6734e700bbcd65ff050fd19cddfec4b031cc8cf1c6967de5a68e \
--hash=sha256:affef7c76a9ef259187ef31599a9260330e0335a3011732c4b9effa01e1cd6e0 \
--hash=sha256:b06f260b16ead11643a5a9f955bd4b5fd76c1a4c6796aeade8520095b75de520 \
--hash=sha256:b1c81d0e5e160651879755c9c675b974276f135558cf4ba79fee7b8413a515df \
--hash=sha256:b281d5eca50189325cfe1f365fafade89b14b4a78d9b40b05ddd1fc7d2a10a9c \
--hash=sha256:b51dcd060f18c19290d9b8a9dd1e0181538df2ce0717f562fff6cf74d9fc0b5b \
--hash=sha256:b7b8288eb7cdd268b0304632da8cb0bb93fadcfec2fe5712f7b9cc8f4d487be2 \
--hash=sha256:b9be91986841a75042b3e3243d0b3cb0b2434252b977baaf0cd56e960fe1e46f \
--hash=sha256:ba58bbcd1b72f136080c0bccc2400d66cc6115f3f906c499013d065ac33a4b61 \
--hash=sha256:bb45474711ba385c46a0bfe696c695a929ae69ac636cda8f532be9e8c93d720a \
--hash=sha256:bc01f57ca26269c2c706e838f6422e2a8788e41b3e3c65e2f41148212e57cd59 \
--hash=sha256:bc91b314cef27742da486d6839b677b3f2793dfe52b51bbbb7cf736d5c29281c \
--hash=sha256:bda5e34f8a75721c96085903c6f2197dc398c20ffd98df33f866a9c8fd95f4bf \
--hash=sha256:c134869d5ffe34547d14e174c866fd8fe2254918cc0a95e99052903bc1543e07 \
--hash=sha256:c41e71c9cfb854789dee6fc51e46743a6d138b1803fab6cb860af43265b42ea6 \
--hash=sha256:c4e16bd7761c5e454f4efd36f345286d6f7c5fa111623c355691e2755cae3b9e \
--hash=sha256:c7315339eae3b24c2d2fa1ed7d7a38654cba34a13ef19fbcb9425da46d3dc594 \
--hash=sha256:c79124f70465a150e89340de5963f936ee97097d2ef76c869708c4248c63ca49 \
--hash=sha256:cac0fdca17b036af3881a9d2729a850b76553f3f716ccb0360ad4dbc06b3b843 \
--hash=sha256:cc87dd1b6eaf0b848eebb1c86469b9f72a1891cb42ac7adcfbce75eadb13dd14 \
--hash=sha256:cce2109b6219f22ece99db7644b9622f54a4e915dad65660ec435e89a3ea7cc3 \
--hash=sha256:d41213ea25a86f69efd1575073d34ea11aabe075604ddf3d148ecfec9e1e96a1 \
--hash=sha256:dc7c389dce432500273eaf48f410b37886be9208b2dd5710aaf7c57fd442c698 \
--hash=sha256:dd5e856ebb7bfb7672b0086846db5afb4567a7b9714b8a0ebafd211ec7ce6a15 \
--hash=sha256:e1ed71194ef6dea7ed2d5cb5f7243d4bcd334bfb63e59878519be558078f848d \
--hash=sha256:e201e015644e207139f7e2351980feb7040e6f4b2c2978892f3e3789d1c125e5 \
--hash=sha256:e28299d9f2e889e6d51b1f043f58d5f997c373cc12e6403b90df95b8b047c13e \
--hash=sha256:f3c887f96407cea3916294046fc7dab611c2552beadbed4ea901cbc6a40cc7a0 \
--hash=sha256:f49a05acd3dfe1ce9715b657e28d138578bc40126760efb962322c56e9ca344b \
--hash=sha256:f4ab143ab113be368a3e9b795f9cd7906c5ef407d6173fe9675a902e1fffc239 \
--hash=sha256:f51328ffe987aecf6d09f3cd9d979face89a617eacdaea43e7b3080777f647ba \
--hash=sha256:f57b2a3c8353d3e04acf75b3fed57ba41f5c0646bbf1d10c7c282291c97936b4 \
--hash=sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260 \
--hash=sha256:fc04cc7a3db33664e0c2d10eb8990ff6b3536f6842c9590ae8da4c614b9ed05a \
--hash=sha256:fff7b9c3f19957020cac546c70025331113d2e61537f6e2441bc7657913de7d3
coverage[toml]==7.11.2 \
--hash=sha256:004bdc5985b86f565772af627925e368256ee2172623db10a0d78a3b53f20ef1 \
--hash=sha256:03e7e7dc31a7deaebf121c3c3bd3c6442b7fbf50aca72aae2a1d08aa30ca2a20 \
--hash=sha256:07e14a4050525fd98bf3d793f229eb8b3ae81678f4031e38e6a18a068bd59fd4 \
--hash=sha256:0f4a958ff286038ac870f836351e9fb8912f1614d1cdbda200fc899235f7dc9b \
--hash=sha256:10f10c9acf584ef82bfaaa7296163bd11c7487237f1670e81fc2fa7e972be67b \
--hash=sha256:17047fb65fcd1ce8a2f97dd2247c2b59cb4bc8848b3911db02dcb05856f91b71 \
--hash=sha256:1ac3f647ecf25d883051ef42d38d823016e715b9f289f8c1768be5117075d1bd \
--hash=sha256:230317450af65a37c1fdbdd3546f7277e0c1c1b65e0d57409248e5dd0fa13493 \
--hash=sha256:2442afabe9e83b881be083238bb7cf5afd4a10e47f29b6094470338d2336b33c \
--hash=sha256:2970c03fefee2a5f1aebc91201a0706a7d0061cc71ab452bb5c5345b7174a349 \
--hash=sha256:2ca963994d28e44285dc104cf94b25d8a7fd0c6f87cf944f46a23f473910703f \
--hash=sha256:30c437e8b51ce081fe3903c9e368e85c9a803b093fd062c49215f3bf4fd1df37 \
--hash=sha256:36c41bf2ee6f6062de8177e249fee17cd5c9662cd373f7a41e6468a34c5b9c0f \
--hash=sha256:38a5509fe7fabb6fb3161059b947641753b6529150ef483fc01c4516a546f2ad \
--hash=sha256:397778cf6d50df59c890bd3ac10acb5bf413388ff6a013305134f1403d5db648 \
--hash=sha256:3aa8c62460499e10ceac5ea61cc09c4f7ddcd8a68c6313cf08785ad353dfd311 \
--hash=sha256:410cafc1aba1f7eb8c09823d5da381be30a2c9b3595758a4c176fcfc04732731 \
--hash=sha256:43ecf9dca4fcb3baf8a886019dd5ce663c95a5e1c5172719c414f0ebd9eeb785 \
--hash=sha256:44b6e04bb94e59927a2807cd4de86386ce34248eaea95d9f1049a72f81828c38 \
--hash=sha256:461577af3f8ad4da244a55af66c0731b68540ce571dbdc02598b5ec9e7a09e73 \
--hash=sha256:4648c90cf741fb61e142826db1557a44079de0ca868c5c5a363c53d852897e84 \
--hash=sha256:4aa799c61869318d2b86c0d3c413d6805546aec42069f009cbb27df2eefb2790 \
--hash=sha256:4aaf2212302b6f748dde596424b0f08bc3e1285192104e2480f43d56b6824f35 \
--hash=sha256:4c4423ea9c28749080b41e18ec74d658e6c9f148a6b47e719f3d7f56197f8227 \
--hash=sha256:4d1ff4b87ad438148976f2215141a490ae000e878536370d53f8da8c59a175a6 \
--hash=sha256:4f8f6bcaa7fe162460abb38f7a5dbfd7f47cfc51e2a0bf0d3ef9e51427298391 \
--hash=sha256:55ae008253df6000bc885a780c1b0e939bd8c932f41e16df1cfe19a00428a98a \
--hash=sha256:595c6bb2b565cc2d930ee634cae47fa959dfd24cc0e8ae4cf2b6e7e131e0d1f7 \
--hash=sha256:5a02818ec44803e325d66bd022828212df934739b894d1699c9a05b9105d30f2 \
--hash=sha256:5b284931d57389ec97a63fb1edf91c68ec369cee44bc40b37b5c3985ba0a2914 \
--hash=sha256:5c31cdbb95ab0f4a60224a04efc43cfb406ce904f0b60fb6b2a72f37718ea5cb \
--hash=sha256:5db683000ff6217273071c752bd6a1d341b6dc5d6aaa56678c53577a4e70e78a \
--hash=sha256:5f72a49504e1f35443b157d97997c9259a017384373eab52fd09b8ade2ae4674 \
--hash=sha256:61d6a7cc1e7a7a761ac59dcc88cee54219fd4231face52bd1257cfd3df29ae9f \
--hash=sha256:632904d126ca97e5d4ecf7e51ae8b20f086b6f002c6075adcfd4ff3a28574527 \
--hash=sha256:6681164bc697b93676945c8c814b76ac72204c395e11b71ba796a93b33331c24 \
--hash=sha256:689d3b4dd0d4c912ed8bfd7a1b5ff2c5ecb1fa16571840573174704ff5437862 \
--hash=sha256:6f70fa1ef17cba5dada94e144ea1b6e117d4f174666842d1da3aaf765d6eb477 \
--hash=sha256:72a3d109ac233666064d60b29ae5801dd28bc51d1990e69f183a2b91b92d4baf \
--hash=sha256:75ef769be19d69ea71b0417d7fbf090032c444792579cdf9b166346a340987d5 \
--hash=sha256:7e01ab8d69b6cffa2463e78a4d760a6b69dfebe5bf21837eabcc273655c7e7b3 \
--hash=sha256:7ea36e981a8a591acdaa920704f8dc798f9fff356c97dbd5d5702046ae967ce1 \
--hash=sha256:7f1aa017b47e1879d7bac50161b00d2b886f2ff3882fa09427119e1b3572ede1 \
--hash=sha256:7f933bc1fead57373922e383d803e1dd5ec7b5a786c220161152ebee1aa3f006 \
--hash=sha256:8014a28a37ffabf7da7107f4f154d68c6b89672f27fef835a0574591c5cd140b \
--hash=sha256:805efa416085999da918f15f81b26636d8e79863e1fbac1495664686d1e6a6e9 \
--hash=sha256:811bff1f93566a8556a9aeb078bd82573e37f4d802a185fba4cbe75468615050 \
--hash=sha256:84e8e0f5ab5134a2d32d4ebadc18b433dbbeddd0b73481f816333b1edd3ff1c8 \
--hash=sha256:87d7c7b0b2279e174f36d276e2afb7bf16c9ea04e824d4fa277eea1854f4cfd4 \
--hash=sha256:89565d7c9340858424a5ca3223bfefe449aeb116942cdc98cd76c07ca50e9db8 \
--hash=sha256:940d195f4c8ba3ec6e7c302c9f546cdbe63e57289ed535452bc52089b1634f1c \
--hash=sha256:94ced4a29a6987af99faaa49a513bf8d0458e8af004c54174e05dd7a8a31c7d9 \
--hash=sha256:9a6468e1a3a40d3d1f9120a9ff221d3eacef4540a6f819fff58868fe0bd44fa9 \
--hash=sha256:9a95b7a6043b221ec1a0d4d5481e424272b37028353265fbe5fcd3768d652eb7 \
--hash=sha256:9f5f6ee021b3b25e748a9a053f3a8dd61a62b6689efd6425cb47e27360994903 \
--hash=sha256:a35701fe0b5ee9d4b67d31aa76555237af32a36b0cf8dd33f8a74470cf7cd2f5 \
--hash=sha256:a913b21f716aa05b149a8656e9e234d9da04bc1f9842136ad25a53172fecc20e \
--hash=sha256:ae43149b7732df15c3ca9879b310c48b71d08cd8a7ba77fda7f9108f78499e93 \
--hash=sha256:b4776c6555a9f378f37fa06408f2e1cc1d06e4c4e06adb3d157a4926b549efbe \
--hash=sha256:b7658f3d4f728092368c091c18efcfb679be9b612c93bfdf345f33635a325188 \
--hash=sha256:b7fc943097fa48de00d14d2a2f3bcebfede024e031d7cd96063fe135f8cbe96e \
--hash=sha256:b9f28b900d96d83e2ae855b68d5cf5a704fa0b5e618999133fd2fb3bbe35ecb1 \
--hash=sha256:bc65e32fe5bb942f0f5247e1500e355cbbdf326181198f5e27e3bb3ddb81e203 \
--hash=sha256:bee1911c44c52cad6b51d436aa8c6ff5ca5d414fa089c7444592df9e7b890be9 \
--hash=sha256:c4b1bea4c707f4c09f682fe0e646a114dfd068f627880d4a208850d01f8164ad \
--hash=sha256:c5769159986eb174f0f66d049a52da03f2d976ac1355679371f1269e83528599 \
--hash=sha256:c65f4291aec39692a3bfbe1d92ae5bea58c16b5553fdf021de61c655d987233f \
--hash=sha256:c7ea5dec77d79dabb7b5fc712c59361aac52e459cd22028480625c3c743323d0 \
--hash=sha256:c85f44ed4260221e46a4e9e8e8df4b359ab6c0a742c79e85d649779bcf77b534 \
--hash=sha256:c8b9a7ebc6a29202fb095877fd8362aab09882894d1c950060c76d61fb116114 \
--hash=sha256:cbffd1d5c5bf4c576ca247bf77646cdad4dced82928337eeb0b85e2b3be4d64b \
--hash=sha256:d0e80c9946da61cc0bf55dfd90d65707acc1aa5bdcb551d4285ea8906255bb33 \
--hash=sha256:d30a717493583c2a83c99f195e934c073be7f4291b32b7352c246d52e43f6893 \
--hash=sha256:d423991415f73a70c0a5f3e0a226cf4ab374dd0da7409978069b844df3d31582 \
--hash=sha256:d73da4893125e0671f762e408dea9957b2bda0036c9589c2fd258a6b870acbdb \
--hash=sha256:d752a8e398a19e2fb24781e4c73089bfeb417b6ac55f96c2c42cfe5bdb21cc18 \
--hash=sha256:e3b92e10ca996b5421232dd6629b9933f97eb57ce374bca800ab56681fbeda2b \
--hash=sha256:e448ceee2fb880427eafc9a3f8e6162b2ac7cc3e9b30b85d6511f25cc8a11820 \
--hash=sha256:e48b95abe2983be98cdf52900e07127eb7fe7067c87a700851f4f1f53d2b00e6 \
--hash=sha256:e52a028a56889d3ad036c0420e866e4a69417d3203e2fc5f03dcb8841274b64c \
--hash=sha256:e7d3fccd5781c5d29ca0bd1ea272630f05cd40a71d419e7e6105c0991400eb14 \
--hash=sha256:e8eb6cbd7d3b238335b5da0f3ce281102435afb503be4d7bdd69eea3c700a952 \
--hash=sha256:ea10a57568af7cf082a7a4d98a699f993652c2ffbdd5a6c9d63c9ca10b693b4d \
--hash=sha256:ea910cc737ee8553c81ad5c104bc5b135106ebb36f88be506c3493e001b4c733 \
--hash=sha256:eaa2a5eeb82fa7a6a9cd65c4f968ee2a53839d451b4e88e060c67d87a0a40732 \
--hash=sha256:ed6ba9f1777fdd1c8e5650c7d123211fa484a187c61af4d82948dc5ee3c0afcc \
--hash=sha256:ef2d3081562cd83f97984a96e02e7a294efa28f58d5e7f4e28920f59fd752b41 \
--hash=sha256:f633da28958f57b846e955d28661b2b323d8ae84668756e1eea64045414dbe34 \
--hash=sha256:f6b2498f86f2554ed6cb8df64201ee95b8c70fb77064a8b2ae8a7185e7a4a5f0 \
--hash=sha256:f6f985e175dfa1fb8c0a01f47186720ae25d5e20c181cc5f3b9eba95589b8148 \
--hash=sha256:f80cb5b328e870bf3df0568b41643a85ee4b8ccd219a096812389e39aa310ea4 \
--hash=sha256:fd3f7cc6cb999e3eff91a2998a70c662b0fcd3c123d875766147c530ca0d3248
# via -r src/backend/requirements-dev.in
cryptography==44.0.3 \
--hash=sha256:02f55fb4f8b79c1221b0961488eaae21015b69b210e18c386b69de182ebb1259 \
--hash=sha256:157f1f3b8d941c2bd8f3ffee0af9b049c9665c39d3da9db2dc338feca5e98a43 \
--hash=sha256:192ed30fac1728f7587c6f4613c29c584abdc565d7417c13904708db10206645 \
--hash=sha256:21a83f6f35b9cc656d71b5de8d519f566df01e660ac2578805ab245ffd8523f8 \
--hash=sha256:25cd194c39fa5a0aa4169125ee27d1172097857b27109a45fadc59653ec06f44 \
--hash=sha256:3883076d5c4cc56dbef0b898a74eb6992fdac29a7b9013870b34efe4ddb39a0d \
--hash=sha256:3bb0847e6363c037df8f6ede57d88eaf3410ca2267fb12275370a76f85786a6f \
--hash=sha256:3be3f649d91cb182c3a6bd336de8b61a0a71965bd13d1a04a0e15b39c3d5809d \
--hash=sha256:3f07943aa4d7dad689e3bb1638ddc4944cc5e0921e3c227486daae0e31a05e54 \
--hash=sha256:479d92908277bed6e1a1c69b277734a7771c2b78633c224445b5c60a9f4bc1d9 \
--hash=sha256:4ffc61e8f3bf5b60346d89cd3d37231019c17a081208dfbbd6e1605ba03fa137 \
--hash=sha256:5639c2b16764c6f76eedf722dbad9a0914960d3489c0cc38694ddf9464f1bb2f \
--hash=sha256:58968d331425a6f9eedcee087f77fd3c927c88f55368f43ff7e0a19891f2642c \
--hash=sha256:5d186f32e52e66994dce4f766884bcb9c68b8da62d61d9d215bfe5fb56d21334 \
--hash=sha256:5d20cc348cca3a8aa7312f42ab953a56e15323800ca3ab0706b8cd452a3a056c \
--hash=sha256:6866df152b581f9429020320e5eb9794c8780e90f7ccb021940d7f50ee00ae0b \
--hash=sha256:7d5fe7195c27c32a64955740b949070f21cba664604291c298518d2e255931d2 \
--hash=sha256:896530bc9107b226f265effa7ef3f21270f18a2026bc09fed1ebd7b66ddf6375 \
--hash=sha256:962bc30480a08d133e631e8dfd4783ab71cc9e33d5d7c1e192f0b7c06397bb88 \
--hash=sha256:978631ec51a6bbc0b7e58f23b68a8ce9e5f09721940933e9c217068388789fe5 \
--hash=sha256:9b4d4a5dbee05a2c390bf212e78b99434efec37b17a4bff42f50285c5c8c9647 \
--hash=sha256:ab0b005721cc0039e885ac3503825661bd9810b15d4f374e473f8c89b7d5460c \
--hash=sha256:af653022a0c25ef2e3ffb2c673a50e5a0d02fecc41608f4954176f1933b12359 \
--hash=sha256:b0cc66c74c797e1db750aaa842ad5b8b78e14805a9b5d1348dc603612d3e3ff5 \
--hash=sha256:b424563394c369a804ecbee9b06dfb34997f19d00b3518e39f83a5642618397d \
--hash=sha256:c138abae3a12a94c75c10499f1cbae81294a6f983b3af066390adee73f433028 \
--hash=sha256:c6cd67722619e4d55fdb42ead64ed8843d64638e9c07f4011163e46bc512cf01 \
--hash=sha256:c91fc8e8fd78af553f98bc7f2a1d8db977334e4eea302a4bfd75b9461c2d8904 \
--hash=sha256:cad399780053fb383dc067475135e41c9fe7d901a97dd5d9c5dfb5611afc0d7d \
--hash=sha256:cb90f60e03d563ca2445099edf605c16ed1d5b15182d21831f58460c48bffb93 \
--hash=sha256:dad80b45c22e05b259e33ddd458e9e2ba099c86ccf4e88db7bbab4b747b18d06 \
--hash=sha256:dd3db61b8fe5be220eee484a17233287d0be6932d056cf5738225b9c05ef4fff \
--hash=sha256:e28d62e59a4dbd1d22e747f57d4f00c459af22181f0b2f787ea83f5a876d7c76 \
--hash=sha256:e909df4053064a97f1e6565153ff8bb389af12c5c8d29c343308760890560aff \
--hash=sha256:f3ffef566ac88f75967d7abd852ed5f182da252d23fac11b4766da3957766759 \
--hash=sha256:fc3c9babc1e1faefd62704bb46a69f359a9819eb0292e40df3fb6e3574715cd4 \
--hash=sha256:fe19d8bc5536a91a24a8133328880a41831b6c5df54599a8417b62fe015d3053
cryptography==46.0.3 \
--hash=sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217 \
--hash=sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d \
--hash=sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc \
--hash=sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71 \
--hash=sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971 \
--hash=sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a \
--hash=sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926 \
--hash=sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc \
--hash=sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d \
--hash=sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b \
--hash=sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20 \
--hash=sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044 \
--hash=sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3 \
--hash=sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715 \
--hash=sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4 \
--hash=sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506 \
--hash=sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f \
--hash=sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0 \
--hash=sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683 \
--hash=sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3 \
--hash=sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21 \
--hash=sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91 \
--hash=sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c \
--hash=sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8 \
--hash=sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df \
--hash=sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c \
--hash=sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb \
--hash=sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7 \
--hash=sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04 \
--hash=sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db \
--hash=sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459 \
--hash=sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea \
--hash=sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914 \
--hash=sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717 \
--hash=sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9 \
--hash=sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac \
--hash=sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32 \
--hash=sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec \
--hash=sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1 \
--hash=sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb \
--hash=sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac \
--hash=sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665 \
--hash=sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e \
--hash=sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb \
--hash=sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5 \
--hash=sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936 \
--hash=sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de \
--hash=sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372 \
--hash=sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54 \
--hash=sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422 \
--hash=sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849 \
--hash=sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c \
--hash=sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963 \
--hash=sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018
# via
# -c src/backend/requirements.txt
# pdfminer-six
@@ -382,9 +386,9 @@ distlib==0.4.0 \
--hash=sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16 \
--hash=sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d
# via virtualenv
django==4.2.26 \
--hash=sha256:9398e487bcb55e3f142cb56d19fbd9a83e15bb03a97edc31f408361ee76d9d7a \
--hash=sha256:c96e64fc3c359d051a6306871bd26243db1bd02317472a62ffdbe6c3cae14280
django==5.2.8 \
--hash=sha256:23254866a5bb9a2cfa6004e8b809ec6246eba4b58a7589bc2772f1bcc8456c7f \
--hash=sha256:37e687f7bd73ddf043e2b6b97cfe02fcbb11f2dbb3adccc6a2b18c6daa054d7f
# via
# -c src/backend/requirements.txt
# django-slowtests
@@ -396,25 +400,25 @@ django-querycount==0.8.3 \
django-slowtests==1.1.1 \
--hash=sha256:3c6936d420c9df444ac03625b41d97de043c662bbde61fbcd33e4cd407d0c247
# via -r src/backend/requirements-dev.in
django-stubs==5.1.3 \
--hash=sha256:716758ced158b439213062e52de6df3cff7c586f9f9ad7ab59210efbea5dfe78 \
--hash=sha256:8c230bc5bebee6da282ba8a27ad1503c84a0c4cd2f46e63d149e76d2a63e639a
django-stubs==5.2.7 \
--hash=sha256:2864e74b56ead866ff1365a051f24d852f6ed02238959664f558a6c9601c95bf \
--hash=sha256:2a07e47a8a867836a763c6bba8bf3775847b4fd9555bfa940360e32d0ee384a1
# via -r src/backend/requirements-dev.in
django-stubs-ext==5.1.3 \
--hash=sha256:3e60f82337f0d40a362f349bf15539144b96e4ceb4dbd0239be1cd71f6a74ad0 \
--hash=sha256:64561fbc53e963cc1eed2c8eb27e18b8e48dcb90771205180fe29fc8a59e55fd
django-stubs-ext==5.2.7 \
--hash=sha256:0466a7132587d49c5bbe12082ac9824d117a0dedcad5d0ada75a6e0d3aca6f60 \
--hash=sha256:b690655bd4cb8a44ae57abb314e0995dc90414280db8f26fff0cb9fb367d1cac
# via django-stubs
django-test-migrations==1.4.0 \
--hash=sha256:294dff98f6d43d020d4046b971bac5339e7c71458a35e9ad6450c388fe16ed6b \
--hash=sha256:f0c9c92864ed27d0c9a582e92056637e91227f54bd868a50cb9a1726668c563e
django-test-migrations==1.5.0 \
--hash=sha256:1cbff04b1e82c5564a6f635284907b381cc11a2ff883adff46776d9126824f07 \
--hash=sha256:96a08f085fc8bfaa53d44618341d82a2d22fd194c821cd81b147b66f0bec0da8
# via -r src/backend/requirements-dev.in
django-types==0.20.0 \
--hash=sha256:4e55d2c56155e3d69d75def9eb1d95a891303f2ac19fccf6fe8056da4293fae7 \
--hash=sha256:a0b5c2c9a1e591684bb21a93b64e50ca6cb2d3eab48f49faff1eac706bd3a9c7
django-types==0.22.0 \
--hash=sha256:4cecc9eee846e7ff2a398bec9dfe6543e76efb922a7a58c5d6064bcb0e6a3dc5 \
--hash=sha256:ba15c756c7a732e58afd0737e54489f1c5e6f1bd24132e9199c637b1f88b057c
# via -r src/backend/requirements-dev.in
filelock==3.19.1 \
--hash=sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58 \
--hash=sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d
filelock==3.20.0 \
--hash=sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2 \
--hash=sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4
# via virtualenv
identify==2.6.15 \
--hash=sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757 \
@@ -426,16 +430,9 @@ idna==3.11 \
# via
# -c src/backend/requirements.txt
# requests
importlib-metadata==8.7.0 \
--hash=sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000 \
--hash=sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd
# via
# -c src/backend/requirements.txt
# build
# isort
isort==6.1.0 \
--hash=sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784 \
--hash=sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481
isort==7.0.0 \
--hash=sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1 \
--hash=sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187
# via -r src/backend/requirements-dev.in
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
@@ -447,9 +444,9 @@ packaging==25.0 \
# via
# -c src/backend/requirements.txt
# build
pdfminer-six==20250506 \
--hash=sha256:b03cc8df09cf3c7aba8246deae52e0bca7ebb112a38895b5e1d4f5dd2b8ca2e7 \
--hash=sha256:d81ad173f62e5f841b53a8ba63af1a4a355933cfc0ffabd608e568b9193909e3
pdfminer-six==20251107 \
--hash=sha256:5fb0c553799c591777f22c0c72b77fc2522d7d10c70654e25f4c5f1fd996e008 \
--hash=sha256:c09df33e4cbe6b26b2a79248a4ffcccafaa5c5d39c9fff0e6e81567f165b5401
# via -r src/backend/requirements-dev.in
pip==25.3 \
--hash=sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343 \
@@ -459,15 +456,15 @@ pip-tools==7.5.1 \
--hash=sha256:a051a94794ba52df9acad2d7c9b0b09ae001617db458a543f8287fea7b89c2cf \
--hash=sha256:f5ff803823529edc0e6e40c86b1aa7da7266fb1078093c8beea4e5b77877036a
# via -r src/backend/requirements-dev.in
platformdirs==4.4.0 \
--hash=sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85 \
--hash=sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf
platformdirs==4.5.0 \
--hash=sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312 \
--hash=sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3
# via
# -c src/backend/requirements.txt
# virtualenv
pre-commit==4.3.0 \
--hash=sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8 \
--hash=sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16
pre-commit==4.4.0 \
--hash=sha256:b35ea52957cbf83dcc5d8ee636cbead8624e3a15fbfa61a370e42158ac8a5813 \
--hash=sha256:f0233ebab440e9f17cabbb558706eb173d19ace965c68cdce2c081042b4fab15
# via -r src/backend/requirements-dev.in
pycparser==2.23 \
--hash=sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2 \
@@ -624,12 +621,7 @@ tomli==2.3.0 \
--hash=sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf \
--hash=sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463 \
--hash=sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876
# via
# -c src/backend/requirements.txt
# build
# coverage
# django-stubs
# pip-tools
# via coverage
ty==0.0.1a21 \
--hash=sha256:0efba2e52b58f536f4198ba5c4a36cac2ba67d83ec6f429ebc7704233bcda4c3 \
--hash=sha256:1474d883129bb63da3b2380fc7ead824cd3baf6a9551e6aa476ffefc58057af3 \
@@ -663,28 +655,20 @@ typing-extensions==4.15.0 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# -c src/backend/requirements.txt
# asgiref
# django-stubs
# django-stubs-ext
# django-test-migrations
# virtualenv
urllib3==1.26.20 \
--hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
--hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
urllib3==2.5.0 \
--hash=sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760 \
--hash=sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc
# via
# -c src/backend/requirements.txt
# requests
virtualenv==20.35.3 \
--hash=sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44 \
--hash=sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a
virtualenv==20.35.4 \
--hash=sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c \
--hash=sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b
# via pre-commit
wheel==0.45.1 \
--hash=sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729 \
--hash=sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248
# via pip-tools
zipp==3.23.0 \
--hash=sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e \
--hash=sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166
# via
# -c src/backend/requirements.txt
# importlib-metadata
+1 -1
View File
@@ -1,5 +1,5 @@
# Please keep this list sorted - if you pin a version provide a reason
Django<6.0 # Django package
django<6.0 # Django package
blessed # CLI for Q Monitor
cryptography>=44.0.0 # Core cryptographic functionality
django-anymail[amazon_ses,postal] # Email backend for various providers
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -134,7 +134,7 @@ test('Spotlight - No Keys', async ({ browser }) => {
.click();
await page.getByText('License Information').first().waitFor();
await page.getByRole('tab', { name: 'backend Packages' }).waitFor();
await page.getByRole('button', { name: 'Django BSD License' }).click();
await page.getByRole('button', { name: 'Django BSD-3-Clause' }).click();
await page.keyboard.press('Escape');
+1 -1
View File
@@ -78,7 +78,7 @@ def get_installer(content: Optional[dict] = None):
"""Get the installer for the current environment or a content dict."""
if content is None:
content = dict(os.environ)
return content.get('INVENTREE_PKG_INSTALLER', None)
return content.get('INVENTREE_PKG_INSTALLER')
# region execution logging helpers