diff --git a/ena-submission/.gitignore b/ena-submission/.gitignore index 9a6ebe65b0..ecb73e18fb 100644 --- a/ena-submission/.gitignore +++ b/ena-submission/.gitignore @@ -1,2 +1,3 @@ .snakemake/ -results/ \ No newline at end of file +results/ +__pycache__ \ No newline at end of file diff --git a/ena-submission/Snakefile b/ena-submission/Snakefile index 510050f54c..d804ae8880 100644 --- a/ena-submission/Snakefile +++ b/ena-submission/Snakefile @@ -63,13 +63,34 @@ rule get_ena_submission_list: --log-level {params.log_level} \ """ -rule get_ena_submission_list_and_sleep: +rule trigger_submission_to_ena: input: - file="results/ena_submission_list.json" + script="scripts/trigger_submission_to_ena.py", + config="results/config.yaml", + output: + submitted=touch("results/triggered"), + params: + log_level=LOG_LEVEL, + shell: + """ + python {input.script} \ + --config-file {input.config} \ + --log-level {params.log_level} \ + """ + +rule trigger_submission_to_ena_from_file: # for testing + input: + script="scripts/trigger_submission_to_ena.py", + input_file="results/approved_ena_submission_list.json", + config="results/config.yaml", output: - file="results/sleep.txt" + submitted=touch("results/triggered"), + params: + log_level=LOG_LEVEL, shell: """ - sleep 360 - touch {output.file} + python {input.script} \ + --config-file {input.config} \ + --input-file {input.input_file} \ + --log-level {params.log_level} \ """ \ No newline at end of file diff --git a/ena-submission/config/defaults.yaml b/ena-submission/config/defaults.yaml index 81b6f4cb78..6058b5d50e 100644 --- a/ena-submission/config/defaults.yaml +++ b/ena-submission/config/defaults.yaml @@ -2,3 +2,6 @@ username: external_metadata_updater password: external_metadata_updater keycloak_client_id: backend-client ingest_pipeline_submitter: insdc_ingest_user +github_username: fake_username +github_pat: fake_pat +github_url: https://api.github.com/repos/pathoplexus/ena-submission/contents/test/approved_ena_submission_list.json?ref=main diff --git a/ena-submission/flyway/sql/V1__Initial_Schema.sql b/ena-submission/flyway/sql/V1__Initial_Schema.sql index 9c77e8c62d..31b4f6fc19 100644 --- a/ena-submission/flyway/sql/V1__Initial_Schema.sql +++ b/ena-submission/flyway/sql/V1__Initial_Schema.sql @@ -2,26 +2,28 @@ CREATE TABLE submission_table ( accession text not null, version bigint not null, organism text not null, - groupId bigint not null, + group_id bigint not null, errors jsonb, warnings jsonb, status_all text not null, started_at timestamp not null, finished_at timestamp, + metadata jsonb, + aligned_nucleotide_sequences jsonb, external_metadata jsonb, primary key (accession, version) ); CREATE TABLE project_table ( - groupId bigint not null, + group_id bigint not null, organism text not null, errors jsonb, warnings jsonb, status text not null, started_at timestamp not null, finished_at timestamp, - project_metadata jsonb, - primary key (groupId, organism) + result jsonb, + primary key (group_id, organism) ); CREATE TABLE sample_table ( @@ -32,7 +34,7 @@ CREATE TABLE sample_table ( status text not null, started_at timestamp not null, finished_at timestamp, - sample_metadata jsonb, + result jsonb, primary key (accession, version) ); @@ -44,6 +46,6 @@ CREATE TABLE assembly_table ( status text not null, started_at timestamp not null, finished_at timestamp, - assembly_metadata jsonb, + result jsonb, primary key (accession, version) ); \ No newline at end of file diff --git a/ena-submission/scripts/__pycache__/call_loculus.cpython-312.pyc b/ena-submission/scripts/__pycache__/call_loculus.cpython-312.pyc deleted file mode 100644 index 4bf18a0da4..0000000000 Binary files a/ena-submission/scripts/__pycache__/call_loculus.cpython-312.pyc and /dev/null differ diff --git a/ena-submission/scripts/__pycache__/submission_db.cpython-312.pyc b/ena-submission/scripts/__pycache__/submission_db.cpython-312.pyc deleted file mode 100644 index ed7716da55..0000000000 Binary files a/ena-submission/scripts/__pycache__/submission_db.cpython-312.pyc and /dev/null differ diff --git a/ena-submission/scripts/get_ena_submission_list.py b/ena-submission/scripts/get_ena_submission_list.py index 3421bdd8fa..f2b3b9ea76 100644 --- a/ena-submission/scripts/get_ena_submission_list.py +++ b/ena-submission/scripts/get_ena_submission_list.py @@ -9,7 +9,7 @@ import yaml from call_loculus import get_released_data from notifications import get_slack_config, notify, upload_file_with_comment -from submission_db import get_db_config, in_submission_table +from submission_db_helper import get_db_config, in_submission_table logger = logging.getLogger(__name__) logging.basicConfig( diff --git a/ena-submission/scripts/submission_db.py b/ena-submission/scripts/submission_db.py deleted file mode 100644 index e01e1e9104..0000000000 --- a/ena-submission/scripts/submission_db.py +++ /dev/null @@ -1,84 +0,0 @@ -import os -from dataclasses import dataclass -from enum import Enum - -import psycopg2 - - -@dataclass -class DBConfig: - username: str - password: str - host: str - - -def get_db_config(db_password_default: str, db_username_default: str, db_host_default: str): - db_password = os.getenv("DB_PASSWORD") - if not db_password: - db_password = db_password_default - - db_username = os.getenv("DB_USERNAME") - if not db_username: - db_username = db_username_default - - db_host = os.getenv("DB_HOST") - if not db_host: - db_host = db_host_default - - db_params = { - "username": db_username, - "password": db_password, - "host": db_host, - } - - return DBConfig(**db_params) - - -class StatusAll(Enum): - READY_TO_SUBMIT = 0 - SUBMITTING_PROJECT = 1 - SUBMITTING_SAMPLE = 2 - SUBMITTING_ASSEMBLY = 3 - SUBMITTED_ALL = 4 - SENT_TO_LOCULUS = 5 - HAS_ERRORS_PROJECT = 6 - HAS_ERRORS_ASSEMBLY = 7 - HAS_ERRORS_SAMPLE = 8 - - -class Status(Enum): - READY = 0 - SUBMITTING = 1 - SUBMITTED = 2 - HAS_ERRORS = 3 - - -def connect_to_db(username="postgres", password="unsecure", host="127.0.0.1"): - """ - Establish connection to ena_submitter DB, if DB doesn't exist create it. - """ - try: - con = psycopg2.connect( - dbname="loculus", - user=username, - host=host, - password=password, - options="-c search_path=ena-submission", - ) - except ConnectionError as e: - raise ConnectionError("Could not create ena_submitter DB") from e - return con - - -def in_submission_table(accession: str, version: int, db_config: DBConfig) -> bool: - con = connect_to_db( - db_config.username, - db_config.password, - db_config.host, - ) - cur = con.cursor() - cur.execute( - "select * from submission_table where accession=%s and version=%s", - (f"{accession}", f"{version}"), - ) - return bool(cur.rowcount) diff --git a/ena-submission/scripts/submission_db_helper.py b/ena-submission/scripts/submission_db_helper.py new file mode 100644 index 0000000000..1007242cc2 --- /dev/null +++ b/ena-submission/scripts/submission_db_helper.py @@ -0,0 +1,130 @@ +import os +from dataclasses import dataclass +from datetime import datetime +from enum import Enum + +import psycopg2 +import pytz + + +@dataclass +class DBConfig: + username: str + password: str + host: str + + +def get_db_config(db_password_default: str, db_username_default: str, db_host_default: str): + db_password = os.getenv("DB_PASSWORD") + if not db_password: + db_password = db_password_default + + db_username = os.getenv("DB_USERNAME") + if not db_username: + db_username = db_username_default + + db_host = os.getenv("DB_HOST") + if not db_host: + db_host = db_host_default + + db_params = { + "username": db_username, + "password": db_password, + "host": db_host, + } + + return DBConfig(**db_params) + + +class StatusAll(Enum): + READY_TO_SUBMIT = 0 + SUBMITTING_PROJECT = 1 + SUBMITTING_SAMPLE = 2 + SUBMITTING_ASSEMBLY = 3 + SUBMITTED_ALL = 4 + SENT_TO_LOCULUS = 5 + HAS_ERRORS_PROJECT = 6 + HAS_ERRORS_ASSEMBLY = 7 + HAS_ERRORS_SAMPLE = 8 + + def __str__(self): + return self.name + + +class Status(Enum): + READY = 0 + SUBMITTING = 1 + SUBMITTED = 2 + HAS_ERRORS = 3 + + def __str__(self): + return self.name + + +@dataclass +class SubmissionTableEntry: + accession: str + version: str + organism: str + group_id: int + errors: str | None = None + warnings: str | None = None + status_all: StatusAll = StatusAll.READY_TO_SUBMIT + started_at: datetime | None = None + finished_at: datetime | None = None + metadata: str | None = None + aligned_nucleotide_sequences: str | None = None + external_metadata: str | None = None + + +def connect_to_db(db_config: DBConfig): + """ + Establish connection to ena_submitter DB, if DB doesn't exist create it. + """ + try: + con = psycopg2.connect( + dbname="loculus", + user=db_config.username, + host=db_config.host, + password=db_config.password, + options="-c search_path=ena-submission", + ) + except ConnectionError as e: + raise ConnectionError("Could not connect to loculus DB") from e + return con + + +def in_submission_table(accession: str, version: int, db_config: DBConfig) -> bool: + con = connect_to_db(db_config) + cur = con.cursor() + cur.execute( + "select * from submission_table where accession=%s and version=%s", + (f"{accession}", f"{version}"), + ) + return bool(cur.rowcount) + + +def add_to_submission_table(db_config: DBConfig, submission_table_entry: SubmissionTableEntry): + con = connect_to_db(db_config) + cur = con.cursor() + submission_table_entry.started_at = datetime.now(tz=pytz.utc) + + cur.execute( + "insert into submission_table values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + ( + submission_table_entry.accession, + submission_table_entry.version, + submission_table_entry.organism, + submission_table_entry.group_id, + submission_table_entry.errors, + submission_table_entry.warnings, + str(submission_table_entry.status_all), + submission_table_entry.started_at, + submission_table_entry.finished_at, + submission_table_entry.metadata, + submission_table_entry.aligned_nucleotide_sequences, + submission_table_entry.external_metadata, + ), + ) + con.commit() + con.close() diff --git a/ena-submission/scripts/trigger_submission_to_ena.py b/ena-submission/scripts/trigger_submission_to_ena.py new file mode 100644 index 0000000000..c17e4983a5 --- /dev/null +++ b/ena-submission/scripts/trigger_submission_to_ena.py @@ -0,0 +1,126 @@ +# This script adds all approved sequences to the submission_table +# - this should trigger the submission process. + +import base64 +import json +import logging +import os +from dataclasses import dataclass + +import click +import requests +import yaml +from requests.auth import HTTPBasicAuth +from submission_db_helper import ( + SubmissionTableEntry, + add_to_submission_table, + get_db_config, + in_submission_table, +) + +logger = logging.getLogger(__name__) +logging.basicConfig( + encoding="utf-8", + level=logging.INFO, + format="%(asctime)s %(levelname)8s (%(filename)20s:%(lineno)4d) - %(message)s ", + datefmt="%H:%M:%S", +) + + +@dataclass +class Config: + organisms: list[dict[str, str]] + organism: str + db_username: str + db_password: str + db_host: str + github_username: str + github_pat: str + github_url: str + + +@click.command() +@click.option( + "--log-level", + default="INFO", + type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]), +) +@click.option( + "--config-file", + required=True, + type=click.Path(exists=True), +) +@click.option( + "--input-file", + required=False, + type=click.Path(), +) +def trigger_submission_to_ena(log_level, config_file, input_file=None): + logger.setLevel(log_level) + logging.getLogger("requests").setLevel(logging.INFO) + + with open(config_file) as file: + full_config = yaml.safe_load(file) + relevant_config = {key: full_config.get(key, []) for key in Config.__annotations__} + config = Config(**relevant_config) + logger.info(f"Config: {config}") + + db_config = get_db_config(config.db_password, config.db_username, config.db_host) + + if input_file: + sequences_to_upload: dict = json.load(open(input_file, encoding="utf-8")) + for accession, data in sequences_to_upload.items(): + if in_submission_table(accession, data["metadata"]["version"], db_config): + continue + entry = { + "accession": accession, + "version": data["metadata"]["version"], + "group_id": data["metadata"]["groupId"], + "organism": data["organism"], + "metadata": json.dumps(data["metadata"]), + "aligned_nucleotide_sequences": json.dumps(data["alignedNucleotideSequences"]), + } + submission_table_entry = SubmissionTableEntry(**entry) + add_to_submission_table(db_config, submission_table_entry) + logger.info(f"Uploaded {accession} to submission_table") + return + + while True: + # In a loop get approved sequences uploaded to Github and upload to submission_table + github_username = os.getenv("GITHUB_USERNAME") + if not github_username: + github_username = config.github_username + + github_pat = os.getenv("GITHUB_PAT") + if not github_pat: + github_pat = config.github_pat + response = requests.get( + config.github_url, + auth=HTTPBasicAuth(github_username, github_pat), + timeout=10, + ) + + if response.status_code == 200: + file_info = response.json() + sequences_to_upload = json.loads(base64.b64decode(file_info["content"]).decode("utf-8")) + else: + error_msg = f"Failed to retrieve file: {response.status_code}" + raise Exception(error_msg) + for accession, data in sequences_to_upload.items(): + if in_submission_table(accession, data["metadata"]["version"], db_config): + continue + entry = { + "accession": accession, + "version": data["metadata"]["version"], + "group_id": data["metadata"]["groupId"], + "organism": data["organism"], + "metadata": json.dumps(data["metadata"]), + "aligned_nucleotide_sequences": json.dumps(data["alignedNucleotideSequences"]), + } + submission_table_entry = SubmissionTableEntry(**entry) + add_to_submission_table(db_config, submission_table_entry) + logger.info(f"Uploaded {accession} to submission_table") + + +if __name__ == "__main__": + trigger_submission_to_ena() diff --git a/kubernetes/loculus/templates/ena-submission-deployment.yaml b/kubernetes/loculus/templates/ena-submission-deployment.yaml index 999766b0d8..9a64cf9c35 100644 --- a/kubernetes/loculus/templates/ena-submission-deployment.yaml +++ b/kubernetes/loculus/templates/ena-submission-deployment.yaml @@ -93,9 +93,19 @@ spec: secretKeyRef: name: slack-notifications key: slack-channel-id + - name: GITHUB_USERNAME + valueFrom: + secretKeyRef: + name: github-approval-repo + key: github-username + - name: GITHUB_PAT + valueFrom: + secretKeyRef: + name: github-approval-repo + key: github-pat args: - snakemake - - get_ena_submission_list_and_sleep # Do not start submission process yet until on pods until better tested. + - results/triggered volumeMounts: - name: loculus-ena-submission-config-volume mountPath: /package/config/config.yaml @@ -104,4 +114,84 @@ spec: - name: loculus-ena-submission-config-volume configMap: name: loculus-ena-submission-config +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: loculus-get-ena-submission-list-cronjob +spec: + schedule: "0 0 * * *" # get submission list daily at midnight + startingDeadlineSeconds: 60 + concurrencyPolicy: Forbid + jobTemplate: + spec: + activeDeadlineSeconds: {{ $.Values.getSubmissionListLimitSeconds }} + template: + metadata: + labels: + app: loculus + component: loculus-get-ena-submission-list-cronjob + annotations: + argocd.argoproj.io/sync-options: Replace=true + reloader.stakater.com/auto: "true" + spec: + restartPolicy: Never + containers: + - name: ena-submission + image: "ghcr.io/loculus-project/ena-submission:{{ $dockerTag }}" + imagePullPolicy: Always + resources: + requests: + memory: "80Mi" + cpu: "10m" + limits: + memory: "10Gi" + env: + - name: EXTERNAL_METADATA_UPDATER_PASSWORD + valueFrom: + secretKeyRef: + name: service-accounts + key: dummyExternalMetadataUpdaterPassword + - name: DB_HOST + valueFrom: + secretKeyRef: + name: database + key: host + - name: DB_USERNAME + valueFrom: + secretKeyRef: + name: database + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: database + key: password + - name: SLACK_HOOK + valueFrom: + secretKeyRef: + name: slack-notifications + key: slack-hook + - name: SLACK_TOKEN + valueFrom: + secretKeyRef: + name: slack-notifications + key: slack-token + - name: SLACK_CHANNEL_ID + valueFrom: + secretKeyRef: + name: slack-notifications + key: slack-channel-id + args: + - snakemake + - get_ena_submission_list + - --all-temp # Reduce disk usage by not keeping files around + volumeMounts: + - name: loculus-ena-submission-config-volume + mountPath: /package/config/config.yaml + subPath: config.yaml + volumes: + - name: loculus-ena-submission-config-volume + configMap: + name: loculus-ena-submission-config {{- end }} diff --git a/kubernetes/loculus/values.yaml b/kubernetes/loculus/values.yaml index af6b0f73f6..0a4ef33bd5 100644 --- a/kubernetes/loculus/values.yaml +++ b/kubernetes/loculus/values.yaml @@ -1498,6 +1498,11 @@ secrets: slack-hook: "dummy" slack-token: "dummy" slack-channel-id: "dummy" + github-approval-repo: + type: raw + data: + github-username: "dummy" + github-pat: "dummy" enableCrossRefCredentials: true runDevelopmentKeycloakDatabase: true runDevelopmentMainDatabase: true diff --git a/kubernetes/loculus/values_preview_server.yaml b/kubernetes/loculus/values_preview_server.yaml index 6bac7aac8f..199ba7e271 100644 --- a/kubernetes/loculus/values_preview_server.yaml +++ b/kubernetes/loculus/values_preview_server.yaml @@ -27,4 +27,10 @@ secrets: slack-hook: AgCLEhTwqKL278AbNwpqdRqeg6naNrQJWx3q8Zp+ecXjMaaLLBi1C3uQlt0WKioy+pUAhfe7MowXKLM55hLyh/InZ9o3yLi9T/5cVRXcEXCvODWmbhr94XhcYI3KnVngZLcNl9Gr4LR+bz8A0sl/rCijNYrqYeDCLI6XUmB8mlKnHPqrF6CXC8Y5xyDbNYJONx6DAugq+gQcZYJ101vUOtu2LTD8awCsdF5FOzdcZ344Vxn/xwDlbbvUEKEQp5A5aMfx95zpa+rV/sQYHeCb7Dy1oWqpOrZP/rPJ4K9VGRx5QA9o1Qi0Pl3alRUqiPUR6pbMxbX8u0kCN6drFKxXDAMd+SadsppDGbNQNeQP5cphNJwYxL/0MIgXxJTrQpcynJK1FULX9W+1GtXg+tX4hRCtZL5hnCxPw12QcNOL2N8SJLGEe8gK8QtALpu/DH/trVJ3rMDRkZhhWCvtb9Zt9EuvUhxs07sE9DZ7rEAqzx51v4vr9CzmxkHEiAhrC3Se3CxnSspBP1/X9SvZ+GXn+ZuXzN+KivWCnim0RwhRD75Y7ZP8ct/iu3ilb6b7Pl+KOgOkA3In7c4yVAZwXMTuF6aP2/8inPx5Kk6p8ks7c5XeSIDFOH7C6EJuD7E69Fz6ijaF5bJN8NWBVxkE88xq4un5e7dcuqjIqaQ8kLDX1g8aXiklr6qD29q9H/m+gtd6lxcMb53bMl0EI+GHYTBZn3w+T1PxlY1uoBfNzt1efjXJD7AWlTDxze+5PIYgiFVAOdfv3ey5HJMMw1w5MTLMW44hkpt4MCaHvREBTXq5sxBJe10= slack-token: AgDFAP+F7ze+TY+yK71JPOSkKwIpnBXh7WWweyZZwYm1/CwmKS1iQ4O1p54sTrHrMC737Si27MfTVvrEZRj9aAwq0fQJZt5yldpfMaTccZJHj3rQ5kczyuCMYVcFzmyywAr4DXsUCscjrOLgwTiiK/d8jSFkKyXupC7bB2EcZZUFGpFAj7PiSeJuEIKQptkSjeaasCQXkdkuoKczM38GCu502pIxaJ9kIXVrereyKUpsU/uFDgj0IcKqfiT0M2FGs8Ujl3CXpMxcOLSuxVyCnje27GHpsYrd/uEKX3yl1rB1rV6Z+gMzlO9DDPW/XJl6TY5snOxdaCv7uNzAGwgb/rlaZ5fnrNqsOoucJvh35yxMcKDsx/hY72H7PRnzNpLeqZ/2zAub+fQP/o1edjxaYHaSltS0lwzCivIPOHv66/dDOD9v0LncWkCWGXXOp8Fsz9OrF+NcAZjIY/hDzwy+JRDA7Wtn7jlkA07WFpobkyyKfN+bNT1664wS2IMDRYA6+MbkA99v+ScVsEVlxJqn+PiaDtexQAfQcyN2NPbQe+9xMIQavvrcnwxdMwAP8DBME3vhdrD8yDRJ9GN+ygtZ3dB4FC4iW20ETyzlAqJ/H9M2/ed1O3VyraFDCV3PmSBdq7Rinj1Zg6D+IEp54HtwWiu5s7iNeKW37cSSloRUaojWQ1BFPB6msfP/O5yqREdGrWVhmChWvSDMw2LxmnZbCw3mVdMr9B1XeK76GHa4kOhVOcEqzl0X61NILYDecgLP6HVJZhB+NpHJfOY= slack-channel-id: AgDDxu0CGC/AFqOHMeHjV3KUGPoY0QAmGoqtiuxPPaP+GWOuz4xZbBP6Fymh0XbHFHMB9PtWowrMbvFOkfTncRiKqyK6HIU7GU2GtCla2WTZQNRAW82gcJnxtbtm4KymN3LyTj27qBHkQHNZ90qyBGdsJUYwmBW8XB6wNoz10KDyQSvYrYwQEe0onCgnislxslATPB6CQFWHMghKYoaHHAECpf6sN1kS9rvNq/1e7gG8s++lgF7qSZgjQP77Q6kMoiMS5krX03pPKZXsc69mI8GLIvhalsUfg2BO7swa3FgCbjecp32lW6KuRCfzeMmj2NWpWTLcJSPYPJN0sOkhRFOWUlrylztG82l7dGl5JofJWQVopF+qLTAR6LxHaujFHQ+Y2x4/5tBmurwOT6xXknQjXqYs2qbG1OriivJrjRwhRoZWE2vR5YlE+Zz8S9/vYw0JnKibnB1YvbdBBnpllyXYjTJa4818W05DvJ70qLeILMYcEkY4/jv5xqNdGuwp34gZLcW6+qztHfQVXRf0uXM1B7BPNH0aNMBNN7D0m1vWkTNgKC+V2PiEH9nTVhwSF+MlG/rmR2+v84kWhMP3qdX8/28GnBnvS9rryzuc3e2mHBIiIGHwW+SQjlmdq0jDTtuvFtU8I7ncB5PUe+sYZ0zFrn57blraBG5ntqtZfb+aS3modE+ElmCgBzi8gSQoVxXzmOIfMZRRwAaH0w== + github-approval-repo: + type: sealedsecret + clusterWide: "true" + data: + github-username: AgBtyXeimJmI7e1di9ubkzNODtAAlm0LiXEJJGfzzR/KExSrVMYsWcIIQOCfS/bbLvvvSyddKPGeFYhozG6CK943oTJ2rZHeMBjqtXX0AEw655KfJNgHWoWWk1xdmOzITtzKNlHi4cPcxswQte84NhJqRZPJ1sNhQfOm/AwL0NplQXD6xyl7UGAUKOW8rSLgH+gTUsTzmz2prgwiF3SmwwHCSClL/q9H4/nqkMycMQ5NcQK+5cpUUeWidzL3LjANCJS815S8oMdUWbDFwB3NPPfUhYdRuXM19MAHxq2hhAkn6rKGNI8tQhahi8dfmH2QDMyS1KVRof6taBXJIVMxeejL8nOJbNhPBdtyM+3hVasm/frLycUJxzvQpPPv1ictprM5K/r2sJDIqyXVOZSspyXQ09gFz2D13QsOSfaERbKUMtn9L0bfsG3N/zwo614N8YNCXf+dvIjVCoFwMD5RQ5IkqHLPEQiJ77++feSO+4fbfItjMI8qP4mM6YRI7VqweVKufwdSSropRpxhytnjqoWfvHFyDgzsw4ZuJwlyZs01yuJelog4yqG8WszWCv6Ae3fRM3gaquCj2UlFfxTBxN+aZId/lV3LCG1l06g82J6+YTL5wdCnYuYcpZmp21651J9jWwWzCumRWaQgbY6Ou0VjHKZ9if1uDdYA3S7SJJx/ZDZK4cixoS9VXgsPWFgmHbZNHqOFzOd2Dl9kRA== + github-pat: AgBa5wjYBw3GWXihWdoedx3oEbR5z808WoeIGyk5aAZPQ1MTMbWm4ZMdvJUZVPlkRacQXSt+A2pniYMjzl0bqHkc5guVQOoHu6jZgtyfaOyqT4Ergz1+L9VUzyn6E1NwqV2BtdKQTN7A1VvtLw0BLVG0E01SUcHeoU9MWlF+9BWFtXWWYxWL/sTLKoz0tnj8yu6LLHMMYeCnABAoZwZbJ3kf1UVp3FwcJbuSqsrUTPaeS6z5g90jL/zwJ150VUh9kInHs+kwV1imZp06ZUm3gpdY1f/EQZnxHTPUoaYLOo91cEhG7bSD9KQz1B6m07VzjIlnrwduPUCFQs1gk1hU7gO+6MYlMqCIb8NUQbuX5d5cHiENJm9DXAJdd8oNW02JQ371gulVYvXagAdrYgYv75Mr3YjejHZeinZrJ/ZxeZI0+fL4SLkh+77RyuN1R6ab1vGBoUG+LgHsUEBT7mw/wXtrzRmNBwxNBXSfTrrKM+EzM9KUsrSe490sK3Rf4TExjynL/9biS9ZFw601tdBh8luO3xFhCr8Bn4q7sWnKNmvSu+gMZklyhR65Hf0LHTxUdDcK8WZKFyds6foHTAv9kX4+NK7upVfAdjx9Tpf0ONad3nSboi/+1vYoGtAZcL6BZoNL+1Xu2WAlVa+WollJSV2k4KeTEswE1vmPAhoapyxpry9Mz+8CGkohvSPjHGdfXSNSW7K9zKCCubqzHV2+EsnaDWJ9+cwghAQFOpKdbJuHLwalMpAkTztRjr5wHfD6KSo1apCHZl84S0B2u8floEzU1XRiW8zq10m8cb3/xmdteNL3FNMlZlEieD2nsiU= reduceResourceRequest: true