blob: 1255e3be42219f7a1e7d7db91a2420b1152c8629 [file] [log] [blame]
#!/usr/bin/env python
# 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.
# /// script
# requires-python = ">=3.10,<3.11"
# dependencies = [
# "jinja2>=3.1.2",
# "pyyaml>=6.0.3",
# "rich>=13.6.0",
# ]
# ///
from __future__ import annotations
import sys
from collections import defaultdict
from pathlib import Path
from rich.console import Console
sys.path.insert(0, str(Path(__file__).parent.resolve())) # make sure common utils are importable
from common_prek_utils import (
AIRFLOW_CORE_SOURCES_PATH,
AIRFLOW_PROVIDERS_ROOT_PATH,
AIRFLOW_ROOT_PATH,
get_all_provider_info_dicts,
)
sys.path.insert(0, str(AIRFLOW_CORE_SOURCES_PATH)) # make sure setup is imported from Airflow
console = Console(color_system="standard", width=200)
AIRFLOW_PROVIDERS_IMPORT_PREFIX = "airflow.providers."
warnings: list[str] = []
errors: list[str] = []
suspended_paths: list[str] = []
ALL_DEPENDENCIES: dict[str, dict[str, list[str]]] = defaultdict(lambda: defaultdict(list))
LICENCE_CONTENT_RST = """
.. 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.
"""
SECURITY_CONTENT_RST = """
.. include:: /devel-common/src/sphinx_exts/includes/security.rst
"""
INSTALLING_PROVIDERS_FROM_SOURCES_CONTENT_RST = """
.. include:: .. include:: /../../../devel-common/src/sphinx_exts/includes/installing-providers-from-sources.rst
"""
COMMIT_CONTENT_RST = """
.. THIS FILE IS UPDATED AUTOMATICALLY_AT_RELEASE_TIME
"""
failed: list[bool] = []
def process_content_to_write(content: str) -> str:
"""Allow content to be defined with leading empty lines and strip/add EOL"""
if not content:
return content
content_lines = content.splitlines()
if content_lines and content_lines[0] == "":
content_lines = content_lines[1:]
content_to_write = "\n".join(content_lines) + "\n"
return content_to_write
def get_provider_doc_folder(provider_id: str) -> Path:
return AIRFLOW_PROVIDERS_ROOT_PATH.joinpath(*provider_id.split(".")) / "docs"
def check_provider_doc_exists_and_in_index(
*,
provider_id: str,
index_link: str,
file_name: str,
generated_content: str = "",
missing_ok: bool = False,
check_content: bool = True,
):
console.print(f" [bright_blue]Checking [/]: {file_name} ", end="")
fail = False
provider_docs_file = get_provider_doc_folder(provider_id)
file_path = provider_docs_file / file_name
index_file = provider_docs_file / "index.rst"
content_to_write = process_content_to_write(generated_content)
regenerate_file = False
if file_path.exists():
if check_content and not generated_content:
if file_path.read_text() != content_to_write:
console.print("[yellow]Generating[/]")
console.print()
console.print(f"[yellow]Content of the file will be regenerated: [/]{file_path}")
console.print()
regenerate_file = True
else:
if missing_ok:
# Fine - we do not check anything else
console.print("[green]OK[/]")
failed.append(False)
return
if not generated_content:
console.print("[yellow]Generating[/]")
console.print()
console.print(f"[yellow]Missing file: [/]{file_path}")
console.print("[bright_blue]Please create the file looking at other providers as example [/]")
console.print()
else:
regenerate_file = True
if regenerate_file:
fail = True
file_path.write_text(content_to_write)
console.print()
console.print(f"[yellow]Content updated in file: [/]{file_path}")
console.print()
if index_link not in index_file.read_text():
console.print("[red]NOK[/]")
fail = True
console.print()
console.print(
f"[red]ERROR! Missing index link![/]\n"
f"The index file: {index_file} should have this link:\n"
f"{index_link}\n\n"
f"[bright_blue]Please add the entry in the index!"
)
console.print()
if not fail:
console.print("[green]OK[/]")
failed.append(fail)
def check_documentation_link_exists(link: str, docs_file: Path):
console.print(f" [bright_blue]Checking [/]: {docs_file} for link: {link} ", end="")
if link not in docs_file.read_text():
console.print("[red]NOK[/]")
console.print()
console.print(
f"[red]ERROR! The {docs_file} does not contain:\n:[/]{link}\n[bright_blue]Please add it!"
)
console.print()
failed.append(True)
console.print("[green]OK[/]")
failed.append(False)
def has_executor_package_defined(provider_id: str) -> bool:
provider_distribution_path = (
AIRFLOW_PROVIDERS_ROOT_PATH.joinpath(*provider_id.split(".")) / "src"
).joinpath(*provider_id.split("."))
for executors_folder in provider_distribution_path.rglob("executors"):
if executors_folder.is_dir() and (executors_folder / "__init__.py").is_file():
return True
return False
def run_all_checks():
all_providers = get_all_provider_info_dicts()
status: list[bool] = []
for provider_id, provider_info in all_providers.items():
console.print(f"[bright_blue]Checking provider: {provider_id}[/]")
provider_docs_folder = get_provider_doc_folder(provider_id)
if not provider_docs_folder.exists():
provider_docs_folder.mkdir(parents=True)
check_provider_doc_exists_and_in_index(
provider_id=provider_id,
index_link="Detailed list of commits <commits>",
file_name="commits.rst",
generated_content=LICENCE_CONTENT_RST + COMMIT_CONTENT_RST,
# Only create commit content if it is missing, otherwise leave what is there
check_content=False,
)
check_provider_doc_exists_and_in_index(
provider_id=provider_id,
index_link="Security <security>",
file_name="security.rst",
generated_content=LICENCE_CONTENT_RST + SECURITY_CONTENT_RST,
)
check_provider_doc_exists_and_in_index(
provider_id=provider_id,
index_link="Installing from sources <installing-providers-from-sources>",
file_name="installing-providers-from-sources.rst",
generated_content=LICENCE_CONTENT_RST + INSTALLING_PROVIDERS_FROM_SOURCES_CONTENT_RST,
)
if has_executor_package_defined(provider_id) and not provider_info.get("executors"):
provider_yaml = AIRFLOW_PROVIDERS_ROOT_PATH.joinpath(*provider_id.split(".")) / "provider.yaml"
console.print()
console.print(
f"[red]ERROR! The {provider_id} provider has executor package but "
f"does not have `executors` defined in {provider_yaml}."
)
console.print(f"\nPlease add executor class to `executors` array in {provider_yaml}")
status.append(False)
if provider_info.get("executors"):
check_provider_doc_exists_and_in_index(
provider_id=provider_id,
index_link="CLI <cli-ref>",
file_name="cli-ref.rst",
missing_ok=True,
check_content=False,
)
if (get_provider_doc_folder(provider_id) / "cli-ref.rst").exists():
check_documentation_link_exists(
link=f"and related CLI commands: :doc:`apache-airflow-providers-{provider_id.replace('.', '-')}:cli-ref`",
docs_file=AIRFLOW_ROOT_PATH / "airflow-core" / "docs" / "cli-and-env-variables-ref.rst",
)
print(failed)
if any(failed):
sys.exit(1)
if __name__ == "__main__":
run_all_checks()