[FR] Update to OpenAPI from CoreAPI (#4178)

* [FR] Update to OpenAPI from CoreAPI
Fixes #3226

* factor request function out

* add schema export task

* add api-docs

* add action to check if diff occured

* also wait for docstyle

* use full command

* add envs for inventree

* update inventree before running

* use relative path

* remove schema action

* remove tags to fit 3.0 parsers

* fix url base name for reloads

* revert change in plugin resolver

* remove unused tags

* add rapidoc too

* declare api regex

* fix as suggested by @martonmiklos in
https://github.com/inventree/InvenTree/pull/4178#discussion_r1167279443

* set inventree logo

* remove Rapidoc
This commit is contained in:
Matthias Mair 2023-04-18 15:08:36 +02:00 committed by GitHub
parent b0f6021002
commit 9d5522c18c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 70 additions and 14 deletions

View File

@ -26,6 +26,7 @@ from sentry_sdk.integrations.django import DjangoIntegration
from . import config from . import config
from .config import get_boolean_setting, get_custom_file, get_setting from .config import get_boolean_setting, get_custom_file, get_setting
from .version import inventreeApiVersion
INVENTREE_NEWS_URL = 'https://inventree.org/news/feed.atom' INVENTREE_NEWS_URL = 'https://inventree.org/news/feed.atom'
@ -233,6 +234,7 @@ INSTALLED_APPS = [
'django_otp.plugins.otp_static', # Backup codes 'django_otp.plugins.otp_static', # Backup codes
'allauth_2fa', # MFA flow for allauth 'allauth_2fa', # MFA flow for allauth
'drf_spectacular', # API documentation
'django_ical', # For exporting calendars 'django_ical', # For exporting calendars
] ]
@ -356,7 +358,7 @@ REST_FRAMEWORK = {
'rest_framework.permissions.DjangoModelPermissions', 'rest_framework.permissions.DjangoModelPermissions',
'InvenTree.permissions.RolePermission', '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_METADATA_CLASS': 'InvenTree.metadata.InvenTreeMetadata',
'DEFAULT_RENDERER_CLASSES': [ 'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.JSONRenderer',
@ -367,6 +369,15 @@ if DEBUG:
# Enable browsable API if in DEBUG mode # Enable browsable API if in DEBUG mode
REST_FRAMEWORK['DEFAULT_RENDERER_CLASSES'].append('rest_framework.renderers.BrowsableAPIRenderer') 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' WSGI_APPLICATION = 'InvenTree.wsgi.application'
""" """

View File

@ -9,7 +9,7 @@ from django.contrib import admin
from django.urls import include, path, re_path from django.urls import include, path, re_path
from django.views.generic.base import RedirectView 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.api import build_api_urls
from build.urls import build_urls from build.urls import build_urls
@ -62,9 +62,12 @@ apipatterns = [
# Plugin endpoints # Plugin endpoints
path('', include(plugin_api_urls)), path('', include(plugin_api_urls)),
# Webhook endpoints # Common endpoints enpoint
path('', include(common_api_urls)), path('', include(common_api_urls)),
# OpenAPI Schema
re_path('schema/', SpectacularAPIView.as_view(custom_settings={'SCHEMA_PATH_PREFIX': '/api/'}), name='schema'),
# InvenTree information endpoint # InvenTree information endpoint
path('', InfoView.as_view(), name='api-inventree-info'), path('', InfoView.as_view(), name='api-inventree-info'),
@ -136,7 +139,7 @@ backendpatterns = [
re_path(r'^auth/?', auth_request), re_path(r'^auth/?', auth_request),
re_path(r'^api/', include(apipatterns)), 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 = [ frontendpatterns = [

View File

@ -9,7 +9,6 @@ import subprocess
import django import django
import common.models
from InvenTree.api_version import INVENTREE_API_VERSION from InvenTree.api_version import INVENTREE_API_VERSION
# InvenTree software version # InvenTree software version
@ -18,11 +17,15 @@ INVENTREE_SW_VERSION = "0.12.0 dev"
def inventreeInstanceName(): def inventreeInstanceName():
"""Returns the InstanceName settings for the current database.""" """Returns the InstanceName settings for the current database."""
import common.models
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "") return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
def inventreeInstanceTitle(): def inventreeInstanceTitle():
"""Returns the InstanceTitle for the current database.""" """Returns the InstanceTitle for the current database."""
import common.models
if common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE_TITLE", False): if common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE_TITLE", False):
return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "") return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
else: 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" 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) 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! # No record for "latest" version - we must assume we are up to date!

View File

@ -27,6 +27,7 @@ django-user-sessions # user sessions in DB
django-weasyprint # django weasyprint integration django-weasyprint # django weasyprint integration
djangorestframework # DRF framework djangorestframework # DRF framework
django-xforwardedfor-middleware # IP forwarding metadata django-xforwardedfor-middleware # IP forwarding metadata
drf-spectacular # DRF API documentation
feedparser # RSS newsfeed parser feedparser # RSS newsfeed parser
gunicorn # Gunicorn web server gunicorn # Gunicorn web server
pdf2image # PDF to image conversion pdf2image # PDF to image conversion

View File

@ -8,6 +8,8 @@ arrow==1.2.3
# via django-q # via django-q
asgiref==3.6.0 asgiref==3.6.0
# via django # via django
attrs==22.2.0
# via jsonschema
babel==2.12.1 babel==2.12.1
# via py-moneyed # via py-moneyed
bleach[css]==6.0.0 bleach[css]==6.0.0
@ -70,6 +72,7 @@ django==3.2.18
# django-weasyprint # django-weasyprint
# django-xforwardedfor-middleware # django-xforwardedfor-middleware
# djangorestframework # djangorestframework
# drf-spectacular
django-allauth==0.54.0 django-allauth==0.54.0
# via # via
# -r requirements.in # -r requirements.in
@ -129,6 +132,10 @@ django-weasyprint==2.2.0
django-xforwardedfor-middleware==2.0 django-xforwardedfor-middleware==2.0
# via -r requirements.in # via -r requirements.in
djangorestframework==3.14.0 djangorestframework==3.14.0
# via
# -r requirements.in
# drf-spectacular
drf-spectacular==0.25.1
# via -r requirements.in # via -r requirements.in
et-xmlfile==1.1.0 et-xmlfile==1.1.0
# via openpyxl # via openpyxl
@ -146,10 +153,14 @@ idna==3.4
# via requests # via requests
importlib-metadata==6.1.0 importlib-metadata==6.1.0
# via markdown # via markdown
inflection==0.5.1
# via drf-spectacular
itypes==1.2.0 itypes==1.2.0
# via coreapi # via coreapi
jinja2==3.1.2 jinja2==3.1.2
# via coreschema # via coreschema
jsonschema==4.17.3
# via drf-spectacular
markdown==3.4.3 markdown==3.4.3
# via django-markdownify # via django-markdownify
markuppy==1.14 markuppy==1.14
@ -186,6 +197,8 @@ pyphen==0.14.0
# via weasyprint # via weasyprint
pypng==0.20220715.0 pypng==0.20220715.0
# via qrcode # via qrcode
pyrsistent==0.19.3
# via jsonschema
python-barcode[images]==0.14.0 python-barcode[images]==0.14.0
# via -r requirements.in # via -r requirements.in
python-dateutil==2.8.2 python-dateutil==2.8.2
@ -204,7 +217,9 @@ pytz==2023.3
# djangorestframework # djangorestframework
# icalendar # icalendar
pyyaml==6.0 pyyaml==6.0
# via tablib # via
# drf-spectacular
# tablib
qrcode[pil]==7.4.2 qrcode[pil]==7.4.2
# via # via
# -r requirements.in # -r requirements.in
@ -252,7 +267,9 @@ tinycss2==1.1.1
typing-extensions==4.5.0 typing-extensions==4.5.0
# via qrcode # via qrcode
uritemplate==4.1.1 uritemplate==4.1.1
# via coreapi # via
# coreapi
# drf-spectacular
urllib3==1.26.15 urllib3==1.26.15
# via # via
# requests # requests

View File

@ -88,6 +88,22 @@ def manage(c, cmd, pty: bool = False):
), pty=pty) ), 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 # Install tasks
@task @task
def plugins(c): 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}'") print(f"Exporting database records to file '{filename}'")
if Path(filename).is_file() and overwrite is False: check_file_existance(filename, overwrite)
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)
tmpfile = f"{filename}.tmp" tmpfile = f"{filename}.tmp"
@ -621,3 +631,13 @@ def coverage(c):
# Generate coverage report # Generate coverage report
c.run('coverage html -i') 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}')