2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-02-02 03:14:56 +00:00

feat(backend): extend schema intro (#10628)

* small refactor

* add inventree vendor extension

* bump api version

* Add control over schema to settings

* add more details

* disable config as requested

* adjust 3.14 diff

* cleanup diff

* add docs on the new feature

* revert bumping of api version - there is no cahnge by default
This commit is contained in:
Matthias Mair
2026-01-29 06:31:58 +01:00
committed by GitHub
parent 0d80d495cc
commit 620e69be4d
7 changed files with 115 additions and 46 deletions

View File

@@ -119,6 +119,20 @@ The translation process is as follows:
The [API version]({{ sourcefile("src/backend/InvenTree/InvenTree/api_version.py") }}) needs to be bumped every time when the API is changed. The [API version]({{ sourcefile("src/backend/InvenTree/InvenTree/api_version.py") }}) needs to be bumped every time when the API is changed.
### Understanding API shape
While the default Open API schema generation provides a good overview of the API endpoints, it does not provide insights into the shape of the underlying API (serializer) code.
The default schema generation cli command `invoke dev.schema` / endpoint `/api/schema/` can be enhanced by setting the schema generation level in the config file or via the [debugging environment variable or config value](../start/config.md#debugging-and-logging-options) `INVENTREE_SCHEMA_LEVEL`.
At level 1 only simple attributes describing the underlying Django Rest Framework API view of a endpoint are added under the `x-inventree-meta` key.
At level 2 details about the inheritance of the view (key `x-inventree-components`) and model (key `x-inventree-model`) are added. This allows to trace back the view to the underlying serializer and model and ensure naming of endpoints is consistent with the data model.
!!! note "For experiments only"
There are no CI or system checks to use these additional attributes yet. This is an experimental feature to help developers understand the API shape and how it changes better.
## Environment ## Environment
### Software Versions ### Software Versions

View File

@@ -100,6 +100,7 @@ The following debugging / logging options are available:
| INVENTREE_JSON_LOG | json_log | log as json | False | | INVENTREE_JSON_LOG | json_log | log as json | False |
| INVENTREE_WRITE_LOG | write_log | Enable writing of log messages to file at config base | False | | INVENTREE_WRITE_LOG | write_log | Enable writing of log messages to file at config base | False |
| INVENTREE_CONSOLE_LOG | console_log | Enable logging to console | True | | INVENTREE_CONSOLE_LOG | console_log | Enable logging to console | True |
| INVENTREE_SCHEMA_LEVEL | schema.level | Set level of added schema extensions detail (0-3) 0 = including no additional detail | 0 |
### Debug Mode ### Debug Mode

View File

@@ -89,12 +89,13 @@ class ExtendedAutoSchema(AutoSchema):
operation['requestBody'] = request_body operation['requestBody'] = request_body
self.method = original_method self.method = original_method
parameters = operation.get('parameters', [])
# If pagination limit is not set (default state) then all results will return unpaginated. This doesn't match # If pagination limit is not set (default state) then all results will return unpaginated. This doesn't match
# what the schema defines to be the expected result. This forces limit to be present, producing the expected # what the schema defines to be the expected result. This forces limit to be present, producing the expected
# type. # type.
pagination_class = getattr(self.view, 'pagination_class', None) pagination_class = getattr(self.view, 'pagination_class', None)
if pagination_class and pagination_class == LimitOffsetPagination: if pagination_class and pagination_class == LimitOffsetPagination:
parameters = operation.get('parameters', [])
for parameter in parameters: for parameter in parameters:
if parameter['name'] == 'limit': if parameter['name'] == 'limit':
parameter['required'] = True parameter['required'] = True
@@ -102,7 +103,6 @@ class ExtendedAutoSchema(AutoSchema):
# Add valid order selections to the ordering field description. # Add valid order selections to the ordering field description.
ordering_fields = getattr(self.view, 'ordering_fields', None) ordering_fields = getattr(self.view, 'ordering_fields', None)
if ordering_fields is not None: if ordering_fields is not None:
parameters = operation.get('parameters', [])
for parameter in parameters: for parameter in parameters:
if parameter['name'] == 'ordering': if parameter['name'] == 'ordering':
schema_order = [] schema_order = []
@@ -117,8 +117,6 @@ class ExtendedAutoSchema(AutoSchema):
if search_fields is not None: if search_fields is not None:
# Ensure consistent ordering of search fields # Ensure consistent ordering of search fields
search_fields = sorted(search_fields) search_fields = sorted(search_fields)
parameters = operation.get('parameters', [])
for parameter in parameters: for parameter in parameters:
if parameter['name'] == 'search': if parameter['name'] == 'search':
parameter['description'] = ( parameter['description'] = (
@@ -135,8 +133,57 @@ class ExtendedAutoSchema(AutoSchema):
schema['items'] = {'$ref': schema['$ref']} schema['items'] = {'$ref': schema['$ref']}
del schema['$ref'] del schema['$ref']
# Add vendor extensions for custom behavior
operation.update(self.get_inventree_extensions())
return operation return operation
def get_inventree_extensions(self):
"""Add InvenTree specific extensions to the schema."""
from rest_framework.generics import RetrieveAPIView
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin
from data_exporter.mixins import DataExportViewMixin
from InvenTree.api import BulkOperationMixin
from InvenTree.mixins import CleanMixin
lvl = settings.SCHEMA_VENDOREXTENSION_LEVEL
"""Level of detail for InvenTree extensions."""
if lvl == 0:
return {}
mro = self.view.__class__.__mro__
data = {}
if lvl >= 1:
data['x-inventree-meta'] = {
'version': '1.0',
'is_detail': any(
a in mro
for a in [RetrieveModelMixin, UpdateModelMixin, RetrieveAPIView]
),
'is_bulk': BulkOperationMixin in mro,
'is_cleaned': CleanMixin in mro,
'is_filtered': hasattr(self.view, 'output_options'),
'is_exported': DataExportViewMixin in mro,
}
if lvl >= 2:
data['x-inventree-components'] = [str(a) for a in mro]
try:
qs = self.view.get_queryset()
qs = qs.model if qs is not None and hasattr(qs, 'model') else None
except Exception:
qs = None
data['x-inventree-model'] = {
'scope': 'core',
'model': str(qs.__name__) if qs else None,
'app': str(qs._meta.app_label) if qs else None,
}
return data
def postprocess_schema_enums(result, generator, **kwargs): def postprocess_schema_enums(result, generator, **kwargs):
"""Override call to drf-spectacular's enum postprocessor to filter out specific warnings.""" """Override call to drf-spectacular's enum postprocessor to filter out specific warnings."""

View File

@@ -1495,6 +1495,9 @@ LOGIN_REDIRECT_URL = '/api/auth/login-redirect/'
# Configuration for API schema generation / oAuth2 # Configuration for API schema generation / oAuth2
SPECTACULAR_SETTINGS = spectacular.get_spectacular_settings() SPECTACULAR_SETTINGS = spectacular.get_spectacular_settings()
SCHEMA_VENDOREXTENSION_LEVEL = get_setting(
'INVENTREE_SCHEMA_LEVEL', 'schema.level', default_value=0, typecast=int
)
OAUTH2_PROVIDER = { OAUTH2_PROVIDER = {
# default scopes # default scopes

View File

@@ -40,6 +40,10 @@ debug_silk: False
debug_silk_profiling: False debug_silk_profiling: False
debug_shell: False debug_shell: False
# Schema generation options
#schema:
# level: 0 # Level of added schema extensions detail (0-3) 0 = including no additional detail, or use the environment variable INVENTREE_SCHEMA_LEVEL
# Set to False to disable the admin interface, or use the environment variable INVENTREE_ADMIN_ENABLED # Set to False to disable the admin interface, or use the environment variable INVENTREE_ADMIN_ENABLED
#admin_enabled: True #admin_enabled: True

View File

@@ -535,9 +535,9 @@ django-cors-headers==4.9.0 \
--hash=sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449 \ --hash=sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449 \
--hash=sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8 --hash=sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
django-dbbackup==5.1.0 \ django-dbbackup==5.1.1 \
--hash=sha256:611291606bf6a80903733a99235963e65236c23bca26c2edca453b928b504c67 \ --hash=sha256:371d82803743f6963d7d6d44ee145472213ddd287c43576ddbc11f2277972c1d \
--hash=sha256:66c236bbfa0c9bda33a61d30be8c5961d70fa73fed2fe7f829559ac216354130 --hash=sha256:bfefa6fcf64a602fd5416bc7fff679509bf7eee35c114eee295730df8e9589c9
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
django-error-report-2==0.4.2 \ django-error-report-2==0.4.2 \
--hash=sha256:1dd99c497af09b7ea99f5fbaf910501838150a9d5390796ea00e187bc62f6c1b \ --hash=sha256:1dd99c497af09b7ea99f5fbaf910501838150a9d5390796ea00e187bc62f6c1b \
@@ -582,9 +582,9 @@ django-mptt==0.18.0 \
--hash=sha256:bfa3f01627e3966a1df901aeca74570a3e933e66809ebf58d9df673e63627afb \ --hash=sha256:bfa3f01627e3966a1df901aeca74570a3e933e66809ebf58d9df673e63627afb \
--hash=sha256:cf5661357ff22bc64e20d3341c26e538aa54583aea0763cfe6aaec0ab8e3a8ee --hash=sha256:cf5661357ff22bc64e20d3341c26e538aa54583aea0763cfe6aaec0ab8e3a8ee
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
django-oauth-toolkit==3.1.0 \ django-oauth-toolkit==3.2.0 \
--hash=sha256:10ddc90804297d913dfb958edd58d5fac541eb1ca912f47893ca1e482bb2a11f \ --hash=sha256:bd2cd2719b010231a2f370f927dbcc740454fb1d0dd7e7f4138f36227363dc26 \
--hash=sha256:d5a59d07588cfefa8818e99d65040a252eb2ede22512483e2240c91d0b885c8e --hash=sha256:c36761ae6810083d95a652e9c820046cde0d45a2e2a5574bbe7202656ec20bb6
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
django-otp==1.3.0 \ django-otp==1.3.0 \
--hash=sha256:5277731bc05b6cdbf96aa84ac46018e30ed5fb248086053b0146f925de059060 \ --hash=sha256:5277731bc05b6cdbf96aa84ac46018e30ed5fb248086053b0146f925de059060 \
@@ -658,33 +658,33 @@ drf-spectacular==0.29.0 \
--hash=sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc \ --hash=sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc \
--hash=sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a --hash=sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
dulwich==0.25.0 \ dulwich==0.25.1 \
--hash=sha256:14c9aba34e1ac262806174304a5a17a78a0f83d0a6960e506005d3aa1cf9004e \ --hash=sha256:06009f7f86cf3d3cdad3ad113c8667b7d2e7bd6606638462dc10f58fc5e647c3 \
--hash=sha256:1575e7bf93cbc9ae93d6653fe29962357b96a1f5943275ff55cbb772e61359e2 \ --hash=sha256:1316bfd979f708a894bb953c6a1618762169ce7b2db0ea58edffb0e1fc3733ce \
--hash=sha256:3d342daf24cc544f1ccc7e6cf6b8b22d10a4381c1c7ed2bf0e2024a48be9218f \ --hash=sha256:2ffa8166321cf84a1aa31d091c74f11480831347c5a89f8082b2ad519c40d8ae \
--hash=sha256:47f0328af2c0e5149f356b27d1ac5b2860049c29bf32d2e5994d33f879909dd6 \ --hash=sha256:529da7c6dfea3e404a792044f3a45a5eec6d0095f2d3c3f66ab578825a6a5c2d \
--hash=sha256:4a98628ae4150f5084e0e0eab884c967d9f499304ff220f558ebe523868fd564 \ --hash=sha256:5c394bcfea736b380b1e5490b28f791f21769bae7aa198c4bd200ad7aa94b9bd \
--hash=sha256:4b46836c467bd898fd2ff1d4ebe511d2956f7f3f181dccbdde8631d4031cd0fa \ --hash=sha256:6c20393b9e68f68bd772d008ad9a0ce92c9e4cbdc98ab04953e3af668b5dd7e1 \
--hash=sha256:63846a66254dd89bec7b3df75dda61fc37f9c53aa93cddf46d063a9e1f832634 \ --hash=sha256:6d796c5f5c1e3951a7422046419414f88c4bf282ffb29b0e9b5e7ff821a9ac81 \
--hash=sha256:6ca746bd4f8a6a7b849a759c34e960dd7b6fa573225a571e23ea9c73377175d2 \ --hash=sha256:7170de4380e1e0fd1016e567fb932d94f28577fc6a259fd2e25bc25fc86a416b \
--hash=sha256:757ab788d2d87d96e4b5e84eaddc32d7b8e5b57a221f43b8cbb694787a9c1b80 \ --hash=sha256:7b4534dffe836d5654a54cc4ecaecfd909846f065ae148a6fdde3d8dd8091552 \
--hash=sha256:7b88ef0402ce2a94db5ae926e6be8048e59e8cdcc889a71e332d0e7bcc59f8b7 \ --hash=sha256:849195ae2cc245ae2e1b2d67859582e923a7e363f5f9ae5212029429762d9901 \
--hash=sha256:83e1cbff47ce1dc7d44a20f624c0d2fcbc6a70a458c5fe8e0f8bbf84f32aeb1c \ --hash=sha256:8c7756d6f22d11da42fc803584edb290b2cd2a72a6460834644349c739dccab1 \
--hash=sha256:866dcf6103ca4dddf9db5c307700b5b47dd49ddadb63423d957bb24d438a87d2 \ --hash=sha256:97dc27f4af966638c3f0146469f4158eefdf7bddf2fcd9512762ecae4a15c500 \
--hash=sha256:92cc60a9cfd027b0bbaeb588ab06577d58e2b1a41c824f069bd53544f0cccdf3 \ --hash=sha256:a70de3b625cd4cfc249bd24dd5292d1409e76e1d45344e83f408be0a24906862 \
--hash=sha256:97f05e8a38f0e1a872b623e094bd270760318c9ab947ff65359192c9a692bda1 \ --hash=sha256:ba06d579e61ac23a3ad61f9853dd3f5c219d1b4a3bfdda2b1e92ff5f2d952655 \
--hash=sha256:ae6f4c99a3978ff4fb1f537d16435d75a17f97ec84f61e3a9ac2b7b879b4dae8 \ --hash=sha256:bd436ddcd904f3410a87073275a4ad24f91e6c45ec32605f30122661aff63b73 \
--hash=sha256:b074a82f40a3ab4068e2f01697a65b6239db55a3335e5c2e9b2a630601c1aa05 \ --hash=sha256:bd670ef6716ac98888ebef423addf25aa921d99d7a8e8199bce59aa9c34525dd \
--hash=sha256:b2eb2c727cfa173a48b65fbfc67b170f47c5b28d483759a1fc26886b01770345 \ --hash=sha256:c10685eb7f8ef6e072709a766325816a2e2c6cc2e1e2d7fc744f4afc019db542 \
--hash=sha256:b5459ed202fcc7bdaaf619b4bd2718fc7ac7c5dea9c0be682f7e64bf145749e5 \ --hash=sha256:cb69358e22a7241e398273e16c9754c10a1f01a2f4e66eb3272b17be3016554e \
--hash=sha256:baa84b539fea0e6a925a9159c3e0a1d08cceeea5260732b84200e077444a4b0e \ --hash=sha256:cd7627ea178d8c132d8c6ddf048f636862f81e73a3b9d71942c6fd0ab0df4190 \
--hash=sha256:c0bbe69be332d4cee36f628ba5feaf731c6a53dbe1ea1cf40324a4954a92093a \ --hash=sha256:ce50b588981d30cd700fbf7352eea6f9aed9a9fc2823dda5a7d183a4e13d0ab8 \
--hash=sha256:c1731f45fd24b05a01ac32dc0f7e96337a3bd78ab33a230b2035a82f624d112e \ --hash=sha256:d86d028109b3fe24c1d5f87304f2078ae4b8813e954d742049755161bd14b2e6 \
--hash=sha256:caeb9740f6e0d5a3fa48e1a009dee2f99f47be1836c6bc252022aa25327fcb0e \ --hash=sha256:ed00537e01eb1533f2f06dab7c266ea166da9d9ffe88104fe81be84facdb9252 \
--hash=sha256:d8ad390efed25a4fad288f80449a2180bfdb17db19bed4916630c01d20084c4b \ --hash=sha256:efa3922dbf1b9694cf592682f722bbead9eebccc84fa8adf75d3a40b2cd8eb6c \
--hash=sha256:db89094df6567721ec1eae8a70f85afd22e07eefa86a1b11194247407a3426ee \ --hash=sha256:f837add96e55f1c55aa992606f766ba531c76f3b313d26ef0fb60cc398bef04d \
--hash=sha256:e7e9233686fd49c7fa311e1a9f769ce0fa9eb57e546b6ccd88d2dafb5d7cb6bd \ --hash=sha256:fa6832891409d59f9e4996df308794e20bed266428cc523ffbb6f515ff95fad4 \
--hash=sha256:f9d5710c8dbaefe6254bbefb409c612485e32d983df9a1299459987b13f2ac3f --hash=sha256:fe517a1a56c69a0dad252c391122d1d7ff063c7eb31066172396976aa99098a0
# via -r src/backend/requirements.in # via -r src/backend/requirements.in
et-xmlfile==2.0.0 \ et-xmlfile==2.0.0 \
--hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \
@@ -1818,15 +1818,15 @@ s3transfer==0.16.0 \
--hash=sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe \ --hash=sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe \
--hash=sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920 --hash=sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920
# via boto3 # via boto3
sentry-sdk==2.48.0 \ sentry-sdk==2.49.0 \
--hash=sha256:5213190977ff7fdff8a58b722fb807f8d5524a80488626ebeda1b5676c0c1473 \ --hash=sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0 \
--hash=sha256:6b12ac256769d41825d9b7518444e57fa35b5642df4c7c5e322af4d2c8721172 --hash=sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30
# via # via
# -r src/backend/requirements.in # -r src/backend/requirements.in
# django-q-sentry # django-q-sentry
setuptools==80.9.0 \ setuptools==80.10.2 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173
# via # via
# -r src/backend/requirements.in # -r src/backend/requirements.in
# django-money # django-money

View File

@@ -631,9 +631,9 @@ rich==14.2.0 \
--hash=sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4 \ --hash=sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4 \
--hash=sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd --hash=sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd
# via pytest-codspeed # via pytest-codspeed
setuptools==80.9.0 \ setuptools==80.10.2 \
--hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \
--hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173
# via # via
# -c src/backend/requirements-3.14.txt # -c src/backend/requirements-3.14.txt
# -r src/backend/requirements-dev.in # -r src/backend/requirements-dev.in