diff --git a/.github/workflows/api_schema_check.yml b/.github/workflows/api_schema_check.yml index dc8c628791..d2f940c360 100644 --- a/.github/workflows/api_schema_check.yml +++ b/.github/workflows/api_schema_check.yml @@ -47,7 +47,7 @@ jobs: - name: Add schema diff to job summary if: always() - # show text and if for some reason, it can't be generated, state that it can't be. + # show text and if for some reason, it can't be generated, state that it can't be. run: | echo "## API Schema Change Detection Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -55,12 +55,18 @@ jobs: if grep -q "^+" schema-diff.txt || grep -q "^-" schema-diff.txt; then echo "### Schema changes detected" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + # Truncate to first 1000 lines to stay under GitHub's 1MB summary limit + TOTAL_LINES=$(wc -l < schema-diff.txt) + if [ $TOTAL_LINES -gt 1000 ]; then + echo "_Showing first 1000 of ${TOTAL_LINES} lines. See job logs or download artifact for full diff._" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + fi echo '```diff' >> $GITHUB_STEP_SUMMARY - cat schema-diff.txt >> $GITHUB_STEP_SUMMARY + head -n 1000 schema-diff.txt >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY else echo "### No schema changes detected" >> $GITHUB_STEP_SUMMARY fi else - echo "### Unable to generate schema diff" >> $GITHUB_STEP_SUMMARY + echo "### Unable to generate schema diff" >> $GITHUB_STEP_SUMMARY fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e59cfc30c6..091d9d5829 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,6 @@ jobs: - name: api-lint command: /var/lib/awx/venv/awx/bin/tox -e linters coverage-upload-name: "" - - name: api-swagger - command: /start_tests.sh swagger - coverage-upload-name: "" - name: awx-collection command: /start_tests.sh test_collection_all coverage-upload-name: "awx-collection" diff --git a/.gitignore b/.gitignore index 4ea996b190..713db26264 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Ignore generated schema swagger.json schema.json +schema.yaml reference-schema.json # Tags diff --git a/Makefile b/Makefile index 51c9af9d74..2e5727590c 100644 --- a/Makefile +++ b/Makefile @@ -316,20 +316,17 @@ black: reports @echo "fi" >> .git/hooks/pre-commit @chmod +x .git/hooks/pre-commit -genschema: reports - $(MAKE) swagger PYTEST_ADDOPTS="--genschema --create-db " - mv swagger.json schema.json - -swagger: reports +genschema: awx-link reports @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/awx/bin/activate; \ fi; \ - (set -o pipefail && py.test $(COVERAGE_ARGS) $(PARALLEL_TESTS) awx/conf/tests/functional awx/main/tests/functional/api awx/main/tests/docs | tee reports/$@.report) - @if [ "${GITHUB_ACTIONS}" = "true" ]; \ - then \ - echo 'cov-report-files=reports/coverage.xml' >> "${GITHUB_OUTPUT}"; \ - echo 'test-result-files=reports/junit.xml' >> "${GITHUB_OUTPUT}"; \ - fi + $(MANAGEMENT_COMMAND) spectacular --format openapi-json --file schema.json + +genschema-yaml: awx-link reports + @if [ "$(VENV_BASE)" ]; then \ + . $(VENV_BASE)/awx/bin/activate; \ + fi; \ + $(MANAGEMENT_COMMAND) spectacular --format openapi --file schema.yaml check: black @@ -539,14 +536,15 @@ docker-compose-test: awx/projects docker-compose-sources docker-compose-runtest: awx/projects docker-compose-sources $(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports awx_1 /start_tests.sh -docker-compose-build-swagger: awx/projects docker-compose-sources - $(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports --no-deps awx_1 /start_tests.sh swagger +docker-compose-build-schema: awx/projects docker-compose-sources + $(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml run --rm --service-ports --no-deps awx_1 make genschema SCHEMA_DIFF_BASE_BRANCH ?= devel detect-schema-change: genschema curl https://s3.amazonaws.com/awx-public-ci-files/$(SCHEMA_DIFF_BASE_BRANCH)/schema.json -o reference-schema.json # Ignore differences in whitespace with -b - diff -u -b reference-schema.json schema.json + # diff exits with 1 when files differ - capture but don't fail + -diff -u -b reference-schema.json schema.json docker-compose-clean: awx/projects $(DOCKER_COMPOSE) -f tools/docker-compose/_sources/docker-compose.yml rm -sf diff --git a/awx/api/generics.py b/awx/api/generics.py index bfb3da5774..ad14074852 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -161,16 +161,14 @@ def get_view_description(view, html=False): def get_default_schema(): - if settings.DYNACONF.is_development_mode: - from awx.api.swagger import schema_view - - return schema_view - else: - return views.APIView.schema + # drf-spectacular is configured via REST_FRAMEWORK['DEFAULT_SCHEMA_CLASS'] + # Just use the DRF default, which will pick up our CustomAutoSchema + return views.APIView.schema class APIView(views.APIView): - schema = get_default_schema() + # Schema is inherited from DRF's APIView, which uses DEFAULT_SCHEMA_CLASS + # No need to override it here - drf-spectacular will handle it versioning_class = URLPathVersioning def initialize_request(self, request, *args, **kwargs): diff --git a/awx/api/swagger.py b/awx/api/schema.py similarity index 56% rename from awx/api/swagger.py rename to awx/api/schema.py index 16423875ff..b0a7592c7f 100644 --- a/awx/api/swagger.py +++ b/awx/api/schema.py @@ -1,15 +1,17 @@ import warnings -from rest_framework.permissions import AllowAny -from drf_yasg import openapi -from drf_yasg.inspectors import SwaggerAutoSchema -from drf_yasg.views import get_schema_view +from drf_spectacular.openapi import AutoSchema +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularSwaggerView, + SpectacularRedocView, +) -class CustomSwaggerAutoSchema(SwaggerAutoSchema): - """Custom SwaggerAutoSchema to add swagger_topic to tags.""" +class CustomAutoSchema(AutoSchema): + """Custom AutoSchema to add swagger_topic to tags and handle deprecated endpoints.""" - def get_tags(self, operation_keys=None): + def get_tags(self): tags = [] try: if hasattr(self.view, 'get_serializer'): @@ -21,19 +23,22 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema): warnings.warn( '{}.get_serializer() raised an exception during ' 'schema generation. Serializer fields will not be ' - 'generated for {}.'.format(self.view.__class__.__name__, operation_keys) + 'generated for this view.'.format(self.view.__class__.__name__) ) + if hasattr(self.view, 'swagger_topic'): tags.append(str(self.view.swagger_topic).title()) - elif serializer and hasattr(serializer, 'Meta'): + elif serializer and hasattr(serializer, 'Meta') and hasattr(serializer.Meta, 'model'): tags.append(str(serializer.Meta.model._meta.verbose_name_plural).title()) elif hasattr(self.view, 'model'): tags.append(str(self.view.model._meta.verbose_name_plural).title()) else: - tags = ['api'] # Fallback to default value + tags = super().get_tags() # Use default drf-spectacular behavior if not tags: warnings.warn(f'Could not determine tags for {self.view.__class__.__name__}') + tags = ['api'] # Fallback to default value + return tags def is_deprecated(self): @@ -41,15 +46,11 @@ class CustomSwaggerAutoSchema(SwaggerAutoSchema): return getattr(self.view, 'deprecated', False) -schema_view = get_schema_view( - openapi.Info( - title='AWX API', - default_version='v2', - description='AWX API Documentation', - terms_of_service='https://www.google.com/policies/terms/', - contact=openapi.Contact(email='contact@snippets.local'), - license=openapi.License(name='Apache License'), - ), - public=True, - permission_classes=[AllowAny], -) +# Schema view (returns OpenAPI schema JSON/YAML) +schema_view = SpectacularAPIView.as_view() + +# Swagger UI view +swagger_ui_view = SpectacularSwaggerView.as_view(url_name='api:schema-json') + +# ReDoc UI view +redoc_view = SpectacularRedocView.as_view(url_name='api:schema-json') diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index 909a7f0287..a88e6c95c3 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -4,7 +4,6 @@ from __future__ import absolute_import, unicode_literals from django.urls import include, re_path -from awx import MODE from awx.api.generics import LoggedLoginView, LoggedLogoutView from awx.api.views.root import ( ApiRootView, @@ -148,21 +147,21 @@ v2_urls = [ app_name = 'api' + +# Import schema views (needed for both development and testing) +from awx.api.schema import schema_view, swagger_ui_view, redoc_view + urlpatterns = [ re_path(r'^$', ApiRootView.as_view(), name='api_root_view'), re_path(r'^(?P(v2))/', include(v2_urls)), re_path(r'^login/$', LoggedLoginView.as_view(template_name='rest_framework/login.html', extra_context={'inside_login_context': True}), name='login'), re_path(r'^logout/$', LoggedLogoutView.as_view(next_page='/api/', redirect_field_name='next'), name='logout'), + # Schema endpoints (available in all modes for API documentation and testing) + re_path(r'^schema/$', schema_view, name='schema-json'), + re_path(r'^swagger/$', swagger_ui_view, name='schema-swagger-ui'), + re_path(r'^redoc/$', redoc_view, name='schema-redoc'), ] -if MODE == 'development': - # Only include these if we are in the development environment - from awx.api.swagger import schema_view - from awx.api.urls.debug import urls as debug_urls +from awx.api.urls.debug import urls as debug_urls - urlpatterns += [re_path(r'^debug/', include(debug_urls))] - urlpatterns += [ - re_path(r'^swagger(?P\.json|\.yaml)/$', schema_view.without_ui(cache_timeout=0), name='schema-json'), - re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), - re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), - ] +urlpatterns += [re_path(r'^debug/', include(debug_urls))] diff --git a/awx/main/tests/docs/test_swagger_generation.py b/awx/main/tests/docs/test_swagger_generation.py index f05435c1e3..f374e7642f 100644 --- a/awx/main/tests/docs/test_swagger_generation.py +++ b/awx/main/tests/docs/test_swagger_generation.py @@ -7,7 +7,6 @@ from django.core.serializers.json import DjangoJSONEncoder from django.utils.functional import Promise from django.utils.encoding import force_str -from drf_yasg.codecs import OpenAPICodecJson import pytest from awx.api.versioning import drf_reverse @@ -43,10 +42,10 @@ class TestSwaggerGeneration: @pytest.fixture(autouse=True, scope='function') def _prepare(self, get, admin): if not self.__class__.JSON: - url = drf_reverse('api:schema-swagger-ui') + '?format=openapi' + # drf-spectacular returns OpenAPI schema directly from schema endpoint + url = drf_reverse('api:schema-json') + '?format=json' response = get(url, user=admin) - codec = OpenAPICodecJson([]) - data = codec.generate_swagger_object(response.data) + data = response.data if response.has_header('X-Deprecated-Paths'): data['deprecated_paths'] = json.loads(response['X-Deprecated-Paths']) diff --git a/awx/main/tests/unit/api/test_schema.py b/awx/main/tests/unit/api/test_schema.py new file mode 100644 index 0000000000..ee6923ea4c --- /dev/null +++ b/awx/main/tests/unit/api/test_schema.py @@ -0,0 +1,250 @@ +import warnings +from unittest.mock import Mock, patch + +from awx.api.schema import CustomAutoSchema + + +class TestCustomAutoSchema: + """Unit tests for CustomAutoSchema class.""" + + def test_get_tags_with_swagger_topic(self): + """Test get_tags returns swagger_topic when available.""" + view = Mock() + view.swagger_topic = 'custom_topic' + view.get_serializer = Mock(return_value=Mock()) + + schema = CustomAutoSchema() + schema.view = view + + tags = schema.get_tags() + assert tags == ['Custom_Topic'] + + def test_get_tags_with_serializer_meta_model(self): + """Test get_tags returns model verbose_name_plural from serializer.""" + # Create a mock model with verbose_name_plural + mock_model = Mock() + mock_model._meta.verbose_name_plural = 'test models' + + # Create a mock serializer with Meta.model + mock_serializer = Mock() + mock_serializer.Meta.model = mock_model + + view = Mock(spec=[]) # View without swagger_topic + view.get_serializer = Mock(return_value=mock_serializer) + + schema = CustomAutoSchema() + schema.view = view + + tags = schema.get_tags() + assert tags == ['Test Models'] + + def test_get_tags_with_view_model(self): + """Test get_tags returns model verbose_name_plural from view.""" + # Create a mock model with verbose_name_plural + mock_model = Mock() + mock_model._meta.verbose_name_plural = 'view models' + + view = Mock(spec=['model']) # View without swagger_topic or get_serializer + view.model = mock_model + + schema = CustomAutoSchema() + schema.view = view + + tags = schema.get_tags() + assert tags == ['View Models'] + + def test_get_tags_without_get_serializer(self): + """Test get_tags when view doesn't have get_serializer method.""" + mock_model = Mock() + mock_model._meta.verbose_name_plural = 'test objects' + + view = Mock(spec=['model']) + view.model = mock_model + + schema = CustomAutoSchema() + schema.view = view + + tags = schema.get_tags() + assert tags == ['Test Objects'] + + def test_get_tags_serializer_exception_with_warning(self): + """Test get_tags handles exception in get_serializer with warning.""" + mock_model = Mock() + mock_model._meta.verbose_name_plural = 'fallback models' + + view = Mock(spec=['get_serializer', 'model', '__class__']) + view.__class__.__name__ = 'TestView' + view.get_serializer = Mock(side_effect=Exception('Serializer error')) + view.model = mock_model + + schema = CustomAutoSchema() + schema.view = view + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + tags = schema.get_tags() + + # Check that a warning was raised + assert len(w) == 1 + assert 'TestView.get_serializer() raised an exception' in str(w[0].message) + + # Should still get tags from view.model + assert tags == ['Fallback Models'] + + def test_get_tags_serializer_without_meta_model(self): + """Test get_tags when serializer doesn't have Meta.model.""" + mock_serializer = Mock(spec=[]) # No Meta attribute + + view = Mock(spec=['get_serializer']) + view.__class__.__name__ = 'NoMetaView' + view.get_serializer = Mock(return_value=mock_serializer) + + schema = CustomAutoSchema() + schema.view = view + + with patch.object(CustomAutoSchema.__bases__[0], 'get_tags', return_value=['Default Tag']) as mock_super: + tags = schema.get_tags() + mock_super.assert_called_once() + assert tags == ['Default Tag'] + + def test_get_tags_fallback_to_super(self): + """Test get_tags falls back to parent class method.""" + view = Mock(spec=['get_serializer']) + view.get_serializer = Mock(return_value=Mock(spec=[])) + + schema = CustomAutoSchema() + schema.view = view + + with patch.object(CustomAutoSchema.__bases__[0], 'get_tags', return_value=['Super Tag']) as mock_super: + tags = schema.get_tags() + mock_super.assert_called_once() + assert tags == ['Super Tag'] + + def test_get_tags_empty_with_warning(self): + """Test get_tags returns 'api' fallback when no tags can be determined.""" + view = Mock(spec=['get_serializer']) + view.__class__.__name__ = 'EmptyView' + view.get_serializer = Mock(return_value=Mock(spec=[])) + + schema = CustomAutoSchema() + schema.view = view + + with patch.object(CustomAutoSchema.__bases__[0], 'get_tags', return_value=[]): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + tags = schema.get_tags() + + # Check that a warning was raised + assert len(w) == 1 + assert 'Could not determine tags for EmptyView' in str(w[0].message) + + # Should fallback to 'api' + assert tags == ['api'] + + def test_get_tags_swagger_topic_title_case(self): + """Test that swagger_topic is properly title-cased.""" + view = Mock() + view.swagger_topic = 'multi_word_topic' + view.get_serializer = Mock(return_value=Mock()) + + schema = CustomAutoSchema() + schema.view = view + + tags = schema.get_tags() + assert tags == ['Multi_Word_Topic'] + + def test_is_deprecated_true(self): + """Test is_deprecated returns True when view has deprecated=True.""" + view = Mock() + view.deprecated = True + + schema = CustomAutoSchema() + schema.view = view + + assert schema.is_deprecated() is True + + def test_is_deprecated_false(self): + """Test is_deprecated returns False when view has deprecated=False.""" + view = Mock() + view.deprecated = False + + schema = CustomAutoSchema() + schema.view = view + + assert schema.is_deprecated() is False + + def test_is_deprecated_missing_attribute(self): + """Test is_deprecated returns False when view doesn't have deprecated attribute.""" + view = Mock(spec=[]) + + schema = CustomAutoSchema() + schema.view = view + + assert schema.is_deprecated() is False + + def test_get_tags_serializer_meta_without_model(self): + """Test get_tags when serializer has Meta but no model attribute.""" + mock_serializer = Mock() + mock_serializer.Meta = Mock(spec=[]) # Meta exists but no model + + mock_model = Mock() + mock_model._meta.verbose_name_plural = 'backup models' + + view = Mock(spec=['get_serializer', 'model']) + view.get_serializer = Mock(return_value=mock_serializer) + view.model = mock_model + + schema = CustomAutoSchema() + schema.view = view + + tags = schema.get_tags() + # Should fall back to view.model + assert tags == ['Backup Models'] + + def test_get_tags_complex_scenario_exception_recovery(self): + """Test complex scenario where serializer fails but view.model exists.""" + mock_model = Mock() + mock_model._meta.verbose_name_plural = 'recovery models' + + view = Mock(spec=['get_serializer', 'model', '__class__']) + view.__class__.__name__ = 'ComplexView' + view.get_serializer = Mock(side_effect=ValueError('Invalid serializer')) + view.model = mock_model + + schema = CustomAutoSchema() + schema.view = view + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + tags = schema.get_tags() + + # Should have warned about the exception + assert len(w) == 1 + assert 'ComplexView.get_serializer() raised an exception' in str(w[0].message) + + # But still recovered and got tags from view.model + assert tags == ['Recovery Models'] + + def test_get_tags_priority_order(self): + """Test that get_tags respects priority: swagger_topic > serializer.Meta.model > view.model.""" + # Set up a view with all three options + mock_model_view = Mock() + mock_model_view._meta.verbose_name_plural = 'view models' + + mock_model_serializer = Mock() + mock_model_serializer._meta.verbose_name_plural = 'serializer models' + + mock_serializer = Mock() + mock_serializer.Meta.model = mock_model_serializer + + view = Mock() + view.swagger_topic = 'priority_topic' + view.get_serializer = Mock(return_value=mock_serializer) + view.model = mock_model_view + + schema = CustomAutoSchema() + schema.view = view + + tags = schema.get_tags() + # swagger_topic should take priority + assert tags == ['Priority_Topic'] diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 63a45c311c..19ba236679 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -375,15 +375,13 @@ REST_FRAMEWORK = { 'VIEW_DESCRIPTION_FUNCTION': 'awx.api.generics.get_view_description', 'NON_FIELD_ERRORS_KEY': '__all__', 'DEFAULT_VERSION': 'v2', - # For swagger schema generation + # For OpenAPI schema generation with drf-spectacular # see https://github.com/encode/django-rest-framework/pull/6532 - 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.AutoSchema', + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', # 'URL_FORMAT_OVERRIDE': None, } -SWAGGER_SETTINGS = { - 'DEFAULT_AUTO_SCHEMA_CLASS': 'awx.api.swagger.CustomSwaggerAutoSchema', -} +# SWAGGER_SETTINGS removed - migrated to drf-spectacular (see SPECTACULAR_SETTINGS below) AUTHENTICATION_BACKENDS = ('awx.main.backends.AWXModelBackend',) @@ -1036,7 +1034,44 @@ ANSIBLE_BASE_RESOURCE_CONFIG_MODULE = 'awx.resource_api' ANSIBLE_BASE_PERMISSION_MODEL = 'main.Permission' # Defaults to be overridden by DAB -SPECTACULAR_SETTINGS = {} +SPECTACULAR_SETTINGS = { + 'TITLE': 'AWX API', + 'DESCRIPTION': 'AWX API Documentation', + 'VERSION': 'v2', + 'OAS_VERSION': '3.0.3', # Set OpenAPI Specification version to 3.0.3 + 'SERVE_INCLUDE_SCHEMA': False, + 'SCHEMA_PATH_PREFIX': r'/api/v[0-9]', + 'DEFAULT_GENERATOR_CLASS': 'drf_spectacular.generators.SchemaGenerator', + 'SCHEMA_COERCE_PATH_PK_SUFFIX': True, + 'CONTACT': {'email': 'contact@snippets.local'}, + 'LICENSE': {'name': 'Apache License'}, + 'TERMS_OF_SERVICE': 'https://www.google.com/policies/terms/', + # Use our custom schema class that handles swagger_topic and deprecated views + 'DEFAULT_SCHEMA_CLASS': 'awx.api.schema.CustomAutoSchema', + 'COMPONENT_SPLIT_REQUEST': True, + 'SWAGGER_UI_SETTINGS': { + 'deepLinking': True, + 'persistAuthorization': True, + 'displayOperationId': True, + }, + # Resolve enum naming collisions with meaningful names + 'ENUM_NAME_OVERRIDES': { + # Status field collisions + 'Status4e1Enum': 'UnifiedJobStatusEnum', + 'Status876Enum': 'JobStatusEnum', + # Job type field collisions + 'JobType8b8Enum': 'JobTemplateJobTypeEnum', + 'JobType95bEnum': 'AdHocCommandJobTypeEnum', + 'JobType963Enum': 'ProjectUpdateJobTypeEnum', + # Verbosity field collisions + 'Verbosity481Enum': 'JobVerbosityEnum', + 'Verbosity8cfEnum': 'InventoryUpdateVerbosityEnum', + # Event field collision + 'Event4d3Enum': 'JobEventEnum', + # Kind field collision + 'Kind362Enum': 'InventoryKindEnum', + }, +} OAUTH2_PROVIDER = {} # Add a postfix to the API URL patterns diff --git a/awx/settings/development_defaults.py b/awx/settings/development_defaults.py index 9725b0abb7..1e5ecff78a 100644 --- a/awx/settings/development_defaults.py +++ b/awx/settings/development_defaults.py @@ -41,11 +41,14 @@ PENDO_TRACKING_STATE = "off" INSIGHTS_TRACKING_STATE = False # debug toolbar and swagger assume that requirements/requirements_dev.txt are installed -INSTALLED_APPS = "@merge drf_yasg,debug_toolbar" +INSTALLED_APPS = "@merge drf_spectacular,debug_toolbar" MIDDLEWARE = "@insert 0 debug_toolbar.middleware.DebugToolbarMiddleware" DEBUG_TOOLBAR_CONFIG = {'ENABLE_STACKTRACES': True} +# drf-spectacular settings for API schema generation +# SPECTACULAR_SETTINGS moved to defaults.py so it's available in all environments + # Configure a default UUID for development only. SYSTEM_UUID = '00000000-0000-0000-0000-000000000000' INSTALL_UUID = '00000000-0000-0000-0000-000000000000' diff --git a/licenses/drf-spectacular.txt b/licenses/drf-spectacular.txt new file mode 100644 index 0000000000..76001f6264 --- /dev/null +++ b/licenses/drf-spectacular.txt @@ -0,0 +1,30 @@ +Copyright © 2011-present, Encode OSS Ltd. +Copyright © 2019-2021, T. Franzel , Cashlink Technologies GmbH. +Copyright © 2021-present, T. Franzel . + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/uritemplate.txt b/licenses/uritemplate.txt new file mode 100644 index 0000000000..7060b3c98e --- /dev/null +++ b/licenses/uritemplate.txt @@ -0,0 +1,23 @@ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. The name of the author may not be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/pytest.ini b/pytest.ini index 78e499f93c..b26eb5ab75 100644 --- a/pytest.ini +++ b/pytest.ini @@ -23,7 +23,8 @@ filterwarnings = # NOTE: the following are present using python 3.11 # FIXME: Set `USE_TZ` to `True`. - once:The default value of USE_TZ will change from False to True in Django 5.0. Set USE_TZ to False in your project settings if you want to keep the current default behavior.:django.utils.deprecation.RemovedInDjango50Warning:django.conf + # Note: RemovedInDjango50Warning may not exist in newer Django versions + ignore:The default value of USE_TZ will change from False to True in Django 5.0. Set USE_TZ to False in your project settings if you want to keep the current default behavior. # FIXME: Delete this entry once `pyparsing` is updated. once:module 'sre_constants' is deprecated:DeprecationWarning:_pytest.assertion.rewrite @@ -46,11 +47,12 @@ filterwarnings = once:DateTimeField User.date_joined received a naive datetime .2020-01-01 00.00.00. while time zone support is active.:RuntimeWarning:django.db.models.fields # FIXME: Delete this entry once the deprecation is acted upon. - once:'index_together' is deprecated. Use 'Meta.indexes' in 'main.\w+' instead.:django.utils.deprecation.RemovedInDjango51Warning:django.db.models.options + # Note: RemovedInDjango51Warning may not exist in newer Django versions + ignore:'index_together' is deprecated. Use 'Meta.indexes' in 'main.\w+' instead. # FIXME: Update `awx.main.migrations._dab_rbac` and delete this entry. - # once:Using QuerySet.iterator.. after prefetch_related.. without specifying chunk_size is deprecated.:django.utils.deprecation.RemovedInDjango50Warning:django.db.models.query - once:Using QuerySet.iterator.. after prefetch_related.. without specifying chunk_size is deprecated.:django.utils.deprecation.RemovedInDjango50Warning:awx.main.migrations._dab_rbac + # Note: RemovedInDjango50Warning may not exist in newer Django versions + ignore:Using QuerySet.iterator.. after prefetch_related.. without specifying chunk_size is deprecated. # FIXME: Delete this entry once the **broken** always-true assertions in the # FIXME: following tests are fixed: diff --git a/requirements/requirements.in b/requirements/requirements.in index b449ee2e72..93e47d8298 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -22,6 +22,7 @@ django-polymorphic django-solo djangorestframework==3.15.2 # upgrading to 3.16+ throws NOT_REQUIRED_DEFAULT error on required fields in serializer that have no default djangorestframework-yaml +drf-spectacular>=0.27.0 dynaconf filelock GitPython>=3.1.37 # CVE-2023-41040 diff --git a/requirements/requirements.txt b/requirements/requirements.txt index db687fc7c5..5a0746e320 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -137,6 +137,7 @@ django==4.2.21 # django-polymorphic # django-solo # djangorestframework + # drf-spectacular # django-ansible-base @ git+https://github.com/ansible/django-ansible-base@devel # git requirements installed separately # via -r /awx_devel/requirements/requirements_git.txt django-cors-headers==4.9.0 @@ -161,8 +162,11 @@ djangorestframework==3.15.2 # via # -r /awx_devel/requirements/requirements.in # django-ansible-base + # drf-spectacular djangorestframework-yaml==2.0.0 # via -r /awx_devel/requirements/requirements.in +drf-spectacular==0.29.0 + # via -r /awx_devel/requirements/requirements.in durationpy==0.10 # via kubernetes dynaconf==3.2.11 @@ -211,7 +215,9 @@ importlib-resources==6.5.2 incremental==24.7.2 # via twisted inflection==0.5.1 - # via django-ansible-base + # via + # django-ansible-base + # drf-spectacular irc==20.5.0 # via -r /awx_devel/requirements/requirements.in isodate==0.7.2 @@ -248,7 +254,9 @@ jq==1.10.0 json-log-formatter==1.1.1 # via -r /awx_devel/requirements/requirements.in jsonschema==4.25.1 - # via -r /awx_devel/requirements/requirements.in + # via + # -r /awx_devel/requirements/requirements.in + # drf-spectacular jsonschema-specifications==2025.9.1 # via jsonschema kubernetes==34.1.0 @@ -418,6 +426,7 @@ pyyaml==6.0.3 # ansible-runner # dispatcherd # djangorestframework-yaml + # drf-spectacular # kubernetes # receptorctl pyzstd==0.18.0 @@ -517,6 +526,8 @@ typing-extensions==4.15.0 # pyzstd # referencing # twisted +uritemplate==4.2.0 + # via drf-spectacular urllib3==2.3.0 # via # -r /awx_devel/requirements/requirements.in diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 3ccd4b4275..92ef46f576 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -1,7 +1,7 @@ build django-debug-toolbar==3.2.4 django-test-migrations -drf-yasg<1.21.10 # introduces new DeprecationWarning that is turned into error +drf-spectacular>=0.27.0 # Modern OpenAPI 3.0 schema generator # pprofile - re-add once https://github.com/vpelletier/pprofile/issues/41 is addressed ipython>=7.31.1 # https://github.com/ansible/awx/security/dependabot/30 unittest2