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