diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py index 8164e6bbae..f1c5aa8838 100644 --- a/InvenTree/InvenTree/settings.py +++ b/InvenTree/InvenTree/settings.py @@ -26,6 +26,7 @@ from sentry_sdk.integrations.django import DjangoIntegration from . import config from .config import get_boolean_setting, get_custom_file, get_setting +from .version import inventreeApiVersion INVENTREE_NEWS_URL = 'https://inventree.org/news/feed.atom' @@ -233,6 +234,7 @@ INSTALLED_APPS = [ 'django_otp.plugins.otp_static', # Backup codes 'allauth_2fa', # MFA flow for allauth + 'drf_spectacular', # API documentation 'django_ical', # For exporting calendars ] @@ -356,7 +358,7 @@ REST_FRAMEWORK = { 'rest_framework.permissions.DjangoModelPermissions', 'InvenTree.permissions.RolePermission', ), - 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata', 'DEFAULT_RENDERER_CLASSES': [ 'rest_framework.renderers.JSONRenderer', @@ -367,6 +369,15 @@ if DEBUG: # Enable browsable API if in DEBUG mode REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append('rest_framework.renderers.BrowsableAPIRenderer') +SPECTACULAR_SETTINGS = { + 'TITLE': 'InvenTree API', + 'DESCRIPTION': 'API for InvenTree - the intuitive open source inventory management system', + 'LICENSE': {'MIT': 'https://github.com/inventree/InvenTree/blob/master/LICENSE'}, + 'EXTERNAL_DOCS': {'docs': 'https://docs.inventree.org', 'web': 'https://inventree.org'}, + 'VERSION': inventreeApiVersion(), + 'SERVE_INCLUDE_SCHEMA': False, +} + WSGI_APPLICATION = 'InvenTree.wsgi.application' """ diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index ec2aeb13f2..83505ca06b 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -9,7 +9,7 @@ from django.contrib import admin from django.urls import include, path, re_path from django.views.generic.base import RedirectView -from rest_framework.documentation import include_docs_urls +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView from build.api import build_api_urls from build.urls import build_urls @@ -62,9 +62,12 @@ apipatterns = [ # Plugin endpoints path('', include(plugin_api_urls)), - # Webhook endpoints + # Common endpoints enpoint path('', include(common_api_urls)), + # OpenAPI Schema + re_path('schema/', SpectacularAPIView.as_view(custom_settings={'SCHEMA_PATH_PREFIX': '/api/'}), name='schema'), + # InvenTree information endpoint path('', InfoView.as_view(), name='api-inventree-info'), @@ -136,7 +139,7 @@ backendpatterns = [ re_path(r'^auth/?', auth_request), re_path(r'^api/', include(apipatterns)), - re_path(r'^api-doc/', include_docs_urls(title='InvenTree API')), + re_path(r'^api-doc/', SpectacularRedocView.as_view(url_name='schema'), name='api-doc'), ] frontendpatterns = [ diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 0a87428728..1d8923c5bb 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -9,7 +9,6 @@ import subprocess import django -import common.models from InvenTree.api_version import INVENTREE_API_VERSION # InvenTree software version @@ -18,11 +17,15 @@ INVENTREE_SW_VERSION = "0.12.0 dev" def inventreeInstanceName(): """Returns the InstanceName settings for the current database.""" + import common.models + return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "") def inventreeInstanceTitle(): """Returns the InstanceTitle for the current database.""" + import common.models + if common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE_TITLE", False): return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "") else: @@ -66,6 +69,7 @@ def isInvenTreeUpToDate(): A background task periodically queries GitHub for latest version, and stores it to the database as "_INVENTREE_LATEST_VERSION" """ + import common.models latest = common.models.InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION', backup_value=None, create=False) # No record for "latest" version - we must assume we are up to date! diff --git a/requirements.in b/requirements.in index bed603ad1e..5ab78a112a 100644 --- a/requirements.in +++ b/requirements.in @@ -27,6 +27,7 @@ django-user-sessions # user sessions in DB django-weasyprint # django weasyprint integration djangorestframework # DRF framework django-xforwardedfor-middleware # IP forwarding metadata +drf-spectacular # DRF API documentation feedparser # RSS newsfeed parser gunicorn # Gunicorn web server pdf2image # PDF to image conversion diff --git a/requirements.txt b/requirements.txt index ae38b49a5b..cbfc82c78e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,8 @@ arrow==1.2.3 # via django-q asgiref==3.6.0 # via django +attrs==22.2.0 + # via jsonschema babel==2.12.1 # via py-moneyed bleach[css]==6.0.0 @@ -70,6 +72,7 @@ django==3.2.18 # django-weasyprint # django-xforwardedfor-middleware # djangorestframework + # drf-spectacular django-allauth==0.54.0 # via # -r requirements.in @@ -129,6 +132,10 @@ django-weasyprint==2.2.0 django-xforwardedfor-middleware==2.0 # via -r requirements.in djangorestframework==3.14.0 + # via + # -r requirements.in + # drf-spectacular +drf-spectacular==0.25.1 # via -r requirements.in et-xmlfile==1.1.0 # via openpyxl @@ -146,10 +153,14 @@ idna==3.4 # via requests importlib-metadata==6.1.0 # via markdown +inflection==0.5.1 + # via drf-spectacular itypes==1.2.0 # via coreapi jinja2==3.1.2 # via coreschema +jsonschema==4.17.3 + # via drf-spectacular markdown==3.4.3 # via django-markdownify markuppy==1.14 @@ -186,6 +197,8 @@ pyphen==0.14.0 # via weasyprint pypng==0.20220715.0 # via qrcode +pyrsistent==0.19.3 + # via jsonschema python-barcode[images]==0.14.0 # via -r requirements.in python-dateutil==2.8.2 @@ -204,7 +217,9 @@ pytz==2023.3 # djangorestframework # icalendar pyyaml==6.0 - # via tablib + # via + # drf-spectacular + # tablib qrcode[pil]==7.4.2 # via # -r requirements.in @@ -252,7 +267,9 @@ tinycss2==1.1.1 typing-extensions==4.5.0 # via qrcode uritemplate==4.1.1 - # via coreapi + # via + # coreapi + # drf-spectacular urllib3==1.26.15 # via # requests diff --git a/tasks.py b/tasks.py index e6e602f133..a63dc73f6f 100644 --- a/tasks.py +++ b/tasks.py @@ -88,6 +88,22 @@ def manage(c, cmd, pty: bool = False): ), pty=pty) +def check_file_existance(filename: str, overwrite: bool = False): + """Checks if a file exists and asks the user if it should be overwritten. + + Args: + filename (str): Name of the file to check. + overwrite (bool, optional): Overwrite the file without asking. Defaults to False. + """ + if Path(filename).is_file() and overwrite is False: + response = input("Warning: file already exists. Do you want to overwrite? [y/N]: ") + response = str(response).strip().lower() + + if response not in ['y', 'yes']: + print("Cancelled export operation") + sys.exit(1) + + # Install tasks @task def plugins(c): @@ -305,13 +321,7 @@ def export_records(c, filename='data.json', overwrite=False, include_permissions print(f"Exporting database records to file '{filename}'") - if Path(filename).is_file() and overwrite is False: - response = input("Warning: file already exists. Do you want to overwrite? [y/N]: ") - response = str(response).strip().lower() - - if response not in ['y', 'yes']: - print("Cancelled export operation") - sys.exit(1) + check_file_existance(filename, overwrite) tmpfile = f"{filename}.tmp" @@ -621,3 +631,13 @@ def coverage(c): # Generate coverage report c.run('coverage html -i') + + +@task(help={ + 'filename': "Output filename (default = 'schema.yml')", + 'overwrite': "Overwrite existing files without asking first (default = off/False)", +}) +def schema(c, filename='schema.yml', overwrite=False): + """Export current API schema.""" + check_file_existance(filename, overwrite) + manage(c, f'spectacular --file {filename}')