From 620e69be4d41910325440f7a467dbaf611b1587a Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Thu, 29 Jan 2026 06:31:58 +0100 Subject: [PATCH] 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 --- docs/docs/develop/contributing.md | 14 ++++ docs/docs/start/config.md | 1 + src/backend/InvenTree/InvenTree/schema.py | 55 +++++++++++++-- src/backend/InvenTree/InvenTree/settings.py | 3 + src/backend/InvenTree/config_template.yaml | 4 ++ src/backend/requirements-3.14.txt | 78 ++++++++++----------- src/backend/requirements-dev-3.14.txt | 6 +- 7 files changed, 115 insertions(+), 46 deletions(-) diff --git a/docs/docs/develop/contributing.md b/docs/docs/develop/contributing.md index 479e6813c6..d3a2f03595 100644 --- a/docs/docs/develop/contributing.md +++ b/docs/docs/develop/contributing.md @@ -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. +### 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 ### Software Versions diff --git a/docs/docs/start/config.md b/docs/docs/start/config.md index d3eaa04bb6..a9ca613347 100644 --- a/docs/docs/start/config.md +++ b/docs/docs/start/config.md @@ -100,6 +100,7 @@ The following debugging / logging options are available: | 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_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 diff --git a/src/backend/InvenTree/InvenTree/schema.py b/src/backend/InvenTree/InvenTree/schema.py index 095dbe15d4..8ec7392130 100644 --- a/src/backend/InvenTree/InvenTree/schema.py +++ b/src/backend/InvenTree/InvenTree/schema.py @@ -89,12 +89,13 @@ class ExtendedAutoSchema(AutoSchema): operation['requestBody'] = request_body 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 # what the schema defines to be the expected result. This forces limit to be present, producing the expected # type. pagination_class = getattr(self.view, 'pagination_class', None) if pagination_class and pagination_class == LimitOffsetPagination: - parameters = operation.get('parameters', []) for parameter in parameters: if parameter['name'] == 'limit': parameter['required'] = True @@ -102,7 +103,6 @@ class ExtendedAutoSchema(AutoSchema): # Add valid order selections to the ordering field description. ordering_fields = getattr(self.view, 'ordering_fields', None) if ordering_fields is not None: - parameters = operation.get('parameters', []) for parameter in parameters: if parameter['name'] == 'ordering': schema_order = [] @@ -117,8 +117,6 @@ class ExtendedAutoSchema(AutoSchema): if search_fields is not None: # Ensure consistent ordering of search fields search_fields = sorted(search_fields) - - parameters = operation.get('parameters', []) for parameter in parameters: if parameter['name'] == 'search': parameter['description'] = ( @@ -135,8 +133,57 @@ class ExtendedAutoSchema(AutoSchema): schema['items'] = {'$ref': schema['$ref']} del schema['$ref'] + # Add vendor extensions for custom behavior + operation.update(self.get_inventree_extensions()) + 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): """Override call to drf-spectacular's enum postprocessor to filter out specific warnings.""" diff --git a/src/backend/InvenTree/InvenTree/settings.py b/src/backend/InvenTree/InvenTree/settings.py index b72860bd33..ffd17fcdb1 100644 --- a/src/backend/InvenTree/InvenTree/settings.py +++ b/src/backend/InvenTree/InvenTree/settings.py @@ -1495,6 +1495,9 @@ LOGIN_REDIRECT_URL = '/api/auth/login-redirect/' # Configuration for API schema generation / oAuth2 SPECTACULAR_SETTINGS = spectacular.get_spectacular_settings() +SCHEMA_VENDOREXTENSION_LEVEL = get_setting( + 'INVENTREE_SCHEMA_LEVEL', 'schema.level', default_value=0, typecast=int +) OAUTH2_PROVIDER = { # default scopes diff --git a/src/backend/InvenTree/config_template.yaml b/src/backend/InvenTree/config_template.yaml index a65509f072..37ce2b8555 100644 --- a/src/backend/InvenTree/config_template.yaml +++ b/src/backend/InvenTree/config_template.yaml @@ -40,6 +40,10 @@ debug_silk: False debug_silk_profiling: 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 #admin_enabled: True diff --git a/src/backend/requirements-3.14.txt b/src/backend/requirements-3.14.txt index 00d13bbc43..e0812a9288 100644 --- a/src/backend/requirements-3.14.txt +++ b/src/backend/requirements-3.14.txt @@ -535,9 +535,9 @@ django-cors-headers==4.9.0 \ --hash=sha256:15c7f20727f90044dcee2216a9fd7303741a864865f0c3657e28b7056f61b449 \ --hash=sha256:fe5d7cb59fdc2c8c646ce84b727ac2bca8912a247e6e68e1fb507372178e59e8 # via -r src/backend/requirements.in -django-dbbackup==5.1.0 \ - --hash=sha256:611291606bf6a80903733a99235963e65236c23bca26c2edca453b928b504c67 \ - --hash=sha256:66c236bbfa0c9bda33a61d30be8c5961d70fa73fed2fe7f829559ac216354130 +django-dbbackup==5.1.1 \ + --hash=sha256:371d82803743f6963d7d6d44ee145472213ddd287c43576ddbc11f2277972c1d \ + --hash=sha256:bfefa6fcf64a602fd5416bc7fff679509bf7eee35c114eee295730df8e9589c9 # via -r src/backend/requirements.in django-error-report-2==0.4.2 \ --hash=sha256:1dd99c497af09b7ea99f5fbaf910501838150a9d5390796ea00e187bc62f6c1b \ @@ -582,9 +582,9 @@ django-mptt==0.18.0 \ --hash=sha256:bfa3f01627e3966a1df901aeca74570a3e933e66809ebf58d9df673e63627afb \ --hash=sha256:cf5661357ff22bc64e20d3341c26e538aa54583aea0763cfe6aaec0ab8e3a8ee # via -r src/backend/requirements.in -django-oauth-toolkit==3.1.0 \ - --hash=sha256:10ddc90804297d913dfb958edd58d5fac541eb1ca912f47893ca1e482bb2a11f \ - --hash=sha256:d5a59d07588cfefa8818e99d65040a252eb2ede22512483e2240c91d0b885c8e +django-oauth-toolkit==3.2.0 \ + --hash=sha256:bd2cd2719b010231a2f370f927dbcc740454fb1d0dd7e7f4138f36227363dc26 \ + --hash=sha256:c36761ae6810083d95a652e9c820046cde0d45a2e2a5574bbe7202656ec20bb6 # via -r src/backend/requirements.in django-otp==1.3.0 \ --hash=sha256:5277731bc05b6cdbf96aa84ac46018e30ed5fb248086053b0146f925de059060 \ @@ -658,33 +658,33 @@ drf-spectacular==0.29.0 \ --hash=sha256:0a069339ea390ce7f14a75e8b5af4a0860a46e833fd4af027411a3e94fc1a0cc \ --hash=sha256:d1ee7c9535d89848affb4427347f7c4a22c5d22530b8842ef133d7b72e19b41a # via -r src/backend/requirements.in -dulwich==0.25.0 \ - --hash=sha256:14c9aba34e1ac262806174304a5a17a78a0f83d0a6960e506005d3aa1cf9004e \ - --hash=sha256:1575e7bf93cbc9ae93d6653fe29962357b96a1f5943275ff55cbb772e61359e2 \ - --hash=sha256:3d342daf24cc544f1ccc7e6cf6b8b22d10a4381c1c7ed2bf0e2024a48be9218f \ - --hash=sha256:47f0328af2c0e5149f356b27d1ac5b2860049c29bf32d2e5994d33f879909dd6 \ - --hash=sha256:4a98628ae4150f5084e0e0eab884c967d9f499304ff220f558ebe523868fd564 \ - --hash=sha256:4b46836c467bd898fd2ff1d4ebe511d2956f7f3f181dccbdde8631d4031cd0fa \ - --hash=sha256:63846a66254dd89bec7b3df75dda61fc37f9c53aa93cddf46d063a9e1f832634 \ - --hash=sha256:6ca746bd4f8a6a7b849a759c34e960dd7b6fa573225a571e23ea9c73377175d2 \ - --hash=sha256:757ab788d2d87d96e4b5e84eaddc32d7b8e5b57a221f43b8cbb694787a9c1b80 \ - --hash=sha256:7b88ef0402ce2a94db5ae926e6be8048e59e8cdcc889a71e332d0e7bcc59f8b7 \ - --hash=sha256:83e1cbff47ce1dc7d44a20f624c0d2fcbc6a70a458c5fe8e0f8bbf84f32aeb1c \ - --hash=sha256:866dcf6103ca4dddf9db5c307700b5b47dd49ddadb63423d957bb24d438a87d2 \ - --hash=sha256:92cc60a9cfd027b0bbaeb588ab06577d58e2b1a41c824f069bd53544f0cccdf3 \ - --hash=sha256:97f05e8a38f0e1a872b623e094bd270760318c9ab947ff65359192c9a692bda1 \ - --hash=sha256:ae6f4c99a3978ff4fb1f537d16435d75a17f97ec84f61e3a9ac2b7b879b4dae8 \ - --hash=sha256:b074a82f40a3ab4068e2f01697a65b6239db55a3335e5c2e9b2a630601c1aa05 \ - --hash=sha256:b2eb2c727cfa173a48b65fbfc67b170f47c5b28d483759a1fc26886b01770345 \ - --hash=sha256:b5459ed202fcc7bdaaf619b4bd2718fc7ac7c5dea9c0be682f7e64bf145749e5 \ - --hash=sha256:baa84b539fea0e6a925a9159c3e0a1d08cceeea5260732b84200e077444a4b0e \ - --hash=sha256:c0bbe69be332d4cee36f628ba5feaf731c6a53dbe1ea1cf40324a4954a92093a \ - --hash=sha256:c1731f45fd24b05a01ac32dc0f7e96337a3bd78ab33a230b2035a82f624d112e \ - --hash=sha256:caeb9740f6e0d5a3fa48e1a009dee2f99f47be1836c6bc252022aa25327fcb0e \ - --hash=sha256:d8ad390efed25a4fad288f80449a2180bfdb17db19bed4916630c01d20084c4b \ - --hash=sha256:db89094df6567721ec1eae8a70f85afd22e07eefa86a1b11194247407a3426ee \ - --hash=sha256:e7e9233686fd49c7fa311e1a9f769ce0fa9eb57e546b6ccd88d2dafb5d7cb6bd \ - --hash=sha256:f9d5710c8dbaefe6254bbefb409c612485e32d983df9a1299459987b13f2ac3f +dulwich==0.25.1 \ + --hash=sha256:06009f7f86cf3d3cdad3ad113c8667b7d2e7bd6606638462dc10f58fc5e647c3 \ + --hash=sha256:1316bfd979f708a894bb953c6a1618762169ce7b2db0ea58edffb0e1fc3733ce \ + --hash=sha256:2ffa8166321cf84a1aa31d091c74f11480831347c5a89f8082b2ad519c40d8ae \ + --hash=sha256:529da7c6dfea3e404a792044f3a45a5eec6d0095f2d3c3f66ab578825a6a5c2d \ + --hash=sha256:5c394bcfea736b380b1e5490b28f791f21769bae7aa198c4bd200ad7aa94b9bd \ + --hash=sha256:6c20393b9e68f68bd772d008ad9a0ce92c9e4cbdc98ab04953e3af668b5dd7e1 \ + --hash=sha256:6d796c5f5c1e3951a7422046419414f88c4bf282ffb29b0e9b5e7ff821a9ac81 \ + --hash=sha256:7170de4380e1e0fd1016e567fb932d94f28577fc6a259fd2e25bc25fc86a416b \ + --hash=sha256:7b4534dffe836d5654a54cc4ecaecfd909846f065ae148a6fdde3d8dd8091552 \ + --hash=sha256:849195ae2cc245ae2e1b2d67859582e923a7e363f5f9ae5212029429762d9901 \ + --hash=sha256:8c7756d6f22d11da42fc803584edb290b2cd2a72a6460834644349c739dccab1 \ + --hash=sha256:97dc27f4af966638c3f0146469f4158eefdf7bddf2fcd9512762ecae4a15c500 \ + --hash=sha256:a70de3b625cd4cfc249bd24dd5292d1409e76e1d45344e83f408be0a24906862 \ + --hash=sha256:ba06d579e61ac23a3ad61f9853dd3f5c219d1b4a3bfdda2b1e92ff5f2d952655 \ + --hash=sha256:bd436ddcd904f3410a87073275a4ad24f91e6c45ec32605f30122661aff63b73 \ + --hash=sha256:bd670ef6716ac98888ebef423addf25aa921d99d7a8e8199bce59aa9c34525dd \ + --hash=sha256:c10685eb7f8ef6e072709a766325816a2e2c6cc2e1e2d7fc744f4afc019db542 \ + --hash=sha256:cb69358e22a7241e398273e16c9754c10a1f01a2f4e66eb3272b17be3016554e \ + --hash=sha256:cd7627ea178d8c132d8c6ddf048f636862f81e73a3b9d71942c6fd0ab0df4190 \ + --hash=sha256:ce50b588981d30cd700fbf7352eea6f9aed9a9fc2823dda5a7d183a4e13d0ab8 \ + --hash=sha256:d86d028109b3fe24c1d5f87304f2078ae4b8813e954d742049755161bd14b2e6 \ + --hash=sha256:ed00537e01eb1533f2f06dab7c266ea166da9d9ffe88104fe81be84facdb9252 \ + --hash=sha256:efa3922dbf1b9694cf592682f722bbead9eebccc84fa8adf75d3a40b2cd8eb6c \ + --hash=sha256:f837add96e55f1c55aa992606f766ba531c76f3b313d26ef0fb60cc398bef04d \ + --hash=sha256:fa6832891409d59f9e4996df308794e20bed266428cc523ffbb6f515ff95fad4 \ + --hash=sha256:fe517a1a56c69a0dad252c391122d1d7ff063c7eb31066172396976aa99098a0 # via -r src/backend/requirements.in et-xmlfile==2.0.0 \ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \ @@ -1818,15 +1818,15 @@ s3transfer==0.16.0 \ --hash=sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe \ --hash=sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920 # via boto3 -sentry-sdk==2.48.0 \ - --hash=sha256:5213190977ff7fdff8a58b722fb807f8d5524a80488626ebeda1b5676c0c1473 \ - --hash=sha256:6b12ac256769d41825d9b7518444e57fa35b5642df4c7c5e322af4d2c8721172 +sentry-sdk==2.49.0 \ + --hash=sha256:6ea78499133874445a20fe9c826c9e960070abeb7ae0cdf930314ab16bb97aa0 \ + --hash=sha256:c1878599cde410d481c04ef50ee3aedd4f600e4d0d253f4763041e468b332c30 # via # -r src/backend/requirements.in # django-q-sentry -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 # via # -r src/backend/requirements.in # django-money diff --git a/src/backend/requirements-dev-3.14.txt b/src/backend/requirements-dev-3.14.txt index c3d86c86de..ad44be1e6a 100644 --- a/src/backend/requirements-dev-3.14.txt +++ b/src/backend/requirements-dev-3.14.txt @@ -631,9 +631,9 @@ rich==14.2.0 \ --hash=sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4 \ --hash=sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd # via pytest-codspeed -setuptools==80.9.0 \ - --hash=sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922 \ - --hash=sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c +setuptools==80.10.2 \ + --hash=sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70 \ + --hash=sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173 # via # -c src/backend/requirements-3.14.txt # -r src/backend/requirements-dev.in