Skip to content

Commit

Permalink
Deprecate experimental API (apache#9888)
Browse files Browse the repository at this point in the history
  • Loading branch information
mik-laj authored Jul 20, 2020
1 parent 5013fda commit 9126f70
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 11 deletions.
31 changes: 31 additions & 0 deletions airflow/utils/docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

from typing import Optional

from airflow import version


def get_docs_url(page: Optional[str] = None) -> str:
"""Prepare link to Airflow documentation."""
if "dev" in version.version:
result = "https://airflow.readthedocs.io/en/latest/"
else:
result = 'https://airflow.apache.org/docs/{}/'.format(version.version)
if page:
result = result + page
return result
22 changes: 21 additions & 1 deletion airflow/www/api/experimental/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from functools import wraps
from typing import Callable, TypeVar, cast

from flask import Blueprint, current_app, g, jsonify, request, url_for
from flask import Blueprint, Response, current_app, g, jsonify, request, url_for

from airflow import models
from airflow.api.common.experimental import delete_dag as delete, pool as pool_api, trigger_dag as trigger
Expand All @@ -31,6 +31,7 @@
from airflow.api.common.experimental.get_task_instance import get_task_instance
from airflow.exceptions import AirflowException
from airflow.utils import timezone
from airflow.utils.docs import get_docs_url
from airflow.utils.strings import to_boolean
from airflow.version import version

Expand All @@ -51,6 +52,25 @@ def decorated(*args, **kwargs):
api_experimental = Blueprint('api_experimental', __name__)


def add_deprecation_headers(response: Response):
"""
Add `Deprecation HTTP Header Field
<https://tools.ietf.org/id/draft-dalal-deprecation-header-03.html>`__.
"""
response.headers['Deprecation'] = 'true'
doc_url = get_docs_url("stable-rest-api/migration.html")
deprecation_link = f'<{doc_url}>; rel="deprecation"; type="text/html"'
if 'link' in response.headers:
response.headers['Link'] += f', {deprecation_link}'
else:
response.headers['Link'] = f'{deprecation_link}'

return response


api_experimental.after_request(add_deprecation_headers)


@api_experimental.route('/dags/<string:dag_id>/dag_runs', methods=['POST'])
@requires_authentication
def trigger_dag(dag_id):
Expand Down
8 changes: 2 additions & 6 deletions airflow/www/extensions/init_appbuilder_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,17 @@
# specific language governing permissions and limitations
# under the License.

from airflow import version
from airflow.utils.docs import get_docs_url


def init_appbuilder_links(app):
"""Add links to Docs menu in navbar"""
appbuilder = app.appbuilder
if "dev" in version.version:
doc_site = "https://airflow.readthedocs.io/en/latest"
else:
doc_site = 'https://airflow.apache.org/docs/{}'.format(version.version)

appbuilder.add_link(
"Website", href='https://airflow.apache.org', category="Docs", category_icon="fa-globe"
)
appbuilder.add_link("Documentation", href=doc_site, category="Docs", category_icon="fa-cube")
appbuilder.add_link("Documentation", href=get_docs_url(), category="Docs", category_icon="fa-cube")
appbuilder.add_link("GitHub", href='https://github.com/apache/airflow', category="Docs")
appbuilder.add_link(
"REST API Reference (Swagger UI)", href='/api/v1./api/v1_swagger_ui_index', category="Docs"
Expand Down
4 changes: 2 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ Content
kubernetes
lineage
dag-serialization
Using the REST API <stable-rest-api/index.rst>
REST API Migration Guide <stable-rest-api/migration.rst>
Using the REST API <stable-rest-api/index>
REST API Migration Guide <stable-rest-api/migration>
changelog
best-practices
faq
Expand Down
5 changes: 3 additions & 2 deletions docs/rest-api-ref.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ Experimental REST API Reference
Airflow exposes an REST API. It is available through the webserver. Endpoints are
available at ``/api/experimental/``.

.. warning::
.. deprecated:: 2.0

The API structure is not stable. We expect the endpoint definitions to change.
This REST API is deprecated. Please consider using :doc:`the stable REST API <stable-rest-api/redoc>`.
For more information on migration, see: :doc:`stable-rest-api/migration`.

Endpoints
---------
Expand Down
35 changes: 35 additions & 0 deletions tests/utils/test_docs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

import unittest
from unittest import mock

from parameterized import parameterized

from airflow.utils.docs import get_docs_url


class TestGetDocsUrl(unittest.TestCase):
@parameterized.expand([
('2.0.0.dev0', None, 'https://airflow.readthedocs.io/en/latest/'),
('2.0.0.dev0', 'migration.html', 'https://airflow.readthedocs.io/en/latest/migration.html'),
('1.10.0', None, 'https://airflow.apache.org/docs/1.10.0/'),
('1.10.0', 'migration.html', 'https://airflow.apache.org/docs/1.10.0/migration.html'),
])
def test_should_return_link(self, version, page, expected_urk):
with mock.patch('airflow.version.version', version):
self.assertEqual(expected_urk, get_docs_url(page))
22 changes: 22 additions & 0 deletions tests/www/api/experimental/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ def setUp(self):
settings.configure_orm()
self.session = Session

def assert_deprecated(self, resp):
self.assertEqual('true', resp.headers['Deprecation'])
self.assertRegex(
resp.headers['Link'],
r'\<.+/stable-rest-api/migration.html\>; '
'rel="deprecation"; type="text/html"',
)


@parameterized_class([
{"dag_serialization": "False"},
Expand Down Expand Up @@ -89,6 +97,7 @@ def test_info(self):
resp = json.loads(resp_raw.data.decode('utf-8'))

self.assertEqual(version, resp['version'])
self.assert_deprecated(resp_raw)

def test_task_info(self):
with conf_vars(
Expand All @@ -99,6 +108,8 @@ def test_task_info(self):
response = self.client.get(
url_template.format('example_bash_operator', 'runme_0')
)
self.assert_deprecated(response)

self.assertIn('"email"', response.data.decode('utf-8'))
self.assertNotIn('error', response.data.decode('utf-8'))
self.assertEqual(200, response.status_code)
Expand All @@ -124,6 +135,7 @@ def test_get_dag_code(self):
response = self.client.get(
url_template.format('example_bash_operator')
)
self.assert_deprecated(response)
self.assertIn('BashOperator(', response.data.decode('utf-8'))
self.assertEqual(200, response.status_code)

Expand All @@ -143,6 +155,7 @@ def test_dag_paused(self):
response = self.client.get(
pause_url_template.format('example_bash_operator', 'true')
)
self.assert_deprecated(response)
self.assertIn('ok', response.data.decode('utf-8'))
self.assertEqual(200, response.status_code)

Expand Down Expand Up @@ -173,6 +186,7 @@ def test_trigger_dag(self):
data=json.dumps({'run_id': run_id}),
content_type="application/json"
)
self.assert_deprecated(response)

self.assertEqual(200, response.status_code)
response_execution_date = parse_datetime(
Expand Down Expand Up @@ -211,6 +225,7 @@ def test_trigger_dag_for_date(self):
data=json.dumps({'execution_date': datetime_string}),
content_type="application/json"
)
self.assert_deprecated(response)
self.assertEqual(200, response.status_code)
self.assertEqual(datetime_string, json.loads(response.data.decode('utf-8'))['execution_date'])

Expand Down Expand Up @@ -277,6 +292,7 @@ def test_task_instance_info(self):
response = self.client.get(
url_template.format(dag_id, datetime_string, task_id)
)
self.assert_deprecated(response)
self.assertEqual(200, response.status_code)
self.assertIn('state', response.data.decode('utf-8'))
self.assertNotIn('error', response.data.decode('utf-8'))
Expand Down Expand Up @@ -331,6 +347,7 @@ def test_dagrun_status(self):
response = self.client.get(
url_template.format(dag_id, datetime_string)
)
self.assert_deprecated(response)
self.assertEqual(200, response.status_code)
self.assertIn('state', response.data.decode('utf-8'))
self.assertNotIn('error', response.data.decode('utf-8'))
Expand Down Expand Up @@ -401,6 +418,7 @@ def test_lineage_info(self):
response = self.client.get(
url_template.format(dag_id, datetime_string)
)
self.assert_deprecated(response)
self.assertEqual(200, response.status_code)
self.assertIn('task_ids', response.data.decode('utf-8'))
self.assertNotIn('error', response.data.decode('utf-8'))
Expand Down Expand Up @@ -461,6 +479,7 @@ def test_get_pool(self):
response = self.client.get(
'/api/experimental/pools/{}'.format(self.pool.pool),
)
self.assert_deprecated(response)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.data.decode('utf-8')),
self.pool.to_json())
Expand All @@ -473,6 +492,7 @@ def test_get_pool_non_existing(self):

def test_get_pools(self):
response = self.client.get('/api/experimental/pools')
self.assert_deprecated(response)
self.assertEqual(response.status_code, 200)
pools = json.loads(response.data.decode('utf-8'))
self.assertEqual(len(pools), self.TOTAL_POOL_COUNT)
Expand All @@ -489,6 +509,7 @@ def test_create_pool(self):
}),
content_type='application/json',
)
self.assert_deprecated(response)
self.assertEqual(response.status_code, 200)
pool = json.loads(response.data.decode('utf-8'))
self.assertEqual(pool['pool'], 'foo')
Expand Down Expand Up @@ -518,6 +539,7 @@ def test_delete_pool(self):
response = self.client.delete(
'/api/experimental/pools/{}'.format(self.pool.pool),
)
self.assert_deprecated(response)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.data.decode('utf-8')),
self.pool.to_json())
Expand Down

0 comments on commit 9126f70

Please sign in to comment.