mirror of https://github.com/tiangolo/fastapi.git
♻️ Refactor and move `scripts/notify_translations.py`, no need for a custom GitHub Action (#13270)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
8525b879ed
commit
326fec16b9
|
|
@ -1,7 +0,0 @@
|
||||||
FROM python:3.9
|
|
||||||
|
|
||||||
RUN pip install httpx PyGithub "pydantic==1.5.1" "pyyaml>=5.3.1,<6.0.0"
|
|
||||||
|
|
||||||
COPY ./app /app
|
|
||||||
|
|
||||||
CMD ["python", "/app/main.py"]
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
name: "Notify Translations"
|
|
||||||
description: "Notify in the issue for a translation when there's a new PR available"
|
|
||||||
author: "Sebastián Ramírez <tiangolo@gmail.com>"
|
|
||||||
inputs:
|
|
||||||
token:
|
|
||||||
description: 'Token, to read the GitHub API. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
|
|
||||||
required: true
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'Dockerfile'
|
|
||||||
|
|
@ -15,15 +15,14 @@ on:
|
||||||
required: false
|
required: false
|
||||||
default: 'false'
|
default: 'false'
|
||||||
|
|
||||||
permissions:
|
|
||||||
discussions: write
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
UV_SYSTEM_PYTHON: 1
|
UV_SYSTEM_PYTHON: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
notify-translations:
|
job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
discussions: write
|
||||||
steps:
|
steps:
|
||||||
- name: Dump GitHub context
|
- name: Dump GitHub context
|
||||||
env:
|
env:
|
||||||
|
|
@ -42,12 +41,19 @@ jobs:
|
||||||
cache-dependency-glob: |
|
cache-dependency-glob: |
|
||||||
requirements**.txt
|
requirements**.txt
|
||||||
pyproject.toml
|
pyproject.toml
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: uv pip install -r requirements-github-actions.txt
|
||||||
# Allow debugging with tmate
|
# Allow debugging with tmate
|
||||||
- name: Setup tmate session
|
- name: Setup tmate session
|
||||||
uses: mxschmitt/action-tmate@v3
|
uses: mxschmitt/action-tmate@v3
|
||||||
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.debug_enabled == 'true' }}
|
||||||
with:
|
with:
|
||||||
limit-access-to-actor: true
|
limit-access-to-actor: true
|
||||||
- uses: ./.github/actions/notify-translations
|
env:
|
||||||
with:
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
- name: Notify Translations
|
||||||
|
run: python ./scripts/notify_translations.py
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
NUMBER: ${{ github.event.inputs.number || null }}
|
||||||
|
DEBUG: ${{ github.event.inputs.debug_enabled || 'false' }}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,13 @@ from typing import Any, Dict, List, Union, cast
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from github import Github
|
from github import Github
|
||||||
from pydantic import BaseModel, BaseSettings, SecretStr
|
from pydantic import BaseModel, SecretStr
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
awaiting_label = "awaiting-review"
|
awaiting_label = "awaiting-review"
|
||||||
lang_all_label = "lang-all"
|
lang_all_label = "lang-all"
|
||||||
approved_label = "approved-1"
|
approved_label = "approved-1"
|
||||||
translations_path = Path(__file__).parent / "translations.yml"
|
|
||||||
|
|
||||||
github_graphql_url = "https://api.github.com/graphql"
|
github_graphql_url = "https://api.github.com/graphql"
|
||||||
questions_translations_category_id = "DIC_kwDOCZduT84CT5P9"
|
questions_translations_category_id = "DIC_kwDOCZduT84CT5P9"
|
||||||
|
|
@ -176,19 +177,20 @@ class AllDiscussionsResponse(BaseModel):
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
github_repository: str
|
github_repository: str
|
||||||
input_token: SecretStr
|
github_token: SecretStr
|
||||||
github_event_path: Path
|
github_event_path: Path
|
||||||
github_event_name: Union[str, None] = None
|
github_event_name: Union[str, None] = None
|
||||||
httpx_timeout: int = 30
|
httpx_timeout: int = 30
|
||||||
input_debug: Union[bool, None] = False
|
debug: Union[bool, None] = False
|
||||||
|
number: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class PartialGitHubEventIssue(BaseModel):
|
class PartialGitHubEventIssue(BaseModel):
|
||||||
number: int
|
number: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class PartialGitHubEvent(BaseModel):
|
class PartialGitHubEvent(BaseModel):
|
||||||
pull_request: PartialGitHubEventIssue
|
pull_request: PartialGitHubEventIssue | None = None
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_response(
|
def get_graphql_response(
|
||||||
|
|
@ -202,9 +204,7 @@ def get_graphql_response(
|
||||||
comment_id: Union[str, None] = None,
|
comment_id: Union[str, None] = None,
|
||||||
body: Union[str, None] = None,
|
body: Union[str, None] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
headers = {"Authorization": f"token {settings.input_token.get_secret_value()}"}
|
headers = {"Authorization": f"token {settings.github_token.get_secret_value()}"}
|
||||||
# some fields are only used by one query, but GraphQL allows unused variables, so
|
|
||||||
# keep them here for simplicity
|
|
||||||
variables = {
|
variables = {
|
||||||
"after": after,
|
"after": after,
|
||||||
"category_id": category_id,
|
"category_id": category_id,
|
||||||
|
|
@ -228,37 +228,40 @@ def get_graphql_response(
|
||||||
data = response.json()
|
data = response.json()
|
||||||
if "errors" in data:
|
if "errors" in data:
|
||||||
logging.error(f"Errors in response, after: {after}, category_id: {category_id}")
|
logging.error(f"Errors in response, after: {after}, category_id: {category_id}")
|
||||||
|
logging.error(data["errors"])
|
||||||
logging.error(response.text)
|
logging.error(response.text)
|
||||||
raise RuntimeError(response.text)
|
raise RuntimeError(response.text)
|
||||||
return cast(Dict[str, Any], data)
|
return cast(Dict[str, Any], data)
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_translation_discussions(*, settings: Settings):
|
def get_graphql_translation_discussions(
|
||||||
|
*, settings: Settings
|
||||||
|
) -> List[AllDiscussionsDiscussionNode]:
|
||||||
data = get_graphql_response(
|
data = get_graphql_response(
|
||||||
settings=settings,
|
settings=settings,
|
||||||
query=all_discussions_query,
|
query=all_discussions_query,
|
||||||
category_id=questions_translations_category_id,
|
category_id=questions_translations_category_id,
|
||||||
)
|
)
|
||||||
graphql_response = AllDiscussionsResponse.parse_obj(data)
|
graphql_response = AllDiscussionsResponse.model_validate(data)
|
||||||
return graphql_response.data.repository.discussions.nodes
|
return graphql_response.data.repository.discussions.nodes
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_translation_discussion_comments_edges(
|
def get_graphql_translation_discussion_comments_edges(
|
||||||
*, settings: Settings, discussion_number: int, after: Union[str, None] = None
|
*, settings: Settings, discussion_number: int, after: Union[str, None] = None
|
||||||
):
|
) -> List[CommentsEdge]:
|
||||||
data = get_graphql_response(
|
data = get_graphql_response(
|
||||||
settings=settings,
|
settings=settings,
|
||||||
query=translation_discussion_query,
|
query=translation_discussion_query,
|
||||||
discussion_number=discussion_number,
|
discussion_number=discussion_number,
|
||||||
after=after,
|
after=after,
|
||||||
)
|
)
|
||||||
graphql_response = CommentsResponse.parse_obj(data)
|
graphql_response = CommentsResponse.model_validate(data)
|
||||||
return graphql_response.data.repository.discussion.comments.edges
|
return graphql_response.data.repository.discussion.comments.edges
|
||||||
|
|
||||||
|
|
||||||
def get_graphql_translation_discussion_comments(
|
def get_graphql_translation_discussion_comments(
|
||||||
*, settings: Settings, discussion_number: int
|
*, settings: Settings, discussion_number: int
|
||||||
):
|
) -> list[Comment]:
|
||||||
comment_nodes: List[Comment] = []
|
comment_nodes: List[Comment] = []
|
||||||
discussion_edges = get_graphql_translation_discussion_comments_edges(
|
discussion_edges = get_graphql_translation_discussion_comments_edges(
|
||||||
settings=settings, discussion_number=discussion_number
|
settings=settings, discussion_number=discussion_number
|
||||||
|
|
@ -276,43 +279,49 @@ def get_graphql_translation_discussion_comments(
|
||||||
return comment_nodes
|
return comment_nodes
|
||||||
|
|
||||||
|
|
||||||
def create_comment(*, settings: Settings, discussion_id: str, body: str):
|
def create_comment(*, settings: Settings, discussion_id: str, body: str) -> Comment:
|
||||||
data = get_graphql_response(
|
data = get_graphql_response(
|
||||||
settings=settings,
|
settings=settings,
|
||||||
query=add_comment_mutation,
|
query=add_comment_mutation,
|
||||||
discussion_id=discussion_id,
|
discussion_id=discussion_id,
|
||||||
body=body,
|
body=body,
|
||||||
)
|
)
|
||||||
response = AddCommentResponse.parse_obj(data)
|
response = AddCommentResponse.model_validate(data)
|
||||||
return response.data.addDiscussionComment.comment
|
return response.data.addDiscussionComment.comment
|
||||||
|
|
||||||
|
|
||||||
def update_comment(*, settings: Settings, comment_id: str, body: str):
|
def update_comment(*, settings: Settings, comment_id: str, body: str) -> Comment:
|
||||||
data = get_graphql_response(
|
data = get_graphql_response(
|
||||||
settings=settings,
|
settings=settings,
|
||||||
query=update_comment_mutation,
|
query=update_comment_mutation,
|
||||||
comment_id=comment_id,
|
comment_id=comment_id,
|
||||||
body=body,
|
body=body,
|
||||||
)
|
)
|
||||||
response = UpdateCommentResponse.parse_obj(data)
|
response = UpdateCommentResponse.model_validate(data)
|
||||||
return response.data.updateDiscussionComment.comment
|
return response.data.updateDiscussionComment.comment
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main() -> None:
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
if settings.input_debug:
|
if settings.debug:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
else:
|
else:
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logging.debug(f"Using config: {settings.json()}")
|
logging.debug(f"Using config: {settings.model_dump_json()}")
|
||||||
g = Github(settings.input_token.get_secret_value())
|
g = Github(settings.github_token.get_secret_value())
|
||||||
repo = g.get_repo(settings.github_repository)
|
repo = g.get_repo(settings.github_repository)
|
||||||
if not settings.github_event_path.is_file():
|
if not settings.github_event_path.is_file():
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"No github event file available at: {settings.github_event_path}"
|
f"No github event file available at: {settings.github_event_path}"
|
||||||
)
|
)
|
||||||
contents = settings.github_event_path.read_text()
|
contents = settings.github_event_path.read_text()
|
||||||
github_event = PartialGitHubEvent.parse_raw(contents)
|
github_event = PartialGitHubEvent.model_validate_json(contents)
|
||||||
|
logging.info(f"Using GitHub event: {github_event}")
|
||||||
|
number = (
|
||||||
|
github_event.pull_request and github_event.pull_request.number
|
||||||
|
) or settings.number
|
||||||
|
if number is None:
|
||||||
|
raise RuntimeError("No PR number available")
|
||||||
|
|
||||||
# Avoid race conditions with multiple labels
|
# Avoid race conditions with multiple labels
|
||||||
sleep_time = random.random() * 10 # random number between 0 and 10 seconds
|
sleep_time = random.random() * 10 # random number between 0 and 10 seconds
|
||||||
|
|
@ -323,8 +332,8 @@ if __name__ == "__main__":
|
||||||
time.sleep(sleep_time)
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
# Get PR
|
# Get PR
|
||||||
logging.debug(f"Processing PR: #{github_event.pull_request.number}")
|
logging.debug(f"Processing PR: #{number}")
|
||||||
pr = repo.get_pull(github_event.pull_request.number)
|
pr = repo.get_pull(number)
|
||||||
label_strs = {label.name for label in pr.get_labels()}
|
label_strs = {label.name for label in pr.get_labels()}
|
||||||
langs = []
|
langs = []
|
||||||
for label in label_strs:
|
for label in label_strs:
|
||||||
|
|
@ -415,3 +424,7 @@ if __name__ == "__main__":
|
||||||
f"There doesn't seem to be anything to be done about PR #{pr.number}"
|
f"There doesn't seem to be anything to be done about PR #{pr.number}"
|
||||||
)
|
)
|
||||||
logging.info("Finished")
|
logging.info("Finished")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in New Issue