From 8cd4b4d737fb4785c7ed67686f7a9836ad427126 Mon Sep 17 00:00:00 2001 From: Cedric-Magnan Date: Mon, 17 Oct 2022 18:36:27 +0200 Subject: [PATCH 1/3] refacto: switch to sqlmodel --- .env.template | 3 +- README.md | 3 +- docs/architecture.png | Bin 52912 -> 70297 bytes examples/cloud_run/deploy.sh | 12 +- examples/cloud_run/main.tf | 28 +- .../bin/github_event_process.py | 102 +-- .../bin/github_repo_validation.py | 129 ++-- .../student_challenge_results_validation.py | 142 ---- .../bin/user_pytest_summaries_validation.py | 132 ++++ github_tests_validator_app/config.py | 42 +- .../data/gdrive_hierarchy.yml | 45 -- .../{github_connector.py => github_client.py} | 6 +- .../lib/connectors/google_drive.py | 190 ------ .../lib/connectors/google_sheet.py | 154 ----- .../lib/connectors/sqlalchemy_client.py | 137 ++++ github_tests_validator_app/lib/models/file.py | 28 - .../lib/models/pytest_result.py | 15 - .../lib/models/users.py | 10 - github_tests_validator_app/lib/utils.py | 19 +- poetry.lock | 605 ++++++++++++++---- pyproject.toml | 11 +- tests/units/test_github_repo_validation.py | 6 +- tests/units/test_utils.py | 18 +- 23 files changed, 900 insertions(+), 937 deletions(-) delete mode 100644 github_tests_validator_app/bin/student_challenge_results_validation.py create mode 100644 github_tests_validator_app/bin/user_pytest_summaries_validation.py delete mode 100644 github_tests_validator_app/data/gdrive_hierarchy.yml rename github_tests_validator_app/lib/connectors/{github_connector.py => github_client.py} (97%) delete mode 100644 github_tests_validator_app/lib/connectors/google_drive.py delete mode 100644 github_tests_validator_app/lib/connectors/google_sheet.py create mode 100644 github_tests_validator_app/lib/connectors/sqlalchemy_client.py delete mode 100644 github_tests_validator_app/lib/models/file.py delete mode 100644 github_tests_validator_app/lib/models/pytest_result.py delete mode 100644 github_tests_validator_app/lib/models/users.py diff --git a/.env.template b/.env.template index d325380..50d9c17 100644 --- a/.env.template +++ b/.env.template @@ -3,7 +3,6 @@ GH_APP_KEY="-----BEGIN RSA PRIVATE KEY----- Private Key data... -----END PRIVATE KEY-----" GH_PAT="" -GDRIVE_MAIN_DIRECTORY_NAME="" -USER_SHARE="" +SQLALCHEMY_URI="sqlite:///database.db" ENV="" # "LOCAL" or "GCP" GH_TESTS_REPO_NAME="" diff --git a/README.md b/README.md index a8dce58..8150b64 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,7 @@ But you can deploy the application on many Serverless Container services on any - GH_APP_ID : Auto-generated ID of the GitHub App you created during the [`Prerequisites`](#prerequisites) step. - GH_APP_KEY : Private Key of the GitHub App you created during the [`Prerequisites`](#prerequisites) step. - GH_PAT : GitHub personal access token [you must create](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) that has access to the GitHub repository containing the tests and the original repository which was forked (both could be the same repository). -- GDRIVE_MAIN_DIRECTORY_NAME : Name of the Google Drive Folder where you want the stats to be sent. -- USER_SHARE : Comma-separated list of emails that have access to this Google Drive Folder. +- SQLALCHEMY_URI : Database URI with [SQLAlchemy format](https://docs.sqlalchemy.org/en/14/core/engines.html#database-urls) - LOGGING : "LOCAL" if you are deploying locally, "GCP" if you are deploying on Google Cloud Run. - GH_TESTS_REPO_NAME : (Optional, only if you are using a git submodule for the tests folder) Name of the repository containing the tests (could be convenient if you have a repository with the exercices, and another one with the solutions and you want to have the same tests in both repositories by providing a submodule defined in a third repository). diff --git a/docs/architecture.png b/docs/architecture.png index c377e2738bd38e64be428f03ac38460b69ffb9c9..9d57cc8838af7a9af1a78e1fdca7b24fcdb80bfb 100644 GIT binary patch literal 70297 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 VwrO3F&p7a(qO97jJQ>r+{|k4kJcR%N diff --git a/examples/cloud_run/deploy.sh b/examples/cloud_run/deploy.sh index e398d7b..d3b06c9 100755 --- a/examples/cloud_run/deploy.sh +++ b/examples/cloud_run/deploy.sh @@ -2,17 +2,23 @@ echo "Please specify GCP project ID : " read PROJECT_ID +echo "Please specify GCP region : " +read REGION source .env gcloud config set project $PROJECT_ID gcloud auth application-default login -export TF_project_id=$PROJECT_ID +export TF_VAR_project_id=$PROJECT_ID +export TF_VAR_region=$REGION terraform -chdir=examples/cloud_run apply -input=true set +o history echo "$GH_APP_ID" | gcloud secrets versions add GH_APP_ID --data-file=- echo "$GH_APP_KEY" | gcloud secrets versions add GH_APP_KEY --data-file=- echo "$GH_PAT" | gcloud secrets versions add GH_PAT --data-file=- echo "$GH_TESTS_REPO_NAME" | gcloud secrets versions add GH_TESTS_REPO_NAME --data-file=- -echo "$GDRIVE_MAIN_DIRECTORY_NAME" | gcloud secrets versions add GDRIVE_MAIN_DIRECTORY_NAME --data-file=- -echo "$USER_SHARE" | gcloud secrets versions add USER_SHARE --data-file=- +echo "$SQLALCHEMY_URI" | gcloud secrets versions add SQLALCHEMY_URI --data-file=- echo "$LOGGING" | gcloud secrets versions add LOGGING --data-file=- set -o history +gcloud auth configure-docker ${REGION}-docker.pkg.dev +docker build -t ${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/github_tests_validator_app:latest -f ./docker/Dockerfile . +docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/github_tests_validator_app:latest +terraform -chdir=examples/cloud_run apply -input=true diff --git a/examples/cloud_run/main.tf b/examples/cloud_run/main.tf index e2ff464..63705f6 100644 --- a/examples/cloud_run/main.tf +++ b/examples/cloud_run/main.tf @@ -114,19 +114,10 @@ resource "google_cloud_run_service" "github_test_validator_app" { } } env { - name = "GDRIVE_MAIN_DIRECTORY_NAME" + name = "SQLALCHEMY_URI" value_from { secret_key_ref { - name = "GDRIVE_MAIN_DIRECTORY_NAME" - key = "latest" - } - } - } - env { - name = "USER_SHARE" - value_from { - secret_key_ref { - name = "USER_SHARE" + name = "SQLALCHEMY_URI" key = "latest" } } @@ -210,19 +201,8 @@ resource "google_secret_manager_secret" "GH_TESTS_REPO_NAME" { } } } -resource "google_secret_manager_secret" "GDRIVE_MAIN_DIRECTORY_NAME" { - secret_id = "GDRIVE_MAIN_DIRECTORY_NAME" - - replication { - user_managed { - replicas { - location = "${var.region}" - } - } - } -} -resource "google_secret_manager_secret" "USER_SHARE" { - secret_id = "USER_SHARE" +resource "google_secret_manager_secret" "SQLALCHEMY_URI" { + secret_id = "SQLALCHEMY_URI" replication { user_managed { diff --git a/github_tests_validator_app/bin/github_event_process.py b/github_tests_validator_app/bin/github_event_process.py index a8ee22d..05d62c8 100644 --- a/github_tests_validator_app/bin/github_event_process.py +++ b/github_tests_validator_app/bin/github_event_process.py @@ -4,28 +4,19 @@ from github_tests_validator_app.bin.github_repo_validation import ( get_event, - get_student_github_connector, + get_user_github_connector, validate_github_repo, ) -from github_tests_validator_app.bin.student_challenge_results_validation import ( - send_student_challenge_results, +from github_tests_validator_app.bin.user_pytest_summaries_validation import ( + send_user_pytest_summaries, ) -from github_tests_validator_app.config import ( - GDRIVE_MAIN_DIRECTORY_NAME, - GDRIVE_SUMMARY_SPREADSHEET, - GSHEET_DETAILS_SPREADSHEET, - USER_SHARE, -) -from github_tests_validator_app.lib.connectors.google_drive import GoogleDriveConnector -from github_tests_validator_app.lib.connectors.google_sheet import GSheetConnector -from github_tests_validator_app.lib.models.file import GSheetDetailFile, GSheetFile, WorkSheetFile -from github_tests_validator_app.lib.models.users import GitHubUser +from github_tests_validator_app.lib.connectors.sqlalchemy_client import SQLAlchemyConnector, User from github_tests_validator_app.lib.utils import init_github_user_from_github_event process = { "pull_request": validate_github_repo, "pusher": validate_github_repo, - "workflow_job": send_student_challenge_results, + "workflow_job": send_user_pytest_summaries, } @@ -35,51 +26,12 @@ def handle_process(payload: Dict[str, Any]) -> str: if ( not event or (event == "pull_request" and payload["action"] not in ["reopened", "opened"]) - or ( - event == "workflow_job" - and ( - payload["action"] not in ["completed"] - or payload["workflow_job"]["conclusion"] != "success" - ) - ) + or (event == "workflow_job" and payload["action"] not in ["completed"]) ): return "" return event -def init_gsheet_file( - google_drive: GoogleDriveConnector, - info: Dict[str, Any], - parent_id: str, - shared_user_list: List[str], -) -> GSheetFile: - - gsheet = google_drive.get_gsheet(info["name"], parent_id, shared_user_list) - list_worksheets = [ - WorkSheetFile(NAME=worksheet["name"], HEADERS=worksheet["headers"]) - for _, worksheet in info["worksheets"].items() - ] - return GSheetFile( - NAME=info["name"], - MIMETYPE=gsheet.get("mimeType", ""), - ID=gsheet.get("id", ""), - WORKSHEETS=list_worksheets, - ) - - -def init_gsheet_detail_file( - google_drive: GoogleDriveConnector, info: Dict[str, Any], parent_id: str, user_share: List[str] -) -> GSheetDetailFile: - - gsheet = google_drive.get_gsheet(info["name"], parent_id, user_share) - return GSheetDetailFile( - NAME=info["name"], - MIMETYPE=gsheet.get("mimeType", ""), - ID=gsheet.get("id", ""), - HEADERS=info["headers"], - ) - - def run(payload: Dict[str, Any]) -> None: """ Validator function @@ -95,42 +47,30 @@ def run(payload: Dict[str, Any]) -> None: if not event: return - # Init Google Drive connector and folders - google_drive = GoogleDriveConnector() - folder = google_drive.get_gdrive_folder(GDRIVE_MAIN_DIRECTORY_NAME, USER_SHARE) - - # Init Google sheets - gsheet_summary_file = init_gsheet_file( - google_drive, GDRIVE_SUMMARY_SPREADSHEET, folder["id"], USER_SHARE - ) - gsheet_details_file = init_gsheet_detail_file( - google_drive, GSHEET_DETAILS_SPREADSHEET, folder["id"], USER_SHARE - ) - - # Init Google sheet connector and worksheets - gsheet = GSheetConnector(google_drive.credentials, gsheet_summary_file, gsheet_details_file) - - # Init GitHubUser - student_user = init_github_user_from_github_event(payload) - if not isinstance(student_user, GitHubUser): + # Init User + user = init_github_user_from_github_event(payload) + if not isinstance(user, User): # Logging return - # Send user on Google Sheet - gsheet.add_new_user_on_sheet(student_user) + sql_client = SQLAlchemyConnector() + + sql_client.add_new_user(user) # Check valid repo - student_github_connector = get_student_github_connector(student_user, payload) - if not student_github_connector: - gsheet.add_new_repo_valid_result( - student_user, + user_github_connector = get_user_github_connector(user, payload) + if not user_github_connector: + sql_client.add_new_repository_validation( + user, False, - "[ERROR]: cannot get the student github repository.", + payload, + event, + "[ERROR]: cannot get the user github repository.", ) - logging.error("[ERROR]: cannot get the student github repository.") + logging.error("[ERROR]: cannot get the user github repository.") return logging.info(f'Begin process: "{event}"...') # Run the process - process[event](student_github_connector, gsheet, payload) + process[event](user_github_connector, sql_client, payload, event) logging.info(f'End of process: "{event}".') diff --git a/github_tests_validator_app/bin/github_repo_validation.py b/github_tests_validator_app/bin/github_repo_validation.py index 7ac7f42..1bbfca7 100644 --- a/github_tests_validator_app/bin/github_repo_validation.py +++ b/github_tests_validator_app/bin/github_repo_validation.py @@ -8,37 +8,31 @@ GH_TESTS_FOLDER_NAME, GH_TESTS_REPO_NAME, GH_WORKFLOWS_FOLDER_NAME, + commit_ref_path, default_message, ) -from github_tests_validator_app.lib.connectors.github_connector import GitHubConnector -from github_tests_validator_app.lib.connectors.google_sheet import GSheetConnector -from github_tests_validator_app.lib.models.users import GitHubUser - -commit_sha_path: Dict[str, List[str]] = { - "pull_request": ["pull_request", "head", "ref"], - "pusher": ["ref"], - "workflow_job": [], -} +from github_tests_validator_app.lib.connectors.github_client import GitHubConnector +from github_tests_validator_app.lib.connectors.sqlalchemy_client import SQLAlchemyConnector, User def get_event(payload: Dict[str, Any]) -> str: - for event in commit_sha_path: + for event in commit_ref_path: if event in payload: - return event + return str(event) return "" -def get_student_branch(payload: Dict[str, Any], trigger: Union[str, None] = None) -> Any: +def get_user_branch(payload: Dict[str, Any], trigger: Union[str, None] = None) -> Any: trigger = get_event(payload) if not trigger else trigger if not trigger: # Log error # FIXME # Archive the payload # FIXME - logging.error("Couldn't find the student branch, maybe the trigger is not managed") + logging.error("Couldn't find the user branch, maybe the trigger is not managed") return None - path = commit_sha_path[trigger].copy() + path = commit_ref_path[trigger].copy() branch = payload while path: @@ -54,118 +48,137 @@ def get_student_branch(payload: Dict[str, Any], trigger: Union[str, None] = None return branch -def get_student_github_connector( - student: GitHubUser, payload: Dict[str, Any] -) -> Union[GitHubConnector, None]: +def get_user_github_connector(user: User, payload: Dict[str, Any]) -> Union[GitHubConnector, None]: - if not student: + if not user: return None - github_student_branch = get_student_branch(payload) - if github_student_branch is None: + github_user_branch = get_user_branch(payload) + if github_user_branch is None: return None - return GitHubConnector(student, payload["repository"]["full_name"], github_student_branch) + return GitHubConnector(user, payload["repository"]["full_name"], github_user_branch) def compare_folder( - student_github: GitHubConnector, solution_repo: GitHubConnector, folder: str + user_github: GitHubConnector, solution_repo: GitHubConnector, folder: str ) -> Any: - student_contents = student_github.repo.get_contents(folder, ref=student_github.BRANCH_NAME) + user_contents = user_github.repo.get_contents(folder, ref=user_github.BRANCH_NAME) - if ( - isinstance(student_contents, ContentFile.ContentFile) - and student_contents.type == "submodule" - ): + if isinstance(user_contents, ContentFile.ContentFile) and user_contents.type == "submodule": solution_last_commit = solution_repo.get_last_hash_commit() - student_commit = student_contents.sha - return solution_last_commit == student_commit + user_commit = user_contents.sha + return solution_last_commit == user_commit - student_hash = student_github.get_hash(folder) + user_hash = user_github.get_hash(folder) solution_hash = solution_repo.get_hash(folder) - return student_hash == solution_hash + return user_hash == solution_hash def validate_github_repo( - student_github_connector: GitHubConnector, gsheet: GSheetConnector, payload: Dict[str, Any] + user_github_connector: GitHubConnector, + gsheet: SQLAlchemyConnector, + payload: Dict[str, Any], + event: str, ) -> None: logging.info(f"Connecting to repo : {GH_TESTS_REPO_NAME}") tests_github_connector = GitHubConnector( - user=student_github_connector.user, + user=user_github_connector.user, repo_name=GH_TESTS_REPO_NAME if GH_TESTS_REPO_NAME - else student_github_connector.repo.parent.full_name, + else user_github_connector.repo.parent.full_name, branch_name="main", access_token=GH_PAT, ) - logging.info(f"Connecting to repo : {student_github_connector.repo.parent.full_name}") + logging.info(f"Connecting to repo : {user_github_connector.repo.parent.full_name}") original_github_connector = GitHubConnector( - user=student_github_connector.user, - repo_name=student_github_connector.repo.parent.full_name, + user=user_github_connector.user, + repo_name=user_github_connector.repo.parent.full_name, branch_name="main", access_token=GH_PAT, ) if not tests_github_connector: - gsheet.add_new_repo_valid_result( - student_github_connector.user, + gsheet.add_new_repository_validation( + user_github_connector.user, False, + payload, + event, "[ERROR]: cannot get the tests github repository.", ) logging.error("[ERROR]: cannot get the tests github repository.") return if not original_github_connector: - gsheet.add_new_repo_valid_result( - student_github_connector.user, + gsheet.add_new_repository_validation( + user_github_connector.user, False, + payload, + event, "[ERROR]: cannot get the original github repository.", ) logging.error("[ERROR]: cannot get the original github repository.") return workflows_havent_changed = compare_folder( - student_github_connector, original_github_connector, GH_WORKFLOWS_FOLDER_NAME + user_github_connector, original_github_connector, GH_WORKFLOWS_FOLDER_NAME ) tests_havent_changed = compare_folder( - student_github_connector, tests_github_connector, GH_TESTS_FOLDER_NAME + user_github_connector, tests_github_connector, GH_TESTS_FOLDER_NAME ) # Add valid repo result on Google Sheet - gsheet.add_new_repo_valid_result( - student_github_connector.user, + gsheet.add_new_repository_validation( + user_github_connector.user, workflows_havent_changed, + payload, + event, default_message["valid_repository"]["workflows"][str(workflows_havent_changed)], ) - gsheet.add_new_repo_valid_result( - student_github_connector.user, + gsheet.add_new_repository_validation( + user_github_connector.user, tests_havent_changed, + payload, + event, default_message["valid_repository"]["tests"][str(tests_havent_changed)], ) - # Update Pull Request - if "pull_request" in payload: - issue = student_github_connector.repo.get_issue(number=payload["pull_request"]["number"]) - tests_conclusion = "success" if tests_havent_changed else "failure" - tests_message = default_message["valid_repository"]["tests"][str(tests_havent_changed)] + tests_conclusion = "success" if tests_havent_changed else "failure" + tests_message = default_message["valid_repository"]["tests"][str(tests_havent_changed)] + workflows_conclusion = "success" if workflows_havent_changed else "failure" + workflows_message = default_message["valid_repository"]["workflows"][ + str(workflows_havent_changed) + ] + + if event == "pull_request": + issue = user_github_connector.repo.get_issue(number=payload["pull_request"]["number"]) issue.create_comment(tests_message) - student_github_connector.repo.create_check_run( + user_github_connector.repo.create_check_run( name=tests_message, head_sha=payload["pull_request"]["head"]["sha"], status="completed", conclusion=tests_conclusion, ) - workflows_conclusion = "success" if workflows_havent_changed else "failure" - workflows_message = default_message["valid_repository"]["workflows"][ - str(workflows_havent_changed) - ] - student_github_connector.repo.create_check_run( + user_github_connector.repo.create_check_run( name=workflows_message, head_sha=payload["pull_request"]["head"]["sha"], status="completed", conclusion=workflows_conclusion, ) issue.create_comment(workflows_message) + elif event == "pusher": + user_github_connector.repo.create_check_run( + name=tests_message, + head_sha=payload["after"], + status="completed", + conclusion=tests_conclusion, + ) + user_github_connector.repo.create_check_run( + name=workflows_message, + head_sha=payload["after"], + status="completed", + conclusion=workflows_conclusion, + ) diff --git a/github_tests_validator_app/bin/student_challenge_results_validation.py b/github_tests_validator_app/bin/student_challenge_results_validation.py deleted file mode 100644 index 49ad7e6..0000000 --- a/github_tests_validator_app/bin/student_challenge_results_validation.py +++ /dev/null @@ -1,142 +0,0 @@ -from typing import Any, Dict, List, Tuple, Union - -import logging - -from github_tests_validator_app.config import CHALLENGE_DIR -from github_tests_validator_app.lib.connectors.github_connector import GitHubConnector -from github_tests_validator_app.lib.connectors.google_sheet import GSheetConnector -from github_tests_validator_app.lib.models.pytest_result import PytestResult - - -def init_pytest_result_from_artifact( - artifact: Dict[str, Any], workflow_run_id: int -) -> Union[PytestResult, None]: - if not artifact: - return None - - return PytestResult( - DURATION=artifact["duration"], - TOTAL_TESTS_COLLECTED=artifact["summary"]["collected"], - TOTAL_PASSED_TEST=artifact["summary"]["passed"], - TOTAL_FAILED_TEST=artifact["summary"]["failed"], - DESCRIPTION_TEST_RESULTS=artifact["tests"], - WORKFLOW_RUN_ID=workflow_run_id, - ) - - -def get_student_artifact( - student_github_connector: GitHubConnector, - gsheet: GSheetConnector, - all_student_artifact: Dict[str, Any], - payload: Dict[str, Any], -) -> Any: - - workflow_run_id = payload["workflow_job"]["run_id"] - artifact_info = student_github_connector.get_artifact_info_from_artifacts_with_worflow_run_id( - all_student_artifact["artifacts"], workflow_run_id - ) - if not artifact_info: - gsheet.add_new_student_result_summary( - user=student_github_connector.user, - result=PytestResult(), - info="[ERROR]: Cannot find the artifact of Pytest result on GitHub user repository.", - ) - logging.error( - "[ERROR]: Cannot find the artifact of Pytest result on GitHub user repository." - ) - return None - - # Read Artifact - artifact_resp = student_github_connector.get_artifact(artifact_info) - artifact = student_github_connector.get_artifact_from_format_zip_bytes(artifact_resp.content) - if not artifact: - gsheet.add_new_student_result_summary( - user=student_github_connector.user, - result=PytestResult(), - info="[ERROR]: Cannot read the artifact of Pytest result on GitHub user repository.", - ) - logging.error( - "[ERROR]: Cannot read the artifact of Pytest result on GitHub user repository." - ) - return None - - return artifact - - -def get_test_information(path: str) -> Tuple[str, str, str, str]: - - list_path_name = path.split("::") - file_path = list_path_name[0] - script_name = list_path_name[0].split("/")[-1] - test_name = list_path_name[1] - challenge_id = "-".join( - [ - name[:2] - for name in list_path_name[0].split(CHALLENGE_DIR)[1].split("/") - if ".py" not in name - ] - ) - return challenge_id, file_path, script_name, test_name - - -def parsing_challenge_results(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - - challenge_results = [] - for test in results: - challenge_id, file_path, script_name, test_name = get_test_information(test["nodeid"]) - challenge_results.append( - { - "file_path": file_path, - "script_name": script_name, - "test_name": test_name, - "challenge_id": challenge_id, - "outcome": test["outcome"], - "setup": test["setup"], - "call": test["call"], - "teardown": test["teardown"], - } - ) - - return challenge_results - - -def send_student_challenge_results( - student_github_connector: GitHubConnector, gsheet: GSheetConnector, payload: Dict[str, Any] -) -> None: - - # Get all artifacts - all_student_artifact = student_github_connector.get_all_artifacts() - if not all_student_artifact: - message = f"[ERROR]: Cannot get all artifact on repository {student_github_connector.REPO_NAME} of user {student_github_connector.user.LOGIN}." - if all_student_artifact["total_count"] == 0: - message = f"[ERROR]: No artifact on repository {student_github_connector.REPO_NAME} of user {student_github_connector.user.LOGIN}." - gsheet.add_new_student_result_summary( - user=student_github_connector.user, - result={}, - info=message, - ) - logging.error(message) - return - - # Get student artifact - artifact = get_student_artifact(student_github_connector, gsheet, all_student_artifact, payload) - if not artifact: - return - - # Get summary student results - pytest_result = init_pytest_result_from_artifact(artifact, payload["workflow_job"]["run_id"]) - # Send summary student results to Google Sheet - gsheet.add_new_student_result_summary( - user=student_github_connector.user, - result=pytest_result, - info="Result of student tests", - ) - - # Parsing artifact / challenge results - challenge_results = parsing_challenge_results(artifact["tests"]) - # Send new detail results to Google Sheet - gsheet.add_new_student_detail_results( - user=student_github_connector.user, - results=challenge_results, - workflow_run_id=payload["workflow_job"]["run_id"], - ) diff --git a/github_tests_validator_app/bin/user_pytest_summaries_validation.py b/github_tests_validator_app/bin/user_pytest_summaries_validation.py new file mode 100644 index 0000000..b63b51d --- /dev/null +++ b/github_tests_validator_app/bin/user_pytest_summaries_validation.py @@ -0,0 +1,132 @@ +from typing import Any, Dict, List, Tuple, Union + +import logging +from datetime import datetime + +from github_tests_validator_app.lib.connectors.github_client import GitHubConnector +from github_tests_validator_app.lib.connectors.sqlalchemy_client import SQLAlchemyConnector + + +def get_user_artifact( + user_github_connector: GitHubConnector, + sql_client: SQLAlchemyConnector, + all_user_artifact: Dict[str, Any], + payload: Dict[str, Any], +) -> Any: + + workflow_run_id = payload["workflow_job"]["run_id"] + artifact_info = user_github_connector.get_artifact_info_from_artifacts_with_worflow_run_id( + all_user_artifact["artifacts"], workflow_run_id + ) + if not artifact_info: + sql_client.add_new_pytest_summary( + {}, + workflow_run_id, + user_github_connector.user, + user_github_connector.REPO_NAME, + user_github_connector.BRANCH_NAME, + info="[ERROR]: Cannot find the artifact of Pytest result on GitHub user repository.", + ) + logging.error( + "[ERROR]: Cannot find the artifact of Pytest result on GitHub user repository." + ) + return None + + # Read Artifact + artifact_resp = user_github_connector.get_artifact(artifact_info) + artifact = user_github_connector.get_artifact_from_format_zip_bytes(artifact_resp.content) + if not artifact: + sql_client.add_new_pytest_summary( + {}, + workflow_run_id, + user_github_connector.user, + user_github_connector.REPO_NAME, + user_github_connector.BRANCH_NAME, + info="[ERROR]: Cannot read the artifact of Pytest result on GitHub user repository.", + ) + logging.error( + "[ERROR]: Cannot read the artifact of Pytest result on GitHub user repository." + ) + return None + + return artifact + + +def get_test_information(path: str) -> Tuple[str, str, str]: + + list_path_name = path.split("::") + file_path = list_path_name[0] + script_name = list_path_name[0].split("/")[-1] + test_name = list_path_name[1] + return file_path, script_name, test_name + + +def parsing_pytest_summaries(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + + pytest_summaries = [] + for test in results: + file_path, script_name, test_name = get_test_information(test["nodeid"]) + pytest_summaries.append( + { + "file_path": file_path, + "script_name": script_name, + "test_name": test_name, + "outcome": test["outcome"], + "setup": test["setup"], + "call": test["call"], + "teardown": test["teardown"], + } + ) + + return pytest_summaries + + +def send_user_pytest_summaries( + user_github_connector: GitHubConnector, + sql_client: SQLAlchemyConnector, + payload: Dict[str, Any], + event: str, +) -> None: + + # Get all artifacts + all_user_artifact = user_github_connector.get_all_artifacts() + if not all_user_artifact: + message = f"[ERROR]: Cannot get all artifact on repository {user_github_connector.REPO_NAME} of user {user_github_connector.user.LOGIN}." + if all_user_artifact["total_count"] == 0: + message = f"[ERROR]: No artifact on repository {user_github_connector.REPO_NAME} of user {user_github_connector.user.LOGIN}." + sql_client.add_new_pytest_summary( + {}, + payload["workflow_job"]["run_id"], + user_github_connector.user, + user_github_connector.REPO_NAME, + user_github_connector.BRANCH_NAME, + info=message, + ) + logging.error(message) + return + + # Get user artifact + artifact = get_user_artifact(user_github_connector, sql_client, all_user_artifact, payload) + if not artifact: + return + + # Send summary user results to Google Sheet + sql_client.add_new_pytest_summary( + artifact, + payload["workflow_job"]["run_id"], + user_github_connector.user, + user_github_connector.REPO_NAME, + user_github_connector.BRANCH_NAME, + info="Result of user tests", + ) + + # Parsing artifact / challenge results + pytest_summaries = parsing_pytest_summaries(artifact["tests"]) + # Send new detail results to Google Sheet + sql_client.add_new_pytest_detail( + user=user_github_connector.user, + repository=user_github_connector.REPO_NAME, + branch=user_github_connector.BRANCH_NAME, + results=pytest_summaries, + workflow_run_id=payload["workflow_job"]["run_id"], + ) diff --git a/github_tests_validator_app/config.py b/github_tests_validator_app/config.py index a366e49..de7c73b 100644 --- a/github_tests_validator_app/config.py +++ b/github_tests_validator_app/config.py @@ -1,12 +1,14 @@ -from typing import Dict, cast +from typing import Dict, List, cast import logging import os import google.cloud.logging -import yaml +from dotenv import load_dotenv -if os.getenv("LOGGING", "").replace("\r\n", "") == "GCP": +load_dotenv() + +if os.getenv("LOGGING", "").replace("\r\n", "").replace("\r", "") == "GCP": logging_client = google.cloud.logging.Client() logging_client.get_default_handler() logging_client.setup_logging() @@ -22,29 +24,26 @@ if logging.getLogger("uvicorn") and logging.getLogger("uvicorn").handlers: logging.getLogger("uvicorn").removeHandler(logging.getLogger("uvicorn").handlers[0]) + +commit_ref_path: Dict[str, List[str]] = { + "pull_request": ["pull_request", "head", "ref"], + "pusher": ["ref"], + "workflow_job": [], +} + # GitHub -GH_APP_ID = cast(str, os.getenv("GH_APP_ID", "")).replace("\r\n", "") +GH_APP_ID = cast(str, os.getenv("GH_APP_ID", "")).replace("\r\n", "").replace("\r", "") GH_APP_KEY = cast(str, os.getenv("GH_APP_KEY", "")) -GH_PAT = cast(str, os.getenv("GH_PAT", "")).replace("\r\n", "") -GH_TESTS_REPO_NAME = cast(str, os.getenv("GH_TESTS_REPO_NAME", "")).replace("\r\n", "") +GH_PAT = cast(str, os.getenv("GH_PAT", "")).replace("\r\n", "").replace("\r", "") +SQLALCHEMY_URI = cast(str, os.getenv("SQLALCHEMY_URI", "")).replace("\r\n", "").replace("\r", "") +GH_TESTS_REPO_NAME = ( + cast(str, os.getenv("GH_TESTS_REPO_NAME", "")).replace("\r\n", "").replace("\r", "") +) GH_TESTS_FOLDER_NAME = "tests" GH_WORKFLOWS_FOLDER_NAME = ".github/workflows" GH_API = "https://api.github.com/repos" GH_ALL_ARTIFACT_ENDPOINT = "actions/artifacts" -# Google Drive -GDRIVE_MAIN_DIRECTORY_NAME = cast(str, os.getenv("GDRIVE_MAIN_DIRECTORY_NAME", "")).replace( - "\r\n", "" -) - -# Google Sheet -GDRIVE_HIERARCHY_PATH = "github_tests_validator_app/data/gdrive_hierarchy.yml" -with open(GDRIVE_HIERARCHY_PATH) as file: - data = yaml.safe_load(file) - -GDRIVE_SUMMARY_SPREADSHEET = data["gdrive_summary_spreadsheet"] -GSHEET_DETAILS_SPREADSHEET = data["gsheet_details_spreadsheet"] - # Log message default_message: Dict[str, Dict[str, Dict[str, str]]] = { "valid_repository": { @@ -58,8 +57,3 @@ }, }, } - -# Common -CHALLENGE_DIR = "tests/tests/" -DATE_FORMAT = "%d/%m/%Y %H:%M:%S" -USER_SHARE = os.getenv("USER_SHARE", "").replace("\r\n", "").split(",") diff --git a/github_tests_validator_app/data/gdrive_hierarchy.yml b/github_tests_validator_app/data/gdrive_hierarchy.yml deleted file mode 100644 index 70ddedc..0000000 --- a/github_tests_validator_app/data/gdrive_hierarchy.yml +++ /dev/null @@ -1,45 +0,0 @@ ---- -gdrive_summary_spreadsheet: - name: "Student challenge results" - worksheets: - student: - name: students - headers: - - login - - url - - id - - created_at - check_validation_repo: - name: check_validation_repo - headers: - - login - - user_id - - is_valid - - created_at - - info - student_challenge_results: - name: student_challenge_results - headers: - - login - - workflow_run_id - - created_at - - total_tests_collected - - total_passed_test - - total_failed_test - - duration - - info - -gsheet_details_spreadsheet: - name: Details - headers: - - login - - workflow_run_id - - created_at - - file_path - - script_name - - test_name - - outcome - - challenge_id - - setup - - call - - teardown diff --git a/github_tests_validator_app/lib/connectors/github_connector.py b/github_tests_validator_app/lib/connectors/github_client.py similarity index 97% rename from github_tests_validator_app/lib/connectors/github_connector.py rename to github_tests_validator_app/lib/connectors/github_client.py index 78cb72a..fd08fc0 100644 --- a/github_tests_validator_app/lib/connectors/github_connector.py +++ b/github_tests_validator_app/lib/connectors/github_client.py @@ -13,18 +13,18 @@ GH_APP_ID, GH_APP_KEY, ) -from github_tests_validator_app.lib.models.users import GitHubUser +from github_tests_validator_app.lib.connectors.sqlalchemy_client import User from github_tests_validator_app.lib.utils import get_hash_files class GitHubConnector: def __init__( self, - user: GitHubUser, + user: User, repo_name: str, branch_name: str, access_token: Union[str, None] = None, - ): + ) -> None: self.user = user self.REPO_NAME = repo_name self.BRANCH_NAME = branch_name diff --git a/github_tests_validator_app/lib/connectors/google_drive.py b/github_tests_validator_app/lib/connectors/google_drive.py deleted file mode 100644 index fae17d2..0000000 --- a/github_tests_validator_app/lib/connectors/google_drive.py +++ /dev/null @@ -1,190 +0,0 @@ -from typing import Any, Dict, List - -import logging -from readline import set_completion_display_matches_hook - -from google.auth import default -from googleapiclient.discovery import build - - -class GoogleDriveConnector: - def __init__(self): - logging.info(f"Connecting to Google Drive API ...") - self.credentials = self._get_credentials() - self.client = self._get_client() - logging.info(f"Done.") - - def _get_credentials(self): - credentials, _ = default( - scopes=[ - "https://www.googleapis.com/auth/drive", - ] - ) - return credentials - - def _get_client(self): - return build("drive", "v3", credentials=self.credentials) - - def search_folder(self, folder_name: str) -> Any: - """Get all folders from a google drive. - ... - :return: All folders informations. - :rtype: Any - """ - results = ( - self.client.files() - .list( - q=f"name = '{folder_name}' and (mimeType = 'application/vnd.google-apps.folder')", - spaces="drive", - fields="files(id, name, mimeType, permissions)", - ) - .execute() - ) - return results.get("files", []) - - def get_all_file(self, file_name: str, parent_folder_ids: str = "") -> Any: - """Get all files from a folder on Google Drive. - :param parent_folder_ids: Folder ID. - ... - :return: All file informations from a folder. - :rtype: Any - """ - query = "" - if parent_folder_ids: - query = f"name = '{file_name}'" - response = ( - self.client.files() - .list( - q=query, - spaces="drive", - fields="files(id, name, mimeType, permissions)", - pageToken=None, - ) - .execute() - ) - return response.get("files", []) - - def create_folder(self, folder_name: str) -> Any: - """Create a folder in google drive. - :param folder_name: Folder title. - ... - :return: new folder informations - :rtype: Any - """ - # create drive api client - file_metadata = {"name": folder_name, "mimeType": "application/vnd.google-apps.folder"} - - # pylint: disable=maybe-no-member - folder = self.client.files().create(body=file_metadata).execute() - logging.info(f'Folder {folder["name"]} has created with ID: "{folder["id"]}".') - return folder - - def share_file(self, real_file_id: str, user_email: str) -> List[Any]: - """Share the file with new user email. - :param real_file_id: File ID. - :param user_email: email that we want to share with the file. - ... - :return: informations of the folder - :rtype: List[Any] - """ - ids = [] - file_id = real_file_id - - def callback(request_id, response, exception): - if exception: - logging.error(f"Request_Id: {request_id}") - logging.error(exception) - else: - ids.append(response.get("id")) - - logging.info(f"Sharing file {real_file_id} to : {user_email}") - - batch = self.client.new_batch_http_request(callback=callback) - user_permission = {"type": "user", "role": "writer", "emailAddress": user_email} - batch.add( - self.client.permissions().create(fileId=file_id, body=user_permission, fields="id") - ) - batch.execute() - - return ids - - def share_file_from_users(self, file_info: Dict[str, Any], users: List[str] = []) -> None: - if not users: - return - user_shared = [user["emailAddress"] for user in file_info.get("permissions", [])] - new_shared_users = list(set(users) - set(user_shared)) - for user in new_shared_users: - self.share_file(file_info["id"], user) - - def get_gdrive_folder(self, folder_name: str, shared_user_list: List[str] = []) -> Any: - """Get the folder information in google drive. - .. note :: - If the folder doesn't exist, it will create a new one. - :param folder_name: Folder title. - :param user_share: email that we want to share with the folder. - ... - :return: informations of the folder - :rtype: Any - """ - list_folder = self.search_folder(folder_name) - for folder in list_folder: - if folder.get("name", None) == folder_name: - if shared_user_list: - self.share_file_from_users(folder, shared_user_list) - return folder - - folder = self.create_folder(folder_name) - if "id" in folder and shared_user_list: - self.share_file_from_users(folder, shared_user_list) - return folder - - def get_gsheet( - self, gsheet_name: str, parent_folder_ids: str = "", shared_user_list: List[str] = [] - ) -> Any: - """Get the google sheet information. - .. note :: - If the google sheet doesn't exist, it will create a new one. - :param gsheet_name: Google Sheet title. - :param parent_folder_ids: A list of strings of parent folder ids (if any). - :param user_share: email that we want to share with the google sheet. - ... - :return: informations of the google sheet - :rtype: Any - """ - list_file = self.get_all_file(gsheet_name, parent_folder_ids) - for file in list_file: - if file["name"] == gsheet_name and "spreadsheet" in file["mimeType"]: - if shared_user_list: - self.share_file_from_users(file, shared_user_list) - return file - file = self.create_google_file( - gsheet_name, - "application/vnd.google-apps.spreadsheet", - [parent_folder_ids], - ) - if shared_user_list: - self.share_file_from_users(file, shared_user_list) - return file - - def create_google_file( - self, title: str, mimeType: str, parent_folder_ids: List[str] = [] - ) -> Any: - """Create a new file on Google drive. - .. note :: - Created file is not instantly visible in your Drive search and you need to access it by direct link. - :param title: File title - :param parent_folder_ids: A list of strings of parent folder ids (if any). - ... - :return: informations of new file - :rtype: Any - """ - logging.info(f"Creating Sheet {title}") - body: Dict[str, Any] = { - "name": title, - "mimeType": mimeType, - } - if parent_folder_ids: - body["parents"] = parent_folder_ids - req = self.client.files().create(body=body) - new_sheet = req.execute() - return new_sheet diff --git a/github_tests_validator_app/lib/connectors/google_sheet.py b/github_tests_validator_app/lib/connectors/google_sheet.py deleted file mode 100644 index 10a8fb6..0000000 --- a/github_tests_validator_app/lib/connectors/google_sheet.py +++ /dev/null @@ -1,154 +0,0 @@ -from typing import Any, Dict, List - -import json -import logging - -import gspread -from github_tests_validator_app.config import GDRIVE_SUMMARY_SPREADSHEET -from github_tests_validator_app.lib.models.file import GSheetDetailFile, GSheetFile -from github_tests_validator_app.lib.models.pytest_result import PytestResult -from github_tests_validator_app.lib.models.users import GitHubUser -from google.oauth2.service_account import Credentials - - -class GSheetConnector: - def __init__( - self, - credentials: Credentials, - gsheet_summary_file: GSheetFile, - gsheet_details_file: GSheetDetailFile, - ): - self.gsheet_summary_file = gsheet_summary_file - self.gsheet_details_file = gsheet_details_file - - logging.info(f"Connecting to Google Sheet API ...") - self.gs_client = gspread.authorize(credentials) - logging.info("Done.") - - logging.info(f"Init spreadsheet ...") - self.summary_spreadsheet = self.init_spreadsheet(gsheet_summary_file) - self.detail_spreadsheet = self.gs_client.open_by_key(gsheet_details_file.ID) - logging.info(f"Done.") - - def add_worksheet( - self, spreadsheet: gspread.spreadsheet.Spreadsheet, title: str, headers: List[str] - ) -> gspread.worksheet.Worksheet: - - new_worksheet = spreadsheet.add_worksheet(title=title, rows=1, cols=1) - new_worksheet.insert_row(headers) - return new_worksheet - - def init_spreadsheet(self, gsheet_file: GSheetFile) -> gspread.spreadsheet.Spreadsheet: - - spreadsheet = self.gs_client.open_by_key(gsheet_file.ID) - all_worksheets = spreadsheet.worksheets() - all_worksheets_name = [worksheet.title for worksheet in all_worksheets] - - # Init all worksheets - for worksheet in gsheet_file.WORKSHEETS: - - if worksheet.NAME in all_worksheets_name: - continue - - if all_worksheets and all_worksheets[0].title == "Sheet1": - new_worksheet = all_worksheets.pop(0) - new_worksheet.update_title(worksheet.NAME) - new_worksheet.insert_row(worksheet.HEADERS) - else: - self.add_worksheet(spreadsheet, worksheet.NAME, worksheet.HEADERS) - return spreadsheet - - def add_new_user_on_sheet(self, user: GitHubUser) -> None: - # Controle the workseet exist of not - worksheet = self.summary_spreadsheet.worksheet( - GDRIVE_SUMMARY_SPREADSHEET["worksheets"]["student"]["name"] - ) - - # Check is user exist - id_cell = worksheet.find(str(user.ID)) - login_cell = worksheet.find(user.LOGIN) - if id_cell and login_cell and id_cell.row == login_cell.row: - logging.info("User already exist in student worksheet.") - else: - logging.info(f"Add new user {user.LOGIN} in student worksheet ...") - headers = worksheet.row_values(1) - user_dict = user.__dict__ - new_row = [ - user_dict[header.upper()] if header.upper() in user_dict else None - for header in headers - ] - worksheet.append_row(new_row) - logging.info("Done.") - - def dict_to_row( - self, headers: List[str], data: Dict[str, Any], to_str: bool = False, **kwargs: Any - ) -> List[str]: - result = [] - for header in headers: - value: Any = "" - if header in data: - value = data[header] - elif header in kwargs: - value = kwargs[header] - if to_str and isinstance(value, dict): - value = json.dumps(value) - result.append(value) - return result - - def add_new_repo_valid_result(self, user: GitHubUser, result: bool, info: str = "") -> None: - worksheet = self.summary_spreadsheet.worksheet( - GDRIVE_SUMMARY_SPREADSHEET["worksheets"]["check_validation_repo"]["name"] - ) - headers = worksheet.row_values(1) - user_dict = {k.lower(): v for k, v in user.__dict__.items()} - new_row = self.dict_to_row( - headers, user_dict, to_str=True, info=info, is_valid=str(result), user_id=user.ID - ) - worksheet.append_row(new_row) - - def add_new_student_result_summary( - self, user: GitHubUser, result: PytestResult, info: str = "" - ) -> None: - worksheet = self.summary_spreadsheet.worksheet( - GDRIVE_SUMMARY_SPREADSHEET["worksheets"]["student_challenge_results"]["name"] - ) - headers = worksheet.row_values(1) - result_dict = {k.lower(): v for k, v in result.__dict__.items()} - user_dict = {k.lower(): v for k, v in user.__dict__.items()} - data = {**user_dict, **result_dict} - - new_row = self.dict_to_row(headers, data, to_str=True, info=info) - worksheet.append_row(new_row) - - def add_new_student_detail_results( - self, user: GitHubUser, results: List[Dict[str, Any]], workflow_run_id: int - ) -> None: - - # All worksheets - list_worksheet = self.detail_spreadsheet.worksheets() - # Get student worksheet - student_worksheet = None - for worksheet in list_worksheet: - if worksheet.title == user.LOGIN: - student_worksheet = worksheet - break - - # Create new worksheet - if not student_worksheet: - student_worksheet = self.detail_spreadsheet.add_worksheet( - title=user.LOGIN, rows=1, cols=1 - ) - student_worksheet.insert_row(self.gsheet_details_file.HEADERS) - - headers = student_worksheet.row_values(1) - user_dict = {k.lower(): v for k, v in user.__dict__.items()} - new_rows = [] - - for test in results: - test = {k.lower(): v for k, v in test.items()} - data = {**user_dict, **test} - row = self.dict_to_row(headers, data, to_str=True, workflow_run_id=workflow_run_id) - new_rows.append(row) - self.detail_spreadsheet.values_append( - student_worksheet.title, {"valueInputOption": "USER_ENTERED"}, {"values": new_rows} - ) diff --git a/github_tests_validator_app/lib/connectors/sqlalchemy_client.py b/github_tests_validator_app/lib/connectors/sqlalchemy_client.py new file mode 100644 index 0000000..541bc51 --- /dev/null +++ b/github_tests_validator_app/lib/connectors/sqlalchemy_client.py @@ -0,0 +1,137 @@ +from typing import Any, Dict, List, Optional + +import operator +from datetime import datetime +from functools import reduce + +from github_tests_validator_app.config import SQLALCHEMY_URI, commit_ref_path +from sqlmodel import JSON, Column, Field, Relationship, Session, SQLModel, create_engine + + +class User(SQLModel, table=True): + __tablename__ = "user" + __table_args__ = {"extend_existing": True} + + id: int = Field(primary_key=True) + organization_or_user: str + url: str + created_at: datetime = Field(default=datetime.now()) + + +class WorkflowRun(SQLModel, table=True): + __tablename__ = "workflow_run" + __table_args__ = {"extend_existing": True} + + id: int = Field(primary_key=True) + organization_or_user: str + + repository: str + branch: str + created_at: datetime = Field(default=datetime.now()) + total_tests_collected: int + total_passed_test: int + total_failed_test: int + duration: float + info: str + + user_id: int = Field(foreign_key="user.id") + + +class WorkflowRunDetail(SQLModel, table=True): + __tablename__ = "workflow_run_detail" + __table_args__ = {"extend_existing": True} + + created_at: datetime = Field(primary_key=True, default=datetime.now()) + file_path: str = Field(primary_key=True) + test_name: str = Field(primary_key=True) + repository: str + branch: str + script_name: str + outcome: str + setup: Dict[str, Any] = Field(sa_column=Column(JSON), default={}) + call: Dict[str, Any] = Field(sa_column=Column(JSON), default={}) + teardown: Dict[str, Any] = Field(sa_column=Column(JSON), default={}) + + workflow_run_id: int = Field(foreign_key="workflow_run.id") + + +class RepositoryValidation(SQLModel, table=True): + __tablename__ = "repository_validation" + __table_args__ = {"extend_existing": True} + + repository: str = Field(primary_key=True) + branch: str = Field(primary_key=True) + created_at: datetime = Field(primary_key=True, default=datetime.now()) + organization_or_user: str + is_valid: bool + info: str + + user_id: int = Field(foreign_key="user.id") + + +class SQLAlchemyConnector: + def __init__(self) -> None: + self.engine = create_engine(SQLALCHEMY_URI) + SQLModel.metadata.create_all(self.engine) + + def add_new_user(self, user: User) -> None: + with Session(self.engine) as session: + session.add(user) + session.commit() + + def add_new_repository_validation( + self, user: User, result: bool, payload: Dict[str, Any], event: str, info: str = "" + ) -> None: + repository_validation = RepositoryValidation( + repository=payload["repository"]["full_name"], + branch=reduce(operator.getitem, commit_ref_path[event], payload), + created_at=datetime.now(), + organization_or_user=user.organization_or_user, + user_id=user.id, + is_valid=result, + info=info, + ) + with Session(self.engine) as session: + session.add(repository_validation) + session.commit() + + def add_new_pytest_summary( + self, + artifact: Dict[str, Any], + workflow_run_id: int, + user: User, + repository: str, + branch: str, + info: str, + ) -> None: + pytest_summary = WorkflowRun( + id=workflow_run_id, + organization_or_user=user.organization_or_user, + user_id=user.id, + repository=repository, + branch=branch, + duration=artifact.get("duration", None), + total_tests_collected=artifact.get("summary", {}).get("collected", None), + total_passed_test=artifact.get("summary", {}).get("passed", None), + total_failed_test=artifact.get("summary", {}).get("failed", None), + info=info, + ) + with Session(self.engine) as session: + session.add(pytest_summary) + session.commit() + + def add_new_pytest_detail( + self, + user: User, + repository: str, + branch: str, + results: List[Dict[str, Any]], + workflow_run_id: int, + ) -> None: + with Session(self.engine) as session: + for test in results: + pytest_detail = WorkflowRunDetail( + repository=repository, branch=branch, workflow_run_id=workflow_run_id, **test + ) + session.add(pytest_detail) + session.commit() diff --git a/github_tests_validator_app/lib/models/file.py b/github_tests_validator_app/lib/models/file.py deleted file mode 100644 index 2ff5711..0000000 --- a/github_tests_validator_app/lib/models/file.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import List - -from dataclasses import dataclass, field - - -@dataclass -class File: - - NAME: str = "" - ID: str = "" - MIMETYPE: str = "" - - -@dataclass -class WorkSheetFile(File): - HEADERS: str = "" - - -@dataclass -class GSheetFile(File): - - WORKSHEETS: List[WorkSheetFile] = field(default_factory=List[WorkSheetFile]) - - -@dataclass -class GSheetDetailFile(File): - - HEADERS: List[str] = field(default_factory=List[str]) diff --git a/github_tests_validator_app/lib/models/pytest_result.py b/github_tests_validator_app/lib/models/pytest_result.py deleted file mode 100644 index 42c57a0..0000000 --- a/github_tests_validator_app/lib/models/pytest_result.py +++ /dev/null @@ -1,15 +0,0 @@ -from typing import Any, Dict, Union - -from dataclasses import dataclass, field - - -@dataclass -class PytestResult: - - DURATION: float = 0.0 - TOTAL_TESTS_COLLECTED: int = 0 - TOTAL_PASSED_TEST: int = 0 - TOTAL_FAILED_TEST: int = 0 - WORKFLOW_RUN_ID: int = 0 - DESCRIPTION_TEST_RESULTS: Dict[str, Any] = field(default_factory=Dict[str, Any]) - RESULT: Union[float, None] = None diff --git a/github_tests_validator_app/lib/models/users.py b/github_tests_validator_app/lib/models/users.py deleted file mode 100644 index 19720d2..0000000 --- a/github_tests_validator_app/lib/models/users.py +++ /dev/null @@ -1,10 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class GitHubUser: - - LOGIN: str = "" - URL: str = "" - ID: str = "" - CREATED_AT: str = "" diff --git a/github_tests_validator_app/lib/utils.py b/github_tests_validator_app/lib/utils.py index 7717814..7cb8ca5 100644 --- a/github_tests_validator_app/lib/utils.py +++ b/github_tests_validator_app/lib/utils.py @@ -1,13 +1,10 @@ -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Optional import hashlib -import logging from datetime import datetime from github import ContentFile -from github_tests_validator_app.config import DATE_FORMAT -from github_tests_validator_app.lib.connectors.google_sheet import GSheetConnector -from github_tests_validator_app.lib.models.users import GitHubUser +from github_tests_validator_app.lib.connectors.sqlalchemy_client import User def get_hash_files(contents: List[ContentFile.ContentFile]) -> str: @@ -19,12 +16,12 @@ def get_hash_files(contents: List[ContentFile.ContentFile]) -> str: return str(hash.hexdigest()) -def init_github_user_from_github_event(data: Dict[str, Any]) -> Union[GitHubUser, None]: +def init_github_user_from_github_event(data: Dict[str, Any]) -> Optional[User]: - if not "repository" in data or not "owner" in data["repository"]: + if not "sender" in data: return None - login = data["repository"]["owner"].get("login", None) - id = data["repository"]["owner"].get("id", None) - url = data["repository"]["owner"].get("url", None) - return GitHubUser(LOGIN=login, ID=id, URL=url, CREATED_AT=datetime.now().strftime(DATE_FORMAT)) + login = data["sender"]["login"] + id = data["sender"]["id"] + url = data["sender"]["url"] + return User(id=id, organization_or_user=login, url=url, created_at=datetime.now()) diff --git a/poetry.lock b/poetry.lock index 94c9a50..1429b1d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -15,6 +15,14 @@ doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] +[[package]] +name = "asn1crypto" +version = "1.5.1" +description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "astroid" version = "2.12.11" @@ -26,10 +34,7 @@ python-versions = ">=3.7.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} -wrapt = [ - {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, -] +wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} [[package]] name = "attrs" @@ -64,6 +69,21 @@ test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr toml = ["toml"] yaml = ["pyyaml"] +[[package]] +name = "beautifulsoup4" +version = "4.11.1" +description = "Screen-scraping library" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "black" version = "22.10.0" @@ -86,6 +106,38 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "boto3" +version = "1.24.90" +description = "The AWS SDK for Python" +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.27.90,<1.28.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.6.0,<0.7.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.27.90" +description = "Low-level, data-driven core of boto 3." +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = ">=1.25.4,<1.27" + +[package.extras] +crt = ["awscrt (==0.14.0)"] + [[package]] name = "cachetools" version = "5.2.0" @@ -164,7 +216,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "cryptography" -version = "38.0.1" +version = "36.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -177,9 +229,9 @@ cffi = ">=1.12" docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools-rust (>=0.11.4)"] +sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] [[package]] name = "darglint" @@ -260,7 +312,7 @@ test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "databases[sqlite] ( name = "filelock" version = "3.8.0" description = "A platform independent file lock." -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -268,6 +320,14 @@ python-versions = ">=3.7" docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] +[[package]] +name = "future" +version = "0.18.2" +description = "Clean single-source support for Python 3 and 2" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "gitdb" version = "4.0.9" @@ -281,7 +341,7 @@ smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.28" +version = "3.1.29" description = "GitPython is a python library used to interact with Git repositories" category = "dev" optional = false @@ -311,21 +371,6 @@ grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio-status (>=1.33.2,<2.0dev)"] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] -[[package]] -name = "google-api-python-client" -version = "2.64.0" -description = "Google API Client Library for Python" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -google-api-core = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0dev" -google-auth = ">=1.19.0,<3.0.0dev" -google-auth-httplib2 = ">=0.1.0" -httplib2 = ">=0.15.0,<1dev" -uritemplate = ">=3.0.1,<5" - [[package]] name = "google-auth" version = "2.12.0" @@ -347,57 +392,76 @@ pyopenssl = ["pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] [[package]] -name = "google-auth-httplib2" -version = "0.1.0" -description = "Google Authentication Library: httplib2 transport" +name = "google-cloud-appengine-logging" +version = "1.1.6" +description = "" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7" [package.dependencies] -google-auth = "*" -httplib2 = ">=0.15.0" -six = "*" +google-api-core = {version = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0dev", extras = ["grpc"]} +proto-plus = ">=1.22.0,<2.0.0dev" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" [[package]] -name = "google-auth-oauthlib" -version = "0.5.3" -description = "Google Authentication Library" +name = "google-cloud-audit-log" +version = "0.2.4" +description = "Google Cloud Audit Protos" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -google-auth = ">=1.0.0" -requests-oauthlib = ">=0.7.0" - -[package.extras] -tool = ["click (>=6.0.0)"] +googleapis-common-protos = ">=1.56.2,<2.0dev" +protobuf = ">=3.6.0,<5.0.0dev" [[package]] -name = "google-cloud-appengine-logging" -version = "1.1.5" -description = "" +name = "google-cloud-bigquery" +version = "3.3.5" +description = "Google BigQuery API client library" category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.7, <3.11" [package.dependencies] google-api-core = {version = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0dev", extras = ["grpc"]} +google-cloud-bigquery-storage = ">=2.0.0,<3.0.0dev" +google-cloud-core = ">=1.4.1,<3.0.0dev" +google-resumable-media = ">=0.6.0,<3.0dev" +grpcio = ">=1.47.0,<2.0dev" +packaging = ">=14.3,<22.0.0dev" proto-plus = ">=1.22.0,<2.0.0dev" -protobuf = ">=3.20.2,<5.0.0dev" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" +pyarrow = ">=3.0.0,<10.0dev" +python-dateutil = ">=2.7.2,<3.0dev" +requests = ">=2.18.0,<3.0.0dev" + +[package.extras] +all = ["pandas (>=1.0.0)", "db-dtypes (>=0.3.0,<2.0.0dev)", "geopandas (>=0.9.0,<1.0dev)", "Shapely (>=1.6.0,<2.0dev)", "ipython (>=7.0.1,!=8.1.0)", "tqdm (>=4.7.4,<5.0.0dev)", "opentelemetry-api (>=1.1.0)", "opentelemetry-sdk (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)"] +geopandas = ["geopandas (>=0.9.0,<1.0dev)", "Shapely (>=1.6.0,<2.0dev)"] +ipython = ["ipython (>=7.0.1,!=8.1.0)"] +opentelemetry = ["opentelemetry-api (>=1.1.0)", "opentelemetry-sdk (>=1.1.0)", "opentelemetry-instrumentation (>=0.20b0)"] +pandas = ["pandas (>=1.0.0)", "db-dtypes (>=0.3.0,<2.0.0dev)"] +tqdm = ["tqdm (>=4.7.4,<5.0.0dev)"] [[package]] -name = "google-cloud-audit-log" -version = "0.2.4" -description = "Google Cloud Audit Protos" +name = "google-cloud-bigquery-storage" +version = "2.16.2" +description = "BigQuery Storage API API client library" category = "main" optional = false python-versions = ">=3.7" [package.dependencies] -googleapis-common-protos = ">=1.56.2,<2.0dev" -protobuf = ">=3.6.0,<5.0.0dev" +google-api-core = {version = ">=1.32.0,<2.0.0 || >=2.8.0,<3.0.0dev", extras = ["grpc"]} +proto-plus = ">=1.22.0,<2.0.0dev" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" + +[package.extras] +fastavro = ["fastavro (>=0.21.2)"] +pandas = ["pandas (>=0.21.1)"] +pyarrow = ["pyarrow (>=0.15.0)"] [[package]] name = "google-cloud-core" @@ -416,7 +480,7 @@ grpc = ["grpcio (>=1.38.0,<2.0dev)"] [[package]] name = "google-cloud-logging" -version = "3.2.4" +version = "3.2.5" description = "Stackdriver Logging API client library" category = "main" optional = false @@ -429,7 +493,33 @@ google-cloud-audit-log = ">=0.1.0,<1.0.0dev" google-cloud-core = ">=2.0.0,<3.0.0dev" grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" proto-plus = ">=1.22.0,<2.0.0dev" -protobuf = ">=3.20.2,<5.0.0dev" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0dev" + +[[package]] +name = "google-crc32c" +version = "1.5.0" +description = "A python wrapper of the C library 'Google CRC32C'" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +testing = ["pytest"] + +[[package]] +name = "google-resumable-media" +version = "2.4.0" +description = "Utilities for Google Media Downloads and Resumable Uploads" +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +google-crc32c = ">=1.0,<2.0dev" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0dev)"] +requests = ["requests (>=2.18.0,<3.0.0dev)"] [[package]] name = "googleapis-common-protos" @@ -446,6 +536,17 @@ protobuf = ">=3.15.0,<5.0.0dev" [package.extras] grpc = ["grpcio (>=1.0.0,<2.0.0dev)"] +[[package]] +name = "greenlet" +version = "1.1.3.post0" +description = "Lightweight in-process concurrent programming" +category = "main" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" + +[package.extras] +docs = ["sphinx"] + [[package]] name = "grpc-google-iam-v1" version = "0.12.4" @@ -485,18 +586,6 @@ googleapis-common-protos = ">=1.5.5" grpcio = ">=1.49.1" protobuf = ">=4.21.3" -[[package]] -name = "gspread" -version = "5.6.0" -description = "Google Spreadsheets Python API" -category = "main" -optional = false -python-versions = ">=3.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -google-auth = ">=1.12.0" -google-auth-oauthlib = ">=0.4.1" - [[package]] name = "h11" version = "0.14.0" @@ -505,17 +594,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "httplib2" -version = "0.20.4" -description = "A comprehensive HTTP client library." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} - [[package]] name = "identify" version = "2.5.6" @@ -557,6 +635,14 @@ requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "lazy-object-proxy" version = "1.7.1" @@ -565,6 +651,20 @@ category = "dev" optional = false python-versions = ">=3.6" +[[package]] +name = "lxml" +version = "4.9.1" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["beautifulsoup4"] +source = ["Cython (>=0.29.7)"] + [[package]] name = "mccabe" version = "0.7.0" @@ -608,17 +708,23 @@ optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" [[package]] -name = "oauthlib" -version = "3.2.1" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" +name = "numpy" +version = "1.23.4" +description = "NumPy is the fundamental package for array computing with Python." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" -[package.extras] -rsa = ["cryptography (>=3.0.0)"] -signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] +[[package]] +name = "oscrypto" +version = "1.3.0" +description = "TLS (SSL) sockets, key generation, encryption, decryption, signing, verification and KDFs using the OS crypto libraries. Does not require a compiler, and relies on the OS for patching. Works on Windows, OS X and Linux/BSD." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +asn1crypto = ">=1.5.1" [[package]] name = "packaging" @@ -717,6 +823,17 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "pyarrow" +version = "6.0.1" +description = "Python library for Apache Arrow" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +numpy = ">=1.16.6" + [[package]] name = "pyasn1" version = "0.4.8" @@ -744,6 +861,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pycryptodomex" +version = "3.15.0" +description = "Cryptographic library for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + [[package]] name = "pydantic" version = "1.10.2" @@ -775,7 +900,7 @@ toml = ["toml"] [[package]] name = "pygithub" -version = "1.55" +version = "1.56" description = "Use the full Github API v3" category = "main" optional = false @@ -853,6 +978,21 @@ cffi = ">=1.4.1" docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] +[[package]] +name = "pyopenssl" +version = "22.0.0" +description = "Python wrapper module around the OpenSSL library" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cryptography = ">=35.0" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +test = ["flaky", "pretend", "pytest (>=3.0.1)"] + [[package]] name = "pyparsing" version = "3.0.9" @@ -898,9 +1038,28 @@ pytest = ">=5.0" [package.extras] dev = ["pre-commit", "tox", "pytest-asyncio"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2022.4" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pyupgrade" -version = "3.0.0" +version = "3.1.0" description = "A tool to automatically upgrade syntax for newer versions." category = "dev" optional = false @@ -917,6 +1076,27 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "redshift-connector" +version = "2.0.909" +description = "Redshift interface library" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +beautifulsoup4 = ">=4.7.0,<5.0.0" +boto3 = ">=1.9.201,<2.0.0" +botocore = ">=1.12.201,<2.0.0" +lxml = ">=4.6.5" +packaging = "*" +pytz = ">=2020.1" +requests = ">=2.23.0,<3.0.0" +scramp = ">=1.2.0,<1.5.0" + +[package.extras] +full = ["numpy", "pandas"] + [[package]] name = "requests" version = "2.28.1" @@ -935,21 +1115,6 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] -[[package]] -name = "requests-oauthlib" -version = "1.3.1" -description = "OAuthlib authentication support for Requests." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -oauthlib = ">=3.0.0" -requests = ">=2.0.0" - -[package.extras] -rsa = ["oauthlib[signedtoken] (>=3.0.0)"] - [[package]] name = "rich" version = "12.6.0" @@ -999,6 +1164,20 @@ category = "dev" optional = false python-versions = ">=3.5" +[[package]] +name = "s3transfer" +version = "0.6.0" +description = "An Amazon S3 Transfer Manager" +category = "main" +optional = false +python-versions = ">= 3.7" + +[package.dependencies] +botocore = ">=1.12.36,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.20.29,<2.0a.0)"] + [[package]] name = "safety" version = "2.3.1" @@ -1018,6 +1197,17 @@ requests = "*" github = ["pygithub (>=1.43.3)", "jinja2 (>=3.1.0)"] gitlab = ["python-gitlab (>=1.3.0)"] +[[package]] +name = "scramp" +version = "1.4.1" +description = "An implementation of the SCRAM protocol." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +asn1crypto = ">=1.4.0" + [[package]] name = "shellingham" version = "1.5.0" @@ -1058,6 +1248,151 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "snowflake-connector-python" +version = "2.8.0" +description = "Snowflake Connector for Python" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +asn1crypto = ">0.24.0,<2.0.0" +certifi = ">=2017.4.17" +cffi = ">=1.9,<2.0.0" +charset-normalizer = ">=2,<3" +cryptography = ">=3.1.0,<37.0.0" +filelock = ">=3.5,<4" +idna = ">=2.5,<4" +oscrypto = "<2.0.0" +pycryptodomex = ">=3.2,<3.5.0 || >3.5.0,<4.0.0" +pyjwt = "<3.0.0" +pyOpenSSL = ">=16.2.0,<23.0.0" +pytz = "*" +requests = "<3.0.0" +typing-extensions = ">=4.3,<5" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +development = ["cython", "coverage", "more-itertools", "numpy (<1.24.0)", "pendulum (!=2.1.1)", "pexpect", "pytest (<7.2.0)", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "pytest-xdist", "pytzdata"] +pandas = ["pandas (>=1.0.0,<1.5.0)", "pyarrow (>=8.0.0,<8.1.0)"] +secure-local-storage = ["keyring (!=16.1.0,<24.0.0)"] + +[[package]] +name = "snowflake-sqlalchemy" +version = "1.4.2" +description = "Snowflake SQLAlchemy Dialect" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +snowflake-connector-python = "<3.0.0" +sqlalchemy = ">=1.4.0,<2.0.0" + +[package.extras] +development = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-timeout", "mock", "pytz", "numpy"] +pandas = ["snowflake-connector-python[pandas] (<3.0.0)"] + +[[package]] +name = "soupsieve" +version = "2.3.2.post1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "sqlalchemy" +version = "1.4.27" +description = "Database Abstraction Library" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} + +[package.extras] +aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] +aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +asyncio = ["greenlet (!=0.4.17)"] +asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3)"] +mariadb_connector = ["mariadb (>=1.0.1)"] +mssql = ["pyodbc"] +mssql_pymssql = ["pymssql"] +mssql_pyodbc = ["pyodbc"] +mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] +mysql_connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] +postgresql_pg8000 = ["pg8000 (>=1.16.6)"] +postgresql_psycopg2binary = ["psycopg2-binary"] +postgresql_psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql (<1)", "pymysql"] +sqlcipher = ["sqlcipher3-binary"] + +[[package]] +name = "sqlalchemy-bigquery" +version = "1.4.4" +description = "SQLAlchemy dialect for BigQuery" +category = "main" +optional = false +python-versions = ">=3.6, <3.11" + +[package.dependencies] +future = "*" +google-api-core = ">=1.31.5,<2.0.0 || >2.3.0,<3.0.0dev" +google-auth = ">=1.25.0,<3.0.0dev" +google-cloud-bigquery = ">=2.25.2,<4.0.0dev" +google-cloud-bigquery-storage = ">=2.0.0,<3.0.0dev" +pyarrow = ">=3.0.0,<7.0dev" +sqlalchemy = ">=1.2.0,<=1.4.27" + +[package.extras] +alembic = ["alembic"] +all = ["alembic", "shapely", "pytz", "geoalchemy2", "packaging"] +geography = ["geoalchemy2", "shapely"] +tests = ["packaging", "pytz"] + +[[package]] +name = "sqlalchemy-redshift" +version = "0.8.11" +description = "Amazon Redshift Dialect for sqlalchemy" +category = "main" +optional = false +python-versions = ">=3.4" + +[package.dependencies] +packaging = "*" +SQLAlchemy = ">=0.9.2,<2.0.0" + +[[package]] +name = "sqlalchemy2-stubs" +version = "0.0.2a29" +description = "Typing Stubs for SQLAlchemy 1.4" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +typing-extensions = ">=3.7.4" + +[[package]] +name = "sqlmodel" +version = "0.0.8" +description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." +category = "main" +optional = false +python-versions = ">=3.6.1,<4.0.0" + +[package.dependencies] +pydantic = ">=1.8.2,<2.0.0" +SQLAlchemy = ">=1.4.17,<=1.4.41" +sqlalchemy2-stubs = "*" + [[package]] name = "starlette" version = "0.20.4" @@ -1075,7 +1410,7 @@ full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] [[package]] name = "stevedore" -version = "4.0.0" +version = "4.0.1" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false @@ -1171,14 +1506,6 @@ category = "main" optional = false python-versions = ">=3.7" -[[package]] -name = "uritemplate" -version = "4.1.1" -description = "Implementation of RFC 6570 URI Templates" -category = "main" -optional = false -python-versions = ">=3.6" - [[package]] name = "urllib3" version = "1.26.12" @@ -1234,18 +1561,25 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [metadata] lock-version = "1.1" -python-versions = "^3.9" -content-hash = "d9477286cf1bdf683f93d06e49de5cdb4022bbd01cef903fbe861965aab08f59" +python-versions = ">=3.9, <3.11" +content-hash = "08e99f651f7851e76d9fc5ff6c4c7c94f99d75cc63bb156af62443a455c744ed" [metadata.files] anyio = [ {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, ] +asn1crypto = [] astroid = [] attrs = [] bandit = [] +beautifulsoup4 = [ + {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, + {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, +] black = [] +boto3 = [] +botocore = [] cachetools = [ {file = "cachetools-5.2.0-py3-none-any.whl", hash = "sha256:f9f17d2aec496a9aa6b76f53e3b614c965223c061982d434d160f930c698a9db"}, {file = "cachetools-5.2.0.tar.gz", hash = "sha256:6a94c6402995a99c3970cc7e4884bb60b4a8639938157eeed436098bf9831757"}, @@ -1277,33 +1611,28 @@ distlib = [] dparse = [] fastapi = [] filelock = [] +future = [] gitdb = [] gitpython = [] google-api-core = [] -google-api-python-client = [] google-auth = [] -google-auth-httplib2 = [ - {file = "google-auth-httplib2-0.1.0.tar.gz", hash = "sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac"}, - {file = "google_auth_httplib2-0.1.0-py2.py3-none-any.whl", hash = "sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10"}, -] -google-auth-oauthlib = [] google-cloud-appengine-logging = [] google-cloud-audit-log = [] +google-cloud-bigquery = [] +google-cloud-bigquery-storage = [] google-cloud-core = [] google-cloud-logging = [] +google-crc32c = [] +google-resumable-media = [] googleapis-common-protos = [] +greenlet = [] grpc-google-iam-v1 = [ {file = "grpc-google-iam-v1-0.12.4.tar.gz", hash = "sha256:3f0ac2c940b9a855d7ce7e31fde28bddb0d9ac362d32d07c67148306931a0e30"}, {file = "grpc_google_iam_v1-0.12.4-py2.py3-none-any.whl", hash = "sha256:312801ae848aeb8408c099ea372b96d253077e7851aae1a9e745df984f81f20c"}, ] grpcio = [] grpcio-status = [] -gspread = [] h11 = [] -httplib2 = [ - {file = "httplib2-0.20.4-py3-none-any.whl", hash = "sha256:8b6a905cb1c79eefd03f8669fd993c36dc341f7c558f056cb5a33b5c2f458543"}, - {file = "httplib2-0.20.4.tar.gz", hash = "sha256:58a98e45b4b1a48273073f905d2961666ecf0fbac4250ea5b47aef259eb5c585"}, -] identify = [] idna = [] iniconfig = [ @@ -1314,6 +1643,7 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] +jmespath = [] lazy-object-proxy = [ {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, @@ -1353,6 +1683,7 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, ] +lxml = [] mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -1363,7 +1694,8 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] nodeenv = [] -oauthlib = [] +numpy = [] +oscrypto = [] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -1385,6 +1717,7 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +pyarrow = [] pyasn1 = [ {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, @@ -1419,6 +1752,7 @@ pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] +pycryptodomex = [] pydantic = [] pydocstyle = [] pygithub = [] @@ -1426,12 +1760,18 @@ pygments = [] pyjwt = [] pylint = [] pynacl = [] +pyopenssl = [] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [] pytest-mock = [] +python-dateutil = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] +pytz = [] pyupgrade = [] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -1468,13 +1808,15 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +redshift-connector = [] requests = [] -requests-oauthlib = [] rich = [] rsa = [] "ruamel.yaml" = [] "ruamel.yaml.clib" = [] +s3transfer = [] safety = [] +scramp = [] shellingham = [] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1486,6 +1828,17 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +snowflake-connector-python = [] +snowflake-sqlalchemy = [] +soupsieve = [ + {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, + {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, +] +sqlalchemy = [] +sqlalchemy-bigquery = [] +sqlalchemy-redshift = [] +sqlalchemy2-stubs = [] +sqlmodel = [] starlette = [] stevedore = [] tokenize-rt = [] @@ -1503,10 +1856,6 @@ types-pyyaml = [] types-requests = [] types-urllib3 = [] typing-extensions = [] -uritemplate = [ - {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, - {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, -] urllib3 = [] uvicorn = [] virtualenv = [] diff --git a/pyproject.toml b/pyproject.toml index 531d608..fa91666 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ # Update me "launch_github_app" = "github_tests_validator_app.bin.github_app_backend:launch_app" [tool.poetry.dependencies] -python = "^3.9" +python = ">=3.9, <3.11" importlib_metadata = {version = ">=1.6.0", python = "<3.8"} typer = {extras = ["all"], version = ">=0.3.2"} rich = ">=10.1.0" @@ -42,15 +42,16 @@ requests = ">=2.22.0" PyGithub = ">=1.55" cryptography = ">=36.0.1" urllib3 = ">=1.26.5" -gspread = "^5.4.0" types-requests = "^2.28.9" pytest-mock = "^3.8.2" types-PyYAML = "^6.0.11" -google-api-python-client = "^2.60.0" -google-auth-httplib2 = "^0.1.0" -google-auth-oauthlib = "^0.5.2" PyYAML = "^6.0" google-cloud-logging = "^3.2.2" +sqlmodel = "^0.0.8" +sqlalchemy-bigquery = "^1.4.4" +redshift-connector = "^2.0.909" +sqlalchemy-redshift = "^0.8.11" +snowflake-sqlalchemy = "^1.4.2" [tool.poetry.dev-dependencies] darglint = ">=1.8.0" diff --git a/tests/units/test_github_repo_validation.py b/tests/units/test_github_repo_validation.py index a3e32d4..7891b66 100644 --- a/tests/units/test_github_repo_validation.py +++ b/tests/units/test_github_repo_validation.py @@ -1,5 +1,5 @@ import pytest -from github_tests_validator_app.bin.github_repo_validation import get_event, get_student_branch +from github_tests_validator_app.bin.github_repo_validation import get_event, get_user_branch @pytest.mark.parametrize( @@ -25,5 +25,5 @@ def test_get_event(payload, expected): ({"ref": "path"}, "pusher", "path"), ], ) -def test_get_student_branch(payload, trigger, expected): - assert get_student_branch(payload, trigger) == expected +def test_get_user_branch(payload, trigger, expected): + assert get_user_branch(payload, trigger) == expected diff --git a/tests/units/test_utils.py b/tests/units/test_utils.py index fd0d4d1..d1d7465 100644 --- a/tests/units/test_utils.py +++ b/tests/units/test_utils.py @@ -2,7 +2,7 @@ import pytest from github import ContentFile -from github_tests_validator_app.lib.models.users import GitHubUser +from github_tests_validator_app.lib.connectors.sqlalchemy_client import User from github_tests_validator_app.lib.utils import get_hash_files, init_github_user_from_github_event @@ -32,12 +32,12 @@ def test_get_hast_files(mocker, contents, expected): "contents,expected", [ ( - {"repository": {"owner": {"login": "test", "id": "1234", "url": "url"}}}, - GitHubUser(LOGIN="test", ID="1234", URL="url"), + {"sender": {"login": "test", "id": "1234", "url": "url"}}, + User(organization_or_user="test", id="1234", url="url"), ), ( - {"repository": {"owner": {"login": "", "id": "", "url": ""}}}, - GitHubUser(LOGIN="", ID="", URL=""), + {"sender": {"login": "", "id": "", "url": ""}}, + User(organization_or_user="", id="", url=""), ), ({}, None), ], @@ -45,9 +45,9 @@ def test_get_hast_files(mocker, contents, expected): def test_init_github_user_from_github_event(contents, expected): github_user = init_github_user_from_github_event(contents) assert isinstance(github_user, type(expected)) - if isinstance(github_user, GitHubUser): + if isinstance(github_user, User): assert ( - github_user.LOGIN == expected.LOGIN - and github_user.ID == expected.ID - and github_user.URL == expected.URL + github_user.organization_or_user == expected.organization_or_user + and github_user.id == expected.id + and github_user.url == expected.url ) From 5d1bde8d34658ee8a0220054cc43e14ede15f5c3 Mon Sep 17 00:00:00 2001 From: Cedric-Magnan Date: Tue, 15 Nov 2022 18:23:56 +0100 Subject: [PATCH 2/3] fix(sqlalchemy): deploy to GCP --- .gitignore | 1 + README.md | 3 +- examples/cloud_run/deploy.sh | 6 +-- examples/cloud_run/main.tf | 26 +++++++++---- .../bin/github_event_process.py | 10 ++--- .../bin/github_repo_validation.py | 30 +++++++------- .../bin/user_pytest_summaries_validation.py | 15 ++++--- .../lib/connectors/github_client.py | 28 +++++++++---- .../lib/connectors/sqlalchemy_client.py | 39 +++++++++++++------ github_tests_validator_app/lib/utils.py | 4 +- poetry.lock | 24 +++++++++--- pyproject.toml | 1 + tests/units/test_utils.py | 4 +- 13 files changed, 122 insertions(+), 69 deletions(-) diff --git a/.gitignore b/.gitignore index 580b6d3..673d00b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ Icon .terraform/ terraform.tfstate terraform.tfstate.backup +terraform.tfstate.*.backup .terraform.lock.hcl # Files that might appear in the root of a volume diff --git a/README.md b/README.md index 8150b64..14f2c82 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,7 @@ With Cloud Run, you have an example terraform configuration [here](https://githu But you can deploy the application on many Serverless Container services on any cloud by making sure that : - The secrets defined in the `.env` file are available for the container at runtime as environment variables - The container can receive HTTP requests -- The container can use a GCP service account to login with the [Python Google Auth client](https://google-auth.readthedocs.io/en/master/) -- The service account is linked to a GCP Project which has the Google Drive API enabled +- The container can login to any data warehouse with a SQLAlchemy Connection URI : [Bigquery](https://googleapis.dev/python/sqlalchemy-bigquery/latest/README.html#usage), [Snowflake](https://docs.snowflake.com/en/user-guide/sqlalchemy.html#connection-parameters), [Redshift](https://aws.amazon.com/fr/blogs/big-data/use-the-amazon-redshift-sqlalchemy-dialect-to-interact-with-amazon-redshift/) ## Environment variables details diff --git a/examples/cloud_run/deploy.sh b/examples/cloud_run/deploy.sh index d3b06c9..77b07d2 100755 --- a/examples/cloud_run/deploy.sh +++ b/examples/cloud_run/deploy.sh @@ -18,7 +18,7 @@ echo "$GH_TESTS_REPO_NAME" | gcloud secrets versions add GH_TESTS_REPO_NAME --da echo "$SQLALCHEMY_URI" | gcloud secrets versions add SQLALCHEMY_URI --data-file=- echo "$LOGGING" | gcloud secrets versions add LOGGING --data-file=- set -o history -gcloud auth configure-docker ${REGION}-docker.pkg.dev -docker build -t ${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/github_tests_validator_app:latest -f ./docker/Dockerfile . -docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/github_tests_validator_app:latest +gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://${REGION}-docker.pkg.dev +docker build -t ${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/github_tests_validator_app -f ./docker/Dockerfile . +docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/github_tests_validator_app terraform -chdir=examples/cloud_run apply -input=true diff --git a/examples/cloud_run/main.tf b/examples/cloud_run/main.tf index 63705f6..8bd8a7e 100644 --- a/examples/cloud_run/main.tf +++ b/examples/cloud_run/main.tf @@ -22,12 +22,6 @@ provider "google" { region = "${var.region}" } -resource "google_project_service" "drive_api_service" { - project = "${var.project_id}" - service = "drive.googleapis.com" - disable_dependent_services = true -} - resource "google_service_account" "service_account" { project = "${var.project_id}" account_id = "github-tests-validator-app" @@ -61,6 +55,24 @@ resource "google_project_iam_binding" "secret_accessor" { ] } +resource "google_project_iam_binding" "bigquery_job_user" { + project = "${var.project_id}" + role = "roles/bigquery.jobUser" + + members = [ + "serviceAccount:github-tests-validator-app@${var.project_id}.iam.gserviceaccount.com", + ] +} + +resource "google_project_iam_binding" "bigquery_data_editor" { + project = "${var.project_id}" + role = "roles/bigquery.dataEditor" + + members = [ + "serviceAccount:github-tests-validator-app@${var.project_id}.iam.gserviceaccount.com", + ] +} + resource "google_artifact_registry_repository" "github_test_validator_app_registry" { location = "${var.region}" repository_id = "github-app-registry" @@ -76,7 +88,7 @@ resource "google_cloud_run_service" "github_test_validator_app" { timeout_seconds = 300 service_account_name = "github-tests-validator-app@${var.project_id}.iam.gserviceaccount.com" containers { - image = "${var.region}-docker.pkg.dev/${var.project_id}/github-app-registry/github_tests_validator_app:latest" + image = "${var.region}-docker.pkg.dev/${var.project_id}/github-app-registry/github_tests_validator_app" env { name = "GH_APP_ID" value_from { diff --git a/github_tests_validator_app/bin/github_event_process.py b/github_tests_validator_app/bin/github_event_process.py index 05d62c8..aa958a7 100644 --- a/github_tests_validator_app/bin/github_event_process.py +++ b/github_tests_validator_app/bin/github_event_process.py @@ -48,20 +48,20 @@ def run(payload: Dict[str, Any]) -> None: return # Init User - user = init_github_user_from_github_event(payload) - if not isinstance(user, User): + user_data = init_github_user_from_github_event(payload) + if not isinstance(user_data, dict): # Logging return sql_client = SQLAlchemyConnector() - sql_client.add_new_user(user) + sql_client.add_new_user(user_data) # Check valid repo - user_github_connector = get_user_github_connector(user, payload) + user_github_connector = get_user_github_connector(user_data, payload) if not user_github_connector: sql_client.add_new_repository_validation( - user, + user_data, False, payload, event, diff --git a/github_tests_validator_app/bin/github_repo_validation.py b/github_tests_validator_app/bin/github_repo_validation.py index 1bbfca7..da8f96c 100644 --- a/github_tests_validator_app/bin/github_repo_validation.py +++ b/github_tests_validator_app/bin/github_repo_validation.py @@ -48,16 +48,18 @@ def get_user_branch(payload: Dict[str, Any], trigger: Union[str, None] = None) - return branch -def get_user_github_connector(user: User, payload: Dict[str, Any]) -> Union[GitHubConnector, None]: +def get_user_github_connector( + user_data: Dict[str, Any], payload: Dict[str, Any] +) -> Union[GitHubConnector, None]: - if not user: + if not user_data: return None github_user_branch = get_user_branch(payload) if github_user_branch is None: return None - return GitHubConnector(user, payload["repository"]["full_name"], github_user_branch) + return GitHubConnector(user_data, payload["repository"]["full_name"], github_user_branch) def compare_folder( @@ -78,7 +80,7 @@ def compare_folder( def validate_github_repo( user_github_connector: GitHubConnector, - gsheet: SQLAlchemyConnector, + sql_client: SQLAlchemyConnector, payload: Dict[str, Any], event: str, ) -> None: @@ -86,7 +88,7 @@ def validate_github_repo( logging.info(f"Connecting to repo : {GH_TESTS_REPO_NAME}") tests_github_connector = GitHubConnector( - user=user_github_connector.user, + user_data=user_github_connector.user_data, repo_name=GH_TESTS_REPO_NAME if GH_TESTS_REPO_NAME else user_github_connector.repo.parent.full_name, @@ -97,14 +99,14 @@ def validate_github_repo( logging.info(f"Connecting to repo : {user_github_connector.repo.parent.full_name}") original_github_connector = GitHubConnector( - user=user_github_connector.user, + user_data=user_github_connector.user_data, repo_name=user_github_connector.repo.parent.full_name, branch_name="main", access_token=GH_PAT, ) if not tests_github_connector: - gsheet.add_new_repository_validation( - user_github_connector.user, + sql_client.add_new_repository_validation( + user_github_connector.user_data, False, payload, event, @@ -113,8 +115,8 @@ def validate_github_repo( logging.error("[ERROR]: cannot get the tests github repository.") return if not original_github_connector: - gsheet.add_new_repository_validation( - user_github_connector.user, + sql_client.add_new_repository_validation( + user_github_connector.user_data, False, payload, event, @@ -131,15 +133,15 @@ def validate_github_repo( ) # Add valid repo result on Google Sheet - gsheet.add_new_repository_validation( - user_github_connector.user, + sql_client.add_new_repository_validation( + user_github_connector.user_data, workflows_havent_changed, payload, event, default_message["valid_repository"]["workflows"][str(workflows_havent_changed)], ) - gsheet.add_new_repository_validation( - user_github_connector.user, + sql_client.add_new_repository_validation( + user_github_connector.user_data, tests_havent_changed, payload, event, diff --git a/github_tests_validator_app/bin/user_pytest_summaries_validation.py b/github_tests_validator_app/bin/user_pytest_summaries_validation.py index b63b51d..883fcbf 100644 --- a/github_tests_validator_app/bin/user_pytest_summaries_validation.py +++ b/github_tests_validator_app/bin/user_pytest_summaries_validation.py @@ -22,7 +22,7 @@ def get_user_artifact( sql_client.add_new_pytest_summary( {}, workflow_run_id, - user_github_connector.user, + user_github_connector.user_data, user_github_connector.REPO_NAME, user_github_connector.BRANCH_NAME, info="[ERROR]: Cannot find the artifact of Pytest result on GitHub user repository.", @@ -39,7 +39,7 @@ def get_user_artifact( sql_client.add_new_pytest_summary( {}, workflow_run_id, - user_github_connector.user, + user_github_connector.user_data, user_github_connector.REPO_NAME, user_github_connector.BRANCH_NAME, info="[ERROR]: Cannot read the artifact of Pytest result on GitHub user repository.", @@ -85,19 +85,19 @@ def send_user_pytest_summaries( user_github_connector: GitHubConnector, sql_client: SQLAlchemyConnector, payload: Dict[str, Any], - event: str, + _: str, ) -> None: # Get all artifacts all_user_artifact = user_github_connector.get_all_artifacts() if not all_user_artifact: - message = f"[ERROR]: Cannot get all artifact on repository {user_github_connector.REPO_NAME} of user {user_github_connector.user.LOGIN}." + message = f"[ERROR]: Cannot get all artifact on repository {user_github_connector.REPO_NAME} of user {user_github_connector.organization_or_user}." if all_user_artifact["total_count"] == 0: - message = f"[ERROR]: No artifact on repository {user_github_connector.REPO_NAME} of user {user_github_connector.user.LOGIN}." + message = f"[ERROR]: No artifact on repository {user_github_connector.REPO_NAME} of user {user_github_connector.organization_or_user}." sql_client.add_new_pytest_summary( {}, payload["workflow_job"]["run_id"], - user_github_connector.user, + user_github_connector.user_data, user_github_connector.REPO_NAME, user_github_connector.BRANCH_NAME, info=message, @@ -114,7 +114,7 @@ def send_user_pytest_summaries( sql_client.add_new_pytest_summary( artifact, payload["workflow_job"]["run_id"], - user_github_connector.user, + user_github_connector.user_data, user_github_connector.REPO_NAME, user_github_connector.BRANCH_NAME, info="Result of user tests", @@ -124,7 +124,6 @@ def send_user_pytest_summaries( pytest_summaries = parsing_pytest_summaries(artifact["tests"]) # Send new detail results to Google Sheet sql_client.add_new_pytest_detail( - user=user_github_connector.user, repository=user_github_connector.REPO_NAME, branch=user_github_connector.BRANCH_NAME, results=pytest_summaries, diff --git a/github_tests_validator_app/lib/connectors/github_client.py b/github_tests_validator_app/lib/connectors/github_client.py index fd08fc0..0503dc8 100644 --- a/github_tests_validator_app/lib/connectors/github_client.py +++ b/github_tests_validator_app/lib/connectors/github_client.py @@ -13,24 +13,25 @@ GH_APP_ID, GH_APP_KEY, ) -from github_tests_validator_app.lib.connectors.sqlalchemy_client import User from github_tests_validator_app.lib.utils import get_hash_files class GitHubConnector: def __init__( self, - user: User, + user_data: Dict[str, Any], repo_name: str, branch_name: str, access_token: Union[str, None] = None, ) -> None: - self.user = user + self.user_data = user_data self.REPO_NAME = repo_name self.BRANCH_NAME = branch_name self.ACCESS_TOKEN = access_token - logging.info(f"Connecting to Github with user {self.user.LOGIN} on repo: {repo_name} ...") + logging.info( + f"Connecting to Github with user {self.user_data['organization_or_user']} on repo: {repo_name} ..." + ) if not access_token: self.set_git_integration() self.set_access_token(repo_name) @@ -46,12 +47,16 @@ def set_git_integration(self) -> None: def set_access_token(self, repo_name: str) -> None: self.ACCESS_TOKEN = self.git_integration.get_access_token( - self.git_integration.get_installation(self.user.LOGIN, repo_name).id + self.git_integration.get_installation( + self.user_data["organization_or_user"], repo_name + ).id ).token def get_repo(self, repo_name: str) -> Repository.Repository: self.REPO_NAME = repo_name - logging.info(f"Connecting to new repo: {repo_name} with user: {self.user.LOGIN} ...") + logging.info( + f"Connecting to new repo: {repo_name} with user: {self.user_data['organization_or_user']} ..." + ) self.repo = self.connector.get_repo(f"{repo_name}") logging.info("Done.") return self.repo @@ -77,7 +82,14 @@ def get_hash(self, folder_name: str) -> str: return hash def get_all_artifacts(self) -> Union[requests.models.Response, Any]: - url = "/".join([GH_API, self.user.LOGIN, self.REPO_NAME, GH_ALL_ARTIFACT_ENDPOINT]) + url = "/".join( + [ + GH_API, + self.user_data["organization_or_user"], + self.REPO_NAME, + GH_ALL_ARTIFACT_ENDPOINT, + ] + ) headers = self._get_headers() response = self._request_data(url, headers=headers) return response @@ -102,7 +114,7 @@ def get_artifact(self, artifact_info: Dict[str, Any]) -> Union[requests.models.R url = "/".join( [ GH_API, - self.user.LOGIN, + self.user_data["organization_or_user"], self.REPO_NAME, GH_ALL_ARTIFACT_ENDPOINT, artifact_id, diff --git a/github_tests_validator_app/lib/connectors/sqlalchemy_client.py b/github_tests_validator_app/lib/connectors/sqlalchemy_client.py index 541bc51..88d26ad 100644 --- a/github_tests_validator_app/lib/connectors/sqlalchemy_client.py +++ b/github_tests_validator_app/lib/connectors/sqlalchemy_client.py @@ -1,5 +1,6 @@ from typing import Any, Dict, List, Optional +import json import operator from datetime import datetime from functools import reduce @@ -48,9 +49,9 @@ class WorkflowRunDetail(SQLModel, table=True): branch: str script_name: str outcome: str - setup: Dict[str, Any] = Field(sa_column=Column(JSON), default={}) - call: Dict[str, Any] = Field(sa_column=Column(JSON), default={}) - teardown: Dict[str, Any] = Field(sa_column=Column(JSON), default={}) + setup: str = Field(default="{}") + call: str = Field(default="{}") + teardown: str = Field(default="{}") workflow_run_id: int = Field(foreign_key="workflow_run.id") @@ -74,20 +75,26 @@ def __init__(self) -> None: self.engine = create_engine(SQLALCHEMY_URI) SQLModel.metadata.create_all(self.engine) - def add_new_user(self, user: User) -> None: + def add_new_user(self, user_data: Dict[str, Any]) -> None: + user = User(**user_data) with Session(self.engine) as session: session.add(user) session.commit() def add_new_repository_validation( - self, user: User, result: bool, payload: Dict[str, Any], event: str, info: str = "" + self, + user_data: Dict[str, Any], + result: bool, + payload: Dict[str, Any], + event: str, + info: str = "", ) -> None: repository_validation = RepositoryValidation( repository=payload["repository"]["full_name"], branch=reduce(operator.getitem, commit_ref_path[event], payload), created_at=datetime.now(), - organization_or_user=user.organization_or_user, - user_id=user.id, + organization_or_user=user_data["organization_or_user"], + user_id=user_data["id"], is_valid=result, info=info, ) @@ -99,15 +106,15 @@ def add_new_pytest_summary( self, artifact: Dict[str, Any], workflow_run_id: int, - user: User, + user_data: Dict[str, Any], repository: str, branch: str, info: str, ) -> None: pytest_summary = WorkflowRun( id=workflow_run_id, - organization_or_user=user.organization_or_user, - user_id=user.id, + organization_or_user=user_data["organization_or_user"], + user_id=user_data["id"], repository=repository, branch=branch, duration=artifact.get("duration", None), @@ -122,7 +129,6 @@ def add_new_pytest_summary( def add_new_pytest_detail( self, - user: User, repository: str, branch: str, results: List[Dict[str, Any]], @@ -131,7 +137,16 @@ def add_new_pytest_detail( with Session(self.engine) as session: for test in results: pytest_detail = WorkflowRunDetail( - repository=repository, branch=branch, workflow_run_id=workflow_run_id, **test + repository=repository, + branch=branch, + workflow_run_id=workflow_run_id, + file_path=test["file_path"], + test_name=test["test_name"], + script_name=test["script_name"], + outcome=test["outcome"], + setup=json.dumps(test["setup"]), + call=json.dumps(test["call"]), + teardown=json.dumps(test["teardown"]), ) session.add(pytest_detail) session.commit() diff --git a/github_tests_validator_app/lib/utils.py b/github_tests_validator_app/lib/utils.py index 7cb8ca5..eeff3fd 100644 --- a/github_tests_validator_app/lib/utils.py +++ b/github_tests_validator_app/lib/utils.py @@ -16,7 +16,7 @@ def get_hash_files(contents: List[ContentFile.ContentFile]) -> str: return str(hash.hexdigest()) -def init_github_user_from_github_event(data: Dict[str, Any]) -> Optional[User]: +def init_github_user_from_github_event(data: Dict[str, Any]) -> Optional[Dict[str, Any]]: if not "sender" in data: return None @@ -24,4 +24,4 @@ def init_github_user_from_github_event(data: Dict[str, Any]) -> Optional[User]: login = data["sender"]["login"] id = data["sender"]["id"] url = data["sender"]["url"] - return User(id=id, organization_or_user=login, url=url, created_at=datetime.now()) + return dict(id=id, organization_or_user=login, url=url, created_at=datetime.now()) diff --git a/poetry.lock b/poetry.lock index 1429b1d..5d47f95 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1049,6 +1049,17 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "0.21.0" +description = "Read key-value pairs from a .env file and set them as environment variables" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "pytz" version = "2022.4" @@ -1453,7 +1464,7 @@ python-versions = ">=3.6,<4.0" [[package]] name = "typer" -version = "0.6.1" +version = "0.7.0" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." category = "main" optional = false @@ -1466,10 +1477,10 @@ rich = {version = ">=10.11.0,<13.0.0", optional = true, markers = "extra == \"al shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} [package.extras] -test = ["rich (>=10.11.0,<13.0.0)", "isort (>=5.0.6,<6.0.0)", "black (>=22.3.0,<23.0.0)", "mypy (==0.910)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "coverage (>=5.2,<6.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest (>=4.4.0,<5.4.0)", "shellingham (>=1.3.0,<2.0.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.3.1,<2.0.0)"] -all = ["rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)", "colorama (>=0.4.3,<0.5.0)"] +all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)", "rich (>=10.11.0,<13.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "pillow (>=9.3.0,<10.0.0)", "cairosvg (>=2.5.2,<3.0.0)"] +test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<8.0.0)", "pytest-cov (>=2.10.0,<5.0.0)", "coverage (>=6.2,<7.0)", "pytest-xdist (>=1.32.0,<4.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)", "rich (>=10.11.0,<13.0.0)"] [[package]] name = "types-pyyaml" @@ -1562,7 +1573,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [metadata] lock-version = "1.1" python-versions = ">=3.9, <3.11" -content-hash = "08e99f651f7851e76d9fc5ff6c4c7c94f99d75cc63bb156af62443a455c744ed" +content-hash = "50bfa5217ed253ea12cabc67d550ed2f563b19680dc09b07baacccc85173f12b" [metadata.files] anyio = [ @@ -1771,6 +1782,7 @@ python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, ] +python-dotenv = [] pytz = [] pyupgrade = [] pyyaml = [ diff --git a/pyproject.toml b/pyproject.toml index fa91666..905f296 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ sqlalchemy-bigquery = "^1.4.4" redshift-connector = "^2.0.909" sqlalchemy-redshift = "^0.8.11" snowflake-sqlalchemy = "^1.4.2" +python-dotenv = "^0.21.0" [tool.poetry.dev-dependencies] darglint = ">=1.8.0" diff --git a/tests/units/test_utils.py b/tests/units/test_utils.py index d1d7465..0c1fb9b 100644 --- a/tests/units/test_utils.py +++ b/tests/units/test_utils.py @@ -33,11 +33,11 @@ def test_get_hast_files(mocker, contents, expected): [ ( {"sender": {"login": "test", "id": "1234", "url": "url"}}, - User(organization_or_user="test", id="1234", url="url"), + dict(organization_or_user="test", id="1234", url="url"), ), ( {"sender": {"login": "", "id": "", "url": ""}}, - User(organization_or_user="", id="", url=""), + dict(organization_or_user="", id="", url=""), ), ({}, None), ], From bc8c61003999d0a13049e86034c6d725ba064808 Mon Sep 17 00:00:00 2001 From: Cedric-Magnan Date: Wed, 16 Nov 2022 17:12:08 +0100 Subject: [PATCH 3/3] fix(checkpoint): github connector to send validation to PRs --- examples/cloud_run/deploy.sh | 2 ++ examples/cloud_run/main.tf | 7 ++++++- .../lib/connectors/github_client.py | 10 ++++------ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/examples/cloud_run/deploy.sh b/examples/cloud_run/deploy.sh index 77b07d2..b9aa113 100755 --- a/examples/cloud_run/deploy.sh +++ b/examples/cloud_run/deploy.sh @@ -9,6 +9,7 @@ gcloud config set project $PROJECT_ID gcloud auth application-default login export TF_VAR_project_id=$PROJECT_ID export TF_VAR_region=$REGION +export TF_VAR_docker_image="${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/no_image" terraform -chdir=examples/cloud_run apply -input=true set +o history echo "$GH_APP_ID" | gcloud secrets versions add GH_APP_ID --data-file=- @@ -21,4 +22,5 @@ set -o history gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://${REGION}-docker.pkg.dev docker build -t ${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/github_tests_validator_app -f ./docker/Dockerfile . docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/github_tests_validator_app +export TF_VAR_docker_image=$(gcloud artifacts docker images list ${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry --filter="package=${REGION}-docker.pkg.dev/${PROJECT_ID}/github-app-registry/github_tests_validator_app" --sort-by="~UPDATE_TIME" --limit=1 --format="value(format("{0}@{1}",package,version))") terraform -chdir=examples/cloud_run apply -input=true diff --git a/examples/cloud_run/main.tf b/examples/cloud_run/main.tf index 8bd8a7e..ef38e14 100644 --- a/examples/cloud_run/main.tf +++ b/examples/cloud_run/main.tf @@ -8,6 +8,11 @@ variable "region" { description = "GCP region where resources will be deployed" } +variable "docker_image" { + type = string + description = "Docker reference of the image used by Cloud Run" +} + terraform { required_providers { google = { @@ -88,7 +93,7 @@ resource "google_cloud_run_service" "github_test_validator_app" { timeout_seconds = 300 service_account_name = "github-tests-validator-app@${var.project_id}.iam.gserviceaccount.com" containers { - image = "${var.region}-docker.pkg.dev/${var.project_id}/github-app-registry/github_tests_validator_app" + image = "${var.docker_image}" env { name = "GH_APP_ID" value_from { diff --git a/github_tests_validator_app/lib/connectors/github_client.py b/github_tests_validator_app/lib/connectors/github_client.py index 0503dc8..85d5f28 100644 --- a/github_tests_validator_app/lib/connectors/github_client.py +++ b/github_tests_validator_app/lib/connectors/github_client.py @@ -47,16 +47,14 @@ def set_git_integration(self) -> None: def set_access_token(self, repo_name: str) -> None: self.ACCESS_TOKEN = self.git_integration.get_access_token( - self.git_integration.get_installation( - self.user_data["organization_or_user"], repo_name - ).id + installation_id=self.git_integration.get_installation( + repo_name.split("/")[0], repo_name.split("/")[1] + ).id, + user_id=self.user_data["id"], ).token def get_repo(self, repo_name: str) -> Repository.Repository: self.REPO_NAME = repo_name - logging.info( - f"Connecting to new repo: {repo_name} with user: {self.user_data['organization_or_user']} ..." - ) self.repo = self.connector.get_repo(f"{repo_name}") logging.info("Done.") return self.repo