Repository: causify-ai/helpers
Pull request #426: CmTask11401_Find_a_way_to_keep_GitHub_repos_in_sync
Author: login: heanhsok
Labels: name: PR for reviewers
description: The PR needs to be reviewed by RPs
Created: 2025-03-25T23:49:28Z | Merged: 2025-04-14T21:50:47Z

Task https://github.com/causify-ai/csfy/issues/5514

- Sync GH issues labels


Diff:
diff --git a/dev_scripts_helpers/github/labels/gh_issues_labels.yml b/dev_scripts_helpers/github/labels/gh_issues_labels.yml
new file mode 100644
index 000000000..79e90e42d
--- /dev/null
+++ b/dev_scripts_helpers/github/labels/gh_issues_labels.yml
@@ -0,0 +1,68 @@
+# Priority Labels.
+- name: P0
+  description: High priority - needs immediate attention
+  color: "#ff0000"
+- name: P1
+  description: Medium priority - address after P0 items
+  color: "#0052cc"
+- name: P2
+  description: Low priority - schedule as resources allow
+  color: "#2d3094"
+# Issue Types.
+- name: Blocking
+  description: This issue needs to be worked on immediately
+  color: "#e11d21"
+- name: Bug
+  description: Something isn't working
+  color: "#d73a4a"
+- name: Enhancement
+  description: New feature or request for improvement
+  color: "#a2eeef"
+- name: Epic
+  description: Large initiative containing multiple issues
+  color: "#465ff9"
+- name: Design
+  description: Technical design decisions and architecture
+  color: "#cc33cc"
+- name: Documentation
+  description: Improvements or additions to documentation
+  color: "#0075ca"
+- name: Cleanup
+  description: Code cleanup, refactoring, or technical debt
+  color: "#c5def5"
+- name: Customer
+  description: Customer related activities with billable hours
+  color: "#ff9001"
+- name: Readings
+  description: Reading a book, article and getting familiar with code
+  color: "#cd9be1"
+# Pull Request Guidelines.
+- name: PR for authors
+  description: The PR needs changes
+  color: "#eb35f3"
+- name: PR for integrators
+  description: The PR needs to be reviewed and possibly merged
+  color: "#fac906"
+- name: PR for reviewers
+  description: The PR needs to be reviewed by RPs
+  color: "#006b75"
+# Issue Status.
+- name: Paused
+  description: An issue was started and then stopped
+  color: "#cccccc"
+- name: To close
+  description: An issue can be potentially closed
+  color: "#e6e6e6"
+# Contribution Types.
+- name: Good first issue
+  description: Simple tasks for newcomers
+  color: "#7057ff"
+- name: Good second issue
+  description: Slightly more demanding tasks for newcomers
+  color: '#5F5187'
+- name: Outsource
+  description: Anybody can do it
+  color: "#919ebe"
+- name: Bounty
+  description: Help wanted
+  color: '#008672'
diff --git a/dev_scripts_helpers/github/sync_gh_issue_labels.py b/dev_scripts_helpers/github/sync_gh_issue_labels.py
new file mode 100755
index 000000000..51f8940f0
--- /dev/null
+++ b/dev_scripts_helpers/github/sync_gh_issue_labels.py
@@ -0,0 +1,263 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+from typing import Dict, List
+
+import yaml
+
+# TODO(gp): Use hdbg.WARNING
+_WARNING = "\033[33mWARNING\033[0m"
+
+# TODO(*): outsourceable. Convert to dockerized executable.
+try:
+    import github
+except ModuleNotFoundError:
+    _module = "pygithub"
+    print(_WARNING + f": Can't find {_module}: continuing")
+
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+# TODO(*): GSI. Move to `Label` class as static method.
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    with open(path, "r") as file:
+        yaml_data = yaml.safe_load(file)
+        labels = [
+            Label(
+                name=item["name"],
+                description=item["description"],
+                color=item["color"],
+            )
+            for item in yaml_data
+        ]
+        return labels
+
+
+# TODO(*): GSI. Move to `Label` class as static method.
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    with open(path, "w") as file:
+        labels_data = [
+            Label(
+                name=label.name,
+                description=label.description if label.description else None,
+                color=label.color,
+            ).to_dict()
+            for label in labels
+        ]
+        # Set `default_flow_style=False` to use block style instead of
+        # flow style for better readability.
+        yaml.dump(labels_data, file, default_flow_style=False, sort_keys=False)
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--input_file",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument("--repo", required=True, help="GitHub repository name")
+    parser.add_argument(
+        "--token_env_var",
+        required=True,
+        help="Name of the environment variable containing the GitHub token",
+    )
+    parser.add_argument(
+        "--prune",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        action="store_true",
+        help="Print out the actions that would be taken without executing them",
+    )
+    parser.add_argument(
+        "--backup",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    parser.add_argument(
+        "--no_interactive",
+        action="store_true",
+        help="Do not prompt for confirmation before executing actions",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.input_file)
+    labels_map = {label.name: label for label in labels}
+    token = os.environ[args.token_env_var]
+    hdbg.dassert(token)
+    # Initialize GH client.
+    client = github.Github(token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Get current labels from the repo.
+    current_labels = repo.get_labels()
+    current_labels_map = {label.name: label for label in current_labels}
+    # Execute code if not in dry run mode.
+    execute = not args.dry_run
+    # Save the labels if backup is enabled.
+    if args.backup:
+        git_root_dir = hgit.get_client_root(False)
+        file_name = f"tmp.labels.{args.owner}.{args.repo}.yaml"
+        file_path = f"{git_root_dir}/{file_name}"
+        _save_labels(current_labels, file_path)
+        _LOG.info("Labels backed up to %s", file_path)
+    else:
+        _LOG.warning("Skipping saving labels as per user request")
+    # Confirm label synchronization.
+    if not args.no_interactive:
+        hsystem.query_yes_no(
+            "Are you sure you want to synchronize labels?", abort_on_no=True
+        )
+    else:
+        _LOG.warning("Running in non-interactive mode, skipping confirmation")
+    # Delete labels if pruning is enabled.
+    if args.prune:
+        for current_label in current_labels:
+            if current_label.name not in labels_map:
+                if execute:
+                    current_label.delete()
+                    _LOG.info("Label '%s' deleted", current_label.name)
+                else:
+                    _LOG.info(
+                        "Label '%s' will be deleted without --dry_run",
+                        current_label.name,
+                    )
+    # Sync labels.
+    # Create or update labels.
+    for label in labels:
+        current_label = current_labels_map.get(label.name)
+        if current_label is None:
+            # Label doesn't exist, create it.
+            if execute:
+                repo.create_label(
+                    name=label.name,
+                    color=label.color,
+                    description=label.description,
+                )
+                _LOG.info("Label '%s' created", label.name)
+            else:
+                _LOG.info(
+                    "Label '%s' will be created without --dry_run", label.name
+                )
+        elif (
+            current_label.description != label.description
+            or current_label.color != label.color
+        ):
+            # Label exists but needs update.
+            if execute:
+                current_label.edit(
+                    name=label.name,
+                    color=label.color,
+                    description=label.description,
+                )
+                _LOG.info("Label '%s' updated", label.name)
+            else:
+                _LOG.warning(
+                    "Label '%s' will be updated without --dry_run",
+                    label.name,
+                )
+        else:
+            # Label exists and is identical.
+            _LOG.info("Label '%s' not changed", label.name)
+    _LOG.info("Label synchronization completed!")
+
+
+if __name__ == "__main__":
+    _main(_parse())


Reviews:
user: login: sonniki
body: All very nice! Just some style-related nitpicking from me. 
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T10:50:47Z
user: login: heanhsok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T15:49:47Z
user: login: heanhsok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T16:00:50Z
user: login: sonniki
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T16:03:49Z
user: login: sonniki
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T16:26:12Z
user: login: sonniki
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T09:21:39Z
user: login: sonniki
body: See the question above regarding my previous comments on the yaml file. Otherwise, LGTM. 
Do you want to file follow-ups for the shell script and the docs? Or feel free to do it in this PR, up to you.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T09:23:56Z
user: login: heanhsok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T14:47:11Z
user: login: sonniki
body: LGTM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T17:54:10Z
user: login: gpsaggese
body: Adding @Shaunak01 @Prahar08modi @tkpratardan since they are the new blood.

The code is perfect, but there are some interesting architectural and OOP rules.

I would commit as a checkpoint and then file more bugs to:
1) clean up the code
2) unit test it
3) describe the logic and the principles
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-30T13:37:08Z
user: login: heanhsok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T21:46:26Z
user: login: gpsaggese
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T21:50:20Z
user: login: gpsaggese
body: Minor nits and merge.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T00:05:31Z
user: login: heanhsok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T02:11:36Z
user: login: heanhsok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T02:15:16Z
user: login: heanhsok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T02:17:12Z
user: login: gpsaggese
body: The code is perfectly correct. I'm just nit picking on the architectural side and on the UI side. 

You can either fix these now or merge as it is and file an outsourceable issue to apply the following feedback.

Also we should find a way to document the style we use to write these tools so that everyone can follow it and we don't get everything working in a different way.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T17:46:13Z
user: login: gpsaggese
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-13T23:07:40Z
user: login: heanhsok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T21:40:34Z
user: login: heanhsok
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T21:43:33Z

Review comments:
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
+- name: P0
+  description: High priority - needs immediate attention
+  color: "#ff0000"
+- name: P1
+  description: Medium priority - address after P0 items
+  color: "#0052cc"
+- name: P2
+  description: Low priority - schedule as resources allow
+  color: "#2d3094"
+# Issue Types.
+- name: Blocking
+  description: This issue needs to be worked on immediately
+  color: "#e11d21"
+- name: Bug
+  description: Something isn't working
+  color: "#d73a4a"
+- name: Enhancement
+  description: New feature or request for improvement
+  color: "#a2eeef"
+- name: Epic
+  description: Large initiative containing multiple issues
+  color: "#465ff9"
+- name: Design
+  description: Technical design decisions and architecture
+  color: "#cc33cc"
+- name: Documentation
+  description: Improvements or additions to documentation
+  color: "#0075ca"
+- name: Cleanup
+  description: Code cleanup, refactoring, or technical debt
+  color: "#c5def5"
+- name: help wanted
+  description: Extra attention is needed
+  color: '#008672'
+# Pull Request Guidelines.
+- name: PR_for_authors
+  description: The PR needs changes
+  color: "#eb35f3"
+- name: PR_for_integrators
+  description: The PR needs to be reviewed and possibly merged
+  color: "#fac906"
+- name: PR_for_reviewers
+  description: The PR needs to be reviewed by RPs
+  color: "#006b75"
+# Issue Status.
+- name: Paused
+  description: An issue was started and then stopped
+  color: "#cccccc"
+- name: To close
+  description: An issue can be potentially closed
+  color: "#e6e6e6"
+# Contribution Types.
+- name: good first issue
+  description: Good for newcomers
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: sonniki
body: Can we change description to "Simple tasks for newcomers"? To make the difference between "good first issue" and "good second issue" more explicit
created_at: 2025-03-26T08:09:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
+- name: P0
+  description: High priority - needs immediate attention
+  color: "#ff0000"
+- name: P1
+  description: Medium priority - address after P0 items
+  color: "#0052cc"
+- name: P2
+  description: Low priority - schedule as resources allow
+  color: "#2d3094"
+# Issue Types.
+- name: Blocking
+  description: This issue needs to be worked on immediately
+  color: "#e11d21"
+- name: Bug
+  description: Something isn't working
+  color: "#d73a4a"
+- name: Enhancement
+  description: New feature or request for improvement
+  color: "#a2eeef"
+- name: Epic
+  description: Large initiative containing multiple issues
+  color: "#465ff9"
+- name: Design
+  description: Technical design decisions and architecture
+  color: "#cc33cc"
+- name: Documentation
+  description: Improvements or additions to documentation
+  color: "#0075ca"
+- name: Cleanup
+  description: Code cleanup, refactoring, or technical debt
+  color: "#c5def5"
+- name: help wanted
+  description: Extra attention is needed
+  color: '#008672'
+# Pull Request Guidelines.
+- name: PR_for_authors
+  description: The PR needs changes
+  color: "#eb35f3"
+- name: PR_for_integrators
+  description: The PR needs to be reviewed and possibly merged
+  color: "#fac906"
+- name: PR_for_reviewers
+  description: The PR needs to be reviewed by RPs
+  color: "#006b75"
+# Issue Status.
+- name: Paused
+  description: An issue was started and then stopped
+  color: "#cccccc"
+- name: To close
+  description: An issue can be potentially closed
+  color: "#e6e6e6"
+# Contribution Types.
+- name: good first issue
+  description: Good for newcomers
+  color: "#7057ff"
+- name: good second issue
+  description: Slightly more demanding tasks for newcomers
+  color: '#5F5187'
+- name: Outsource
+  description: Anybody can do it
+  color: "#919ebe"
+- name: Customer
+  description: Customer related activities with billable hours
+  color: "#ff9001"
+- name: Readings
+  description: Reading a book, article and getting familiar with code
+  color: "#cd9be1"
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: sonniki
body: Shouldn't these two be in "Issue Types"?
created_at: 2025-03-26T08:35:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
+- name: P0
+  description: High priority - needs immediate attention
+  color: "#ff0000"
+- name: P1
+  description: Medium priority - address after P0 items
+  color: "#0052cc"
+- name: P2
+  description: Low priority - schedule as resources allow
+  color: "#2d3094"
+# Issue Types.
+- name: Blocking
+  description: This issue needs to be worked on immediately
+  color: "#e11d21"
+- name: Bug
+  description: Something isn't working
+  color: "#d73a4a"
+- name: Enhancement
+  description: New feature or request for improvement
+  color: "#a2eeef"
+- name: Epic
+  description: Large initiative containing multiple issues
+  color: "#465ff9"
+- name: Design
+  description: Technical design decisions and architecture
+  color: "#cc33cc"
+- name: Documentation
+  description: Improvements or additions to documentation
+  color: "#0075ca"
+- name: Cleanup
+  description: Code cleanup, refactoring, or technical debt
+  color: "#c5def5"
+- name: help wanted
+  description: Extra attention is needed
+  color: '#008672'
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: sonniki
body: And conversely, this one might be better suited for "Contribution Types"?
created_at: 2025-03-26T08:35:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a YAML configuration file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: Label name
+        :param description: Label description
+        :param color: Label color in hex format
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: sonniki
body: Here and everywhere: as per our conventions, param descriptions should start with a lowercase letter (except when it's a name like "GitHub")
created_at: 2025-03-26T10:38:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a YAML configuration file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: Label name
+        :param description: Label description
+        :param color: Label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
+        """
+        Get all labels from a repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :return: List of Label objects
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: sonniki
body: No need to specify "List of" -- this is already clear from the typing hint
created_at: 2025-03-26T10:39:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a YAML configuration file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: Label name
+        :param description: Label description
+        :param color: Label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
+        """
+        Get all labels from a repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :return: List of Label objects
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        labels = []
+        page = 1
+        per_page = 50
+        while True:
+            response = requests.get(
+                url,
+                headers=self.headers,
+                params={"page": page, "per_page": per_page},
+            )
+            response.raise_for_status()
+            page_labels = response.json()
+            if not page_labels:
+                break
+            for label_data in page_labels:
+                labels.append(
+                    Label(
+                        name=label_data["name"],
+                        description=label_data.get("description", ""),
+                        color=label_data["color"],
+                    )
+                )
+            # Check if we've retrieved all pages.
+            if len(page_labels) < per_page:
+                break
+            page += 1
+        return labels
+
+    def create_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Create a new label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: Label object to create
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.post(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label created: %s", label)
+
+    def update_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Update an existing label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: Label object to update
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{label.name}"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.patch(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label updated: %s", label)
+
+    def delete_label(self, owner: str, repo: str, name: str) -> None:
+        """
+        Delete a label from the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param name: Label name to delete
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{name}"
+        response = requests.delete(url, headers=self.headers)
+        response.raise_for_status()
+        _LOG.info("Label deleted: %s", name)
+
+
+def load_manifest_to_labels(path: str) -> List[Label]:
+    """
+    Load label configurations from YAML file.
+
+    :param path: path to YAML file
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: sonniki
body: Instead of "YAML file" in all the function docstrings I would say "file with the label inventory". Just for the sake of focusing on the "semantics" of what we are referring to instead of the technical "implementation" details.
created_at: 2025-03-26T10:43:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,318 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a YAML configuration file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: Label name
+        :param description: Label description
+        :param color: Label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
+        """
+        Get all labels from a repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :return: List of Label objects
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        labels = []
+        page = 1
+        per_page = 50
+        while True:
+            response = requests.get(
+                url,
+                headers=self.headers,
+                params={"page": page, "per_page": per_page},
+            )
+            response.raise_for_status()
+            page_labels = response.json()
+            if not page_labels:
+                break
+            for label_data in page_labels:
+                labels.append(
+                    Label(
+                        name=label_data["name"],
+                        description=label_data.get("description", ""),
+                        color=label_data["color"],
+                    )
+                )
+            # Check if we've retrieved all pages.
+            if len(page_labels) < per_page:
+                break
+            page += 1
+        return labels
+
+    def create_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Create a new label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: Label object to create
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.post(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label created: %s", label)
+
+    def update_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Update an existing label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: Label object to update
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{label.name}"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.patch(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label updated: %s", label)
+
+    def delete_label(self, owner: str, repo: str, name: str) -> None:
+        """
+        Delete a label from the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param name: Label name to delete
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{name}"
+        response = requests.delete(url, headers=self.headers)
+        response.raise_for_status()
+        _LOG.info("Label deleted: %s", name)
+
+
+def load_manifest_to_labels(path: str) -> List[Label]:
+    """
+    Load label configurations from YAML file.
+
+    :param path: path to YAML file
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading YAML file: %s", str(e))
+        sys.exit(1)
+
+
+def save_labels_to_manifest(labels: List[Label], path: str) -> None:
+    """
+    Save label configurations to YAML file.
+
+    :param labels: List of Label objects
+    :param path: path to save the YAML file to
+    """
+    try:
+        with open(path, "w") as file:
+            yaml_data = [
+                {
+                    "name": label.name,
+                    "description": label.description,
+                    "color": f"#{label.color}",
+                }
+                for label in labels
+            ]
+            yaml.dump(yaml_data, file, default_flow_style=False, sort_keys=False)
+    except Exception as e:
+        _LOG.error("Error saving YAML file: %s", str(e))
+        sys.exit(1)
+
+
+def sync_labels(
+    client: GitHubClient, owner: str, repo: str, labels: List[Label], prune: bool
+) -> None:
+    """
+    Synchronize labels between config and repository.
+    """
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: sonniki
body: Missing param descriptions
created_at: 2025-03-26T10:44:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: sonniki
body: Does this mean that if one creates/updates/deletes a label now, they should also go to this file and update it? And then run the script to sync? Should we put this in the docs?
created_at: 2025-03-26T10:50:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: heanhsok
body: Yes we need to document this process as well.

What I'm not sure yet are
- once the file is updated, who should do it and to which repos (since not everyone can see and has the permission to all the repos)?
- should we prune existing labels (there's an optional flag to do that) that are not in our manifest file?
- this python script is designed to run on one repo, I'm thinking of creating a shell script that would list all the repo (e.g. `gh repo list causify-ai`) and then execute this python script for each of the repo? (but this would also require the account that is used to execute the script to be able to see and update those settings)

WDYT?
created_at: 2025-03-26T15:49:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: heanhsok
body: > this python script is designed to run on one repo, I'm thinking of creating a shell script that would list all the repo (e.g. gh repo list causify-ai) and then execute this python script for each of the repo? (but this would also require the account that is used to execute the script to be able to see and update those settings)

Maybe we can also have a GH pipeline that detect changes in this file when label file is merged to master and then execute the changes?

This way, we are still relying on our script to perform the change and use GH Action as a way to detect the changes and execute our script, or even does regular maintenance through cronjob. 
created_at: 2025-03-26T16:00:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: sonniki
body: > once the file is updated, who should do it and to which repos (since not everyone can see and has the permission to all the repos)?
> this python script is designed to run on one repo, I'm thinking of creating a shell script that would list all the repo (e.g. gh repo list causify-ai) and then execute this python script for each of the repo? (but this would also require the account that is used to execute the script to be able to see and update those settings)

These questions are related IMO. I like the proposed approach of a shell script that would span all repos. I agree that this means that we have to have a user with a super access. @gpsaggese is that possible?

> should we prune existing labels (there's an optional flag to do that) that are not in our manifest file?

I would keep pruning "off" by default. We might have a situation when a user adds a label in a repo but forgets to update the manifest file, and we don't want to then delete this label once somebody runs the sync script. We could throw a logging message that there is label X in repo Y that is not reflected in the label inventory file.
created_at: 2025-03-26T16:03:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: sonniki
body: > Maybe we can also have a GH pipeline that detect changes in this file when label file is merged to master and then execute the changes?

I see the advantage of the user then only needing to update the yaml file when they update the labels. However, with running the script yourself there is more control, one can see logging messages, etc. So I would lean towards keeping the script in the hands of a human. 
created_at: 2025-03-26T16:26:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
+- name: P0
+  description: High priority - needs immediate attention
+  color: "#ff0000"
+- name: P1
+  description: Medium priority - address after P0 items
+  color: "#0052cc"
+- name: P2
+  description: Low priority - schedule as resources allow
+  color: "#2d3094"
+# Issue Types.
+- name: Blocking
+  description: This issue needs to be worked on immediately
+  color: "#e11d21"
+- name: Bug
+  description: Something isn't working
+  color: "#d73a4a"
+- name: Enhancement
+  description: New feature or request for improvement
+  color: "#a2eeef"
+- name: Epic
+  description: Large initiative containing multiple issues
+  color: "#465ff9"
+- name: Design
+  description: Technical design decisions and architecture
+  color: "#cc33cc"
+- name: Documentation
+  description: Improvements or additions to documentation
+  color: "#0075ca"
+- name: Cleanup
+  description: Code cleanup, refactoring, or technical debt
+  color: "#c5def5"
+- name: help wanted
+  description: Extra attention is needed
+  color: '#008672'
+# Pull Request Guidelines.
+- name: PR_for_authors
+  description: The PR needs changes
+  color: "#eb35f3"
+- name: PR_for_integrators
+  description: The PR needs to be reviewed and possibly merged
+  color: "#fac906"
+- name: PR_for_reviewers
+  description: The PR needs to be reviewed by RPs
+  color: "#006b75"
+# Issue Status.
+- name: Paused
+  description: An issue was started and then stopped
+  color: "#cccccc"
+- name: To close
+  description: An issue can be potentially closed
+  color: "#e6e6e6"
+# Contribution Types.
+- name: good first issue
+  description: Good for newcomers
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: sonniki
body: You don't agree with the suggestions for the yaml file? I see you've resolved the conversations but made no updates. It's okay, I just wanted to double check nothing slipped through the cracks
created_at: 2025-03-27T09:21:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
+- name: P0
+  description: High priority - needs immediate attention
+  color: "#ff0000"
+- name: P1
+  description: Medium priority - address after P0 items
+  color: "#0052cc"
+- name: P2
+  description: Low priority - schedule as resources allow
+  color: "#2d3094"
+# Issue Types.
+- name: Blocking
+  description: This issue needs to be worked on immediately
+  color: "#e11d21"
+- name: Bug
+  description: Something isn't working
+  color: "#d73a4a"
+- name: Enhancement
+  description: New feature or request for improvement
+  color: "#a2eeef"
+- name: Epic
+  description: Large initiative containing multiple issues
+  color: "#465ff9"
+- name: Design
+  description: Technical design decisions and architecture
+  color: "#cc33cc"
+- name: Documentation
+  description: Improvements or additions to documentation
+  color: "#0075ca"
+- name: Cleanup
+  description: Code cleanup, refactoring, or technical debt
+  color: "#c5def5"
+- name: help wanted
+  description: Extra attention is needed
+  color: '#008672'
+# Pull Request Guidelines.
+- name: PR_for_authors
+  description: The PR needs changes
+  color: "#eb35f3"
+- name: PR_for_integrators
+  description: The PR needs to be reviewed and possibly merged
+  color: "#fac906"
+- name: PR_for_reviewers
+  description: The PR needs to be reviewed by RPs
+  color: "#006b75"
+# Issue Status.
+- name: Paused
+  description: An issue was started and then stopped
+  color: "#cccccc"
+- name: To close
+  description: An issue can be potentially closed
+  color: "#e6e6e6"
+# Contribution Types.
+- name: good first issue
+  description: Good for newcomers
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: heanhsok
body: Oops sorry I didn't notice that the yaml file was not committed. It should be updated now. thanks for pointing it out.
created_at: 2025-03-27T14:47:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: -> label 

symmetry is > than English :-)
created_at: 2025-03-30T13:21:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: -> `# comment`
created_at: 2025-03-30T13:21:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: Let's move this to helpers/hgithub.py

@tkpratardan is also working on related things so you might want to coordinate
created_at: 2025-03-30T13:22:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: We could move owner and repo to the ctor and say "this is a class to operate on a repo", which is the typical use case.

Another approach is to derive a class `LabelProcessor` from `GitHubClient` or better use GitHubClient as a passed element. Composition is always > inheritance
created_at: 2025-03-30T13:24:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
+        """
+        Get all labels from a repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :return: label objects
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        labels = []
+        page = 1
+        per_page = 50
+        while True:
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: More comments explaining the chunks of work pls!

Also more _LOG.debug. The idea is that adding -v DEBUG helps with meaningful logging
created_at: 2025-03-30T13:25:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
+        """
+        Get all labels from a repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :return: label objects
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        labels = []
+        page = 1
+        per_page = 50
+        while True:
+            response = requests.get(
+                url,
+                headers=self.headers,
+                params={"page": page, "per_page": per_page},
+            )
+            response.raise_for_status()
+            page_labels = response.json()
+            if not page_labels:
+                break
+            for label_data in page_labels:
+                labels.append(
+                    Label(
+                        name=label_data["name"],
+                        description=label_data.get("description", ""),
+                        color=label_data["color"],
+                    )
+                )
+            # Check if we've retrieved all pages.
+            if len(page_labels) < per_page:
+                break
+            page += 1
+        return labels
+
+    def create_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Create a new label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to create
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.post(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label created: %s", label)
+
+    def update_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Update an existing label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to update
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{label.name}"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.patch(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label updated: %s", label)
+
+    def delete_label(self, owner: str, repo: str, name: str) -> None:
+        """
+        Delete a label from the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param name: label name to delete
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{name}"
+        response = requests.delete(url, headers=self.headers)
+        response.raise_for_status()
+        _LOG.info("Label deleted: %s", name)
+
+
+def load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        sys.exit(1)
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: I'd just rethrow here. Exiting is too C-language ;-)
created_at: 2025-03-30T13:27:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
+        """
+        Get all labels from a repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :return: label objects
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        labels = []
+        page = 1
+        per_page = 50
+        while True:
+            response = requests.get(
+                url,
+                headers=self.headers,
+                params={"page": page, "per_page": per_page},
+            )
+            response.raise_for_status()
+            page_labels = response.json()
+            if not page_labels:
+                break
+            for label_data in page_labels:
+                labels.append(
+                    Label(
+                        name=label_data["name"],
+                        description=label_data.get("description", ""),
+                        color=label_data["color"],
+                    )
+                )
+            # Check if we've retrieved all pages.
+            if len(page_labels) < per_page:
+                break
+            page += 1
+        return labels
+
+    def create_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Create a new label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to create
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.post(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label created: %s", label)
+
+    def update_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Update an existing label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to update
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{label.name}"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.patch(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label updated: %s", label)
+
+    def delete_label(self, owner: str, repo: str, name: str) -> None:
+        """
+        Delete a label from the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param name: label name to delete
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{name}"
+        response = requests.delete(url, headers=self.headers)
+        response.raise_for_status()
+        _LOG.info("Label deleted: %s", name)
+
+
+def load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        sys.exit(1)
+
+
+def save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            yaml_data = [
+                {
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: Arch nit: this could be a `to_dict()` method, since it's general enough and avoids you to reach into the object (which is always a violation of encapsulation). In fact, I personally tend to make all variables private (like in C++)
created_at: 2025-03-30T13:29:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
+        """
+        Get all labels from a repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :return: label objects
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        labels = []
+        page = 1
+        per_page = 50
+        while True:
+            response = requests.get(
+                url,
+                headers=self.headers,
+                params={"page": page, "per_page": per_page},
+            )
+            response.raise_for_status()
+            page_labels = response.json()
+            if not page_labels:
+                break
+            for label_data in page_labels:
+                labels.append(
+                    Label(
+                        name=label_data["name"],
+                        description=label_data.get("description", ""),
+                        color=label_data["color"],
+                    )
+                )
+            # Check if we've retrieved all pages.
+            if len(page_labels) < per_page:
+                break
+            page += 1
+        return labels
+
+    def create_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Create a new label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to create
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.post(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label created: %s", label)
+
+    def update_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Update an existing label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to update
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{label.name}"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.patch(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label updated: %s", label)
+
+    def delete_label(self, owner: str, repo: str, name: str) -> None:
+        """
+        Delete a label from the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param name: label name to delete
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{name}"
+        response = requests.delete(url, headers=self.headers)
+        response.raise_for_status()
+        _LOG.info("Label deleted: %s", name)
+
+
+def load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        sys.exit(1)
+
+
+def save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            yaml_data = [
+                {
+                    "name": label.name,
+                    "description": label.description,
+                    "color": f"#{label.color}",
+                }
+                for label in labels
+            ]
+            yaml.dump(yaml_data, file, default_flow_style=False, sort_keys=False)
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: We should comment the magic values, e.g., `default_flow_style`. Why are we setting to false?
created_at: 2025-03-30T13:30:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
+        """
+        Get all labels from a repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :return: label objects
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        labels = []
+        page = 1
+        per_page = 50
+        while True:
+            response = requests.get(
+                url,
+                headers=self.headers,
+                params={"page": page, "per_page": per_page},
+            )
+            response.raise_for_status()
+            page_labels = response.json()
+            if not page_labels:
+                break
+            for label_data in page_labels:
+                labels.append(
+                    Label(
+                        name=label_data["name"],
+                        description=label_data.get("description", ""),
+                        color=label_data["color"],
+                    )
+                )
+            # Check if we've retrieved all pages.
+            if len(page_labels) < per_page:
+                break
+            page += 1
+        return labels
+
+    def create_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Create a new label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to create
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.post(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label created: %s", label)
+
+    def update_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Update an existing label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to update
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{label.name}"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.patch(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label updated: %s", label)
+
+    def delete_label(self, owner: str, repo: str, name: str) -> None:
+        """
+        Delete a label from the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param name: label name to delete
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{name}"
+        response = requests.delete(url, headers=self.headers)
+        response.raise_for_status()
+        _LOG.info("Label deleted: %s", name)
+
+
+def load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        sys.exit(1)
+
+
+def save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            yaml_data = [
+                {
+                    "name": label.name,
+                    "description": label.description,
+                    "color": f"#{label.color}",
+                }
+                for label in labels
+            ]
+            yaml.dump(yaml_data, file, default_flow_style=False, sort_keys=False)
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        sys.exit(1)
+
+
+def sync_labels(
+    client: GitHubClient, owner: str, repo: str, labels: List[Label], prune: bool
+) -> None:
+    """
+    Synchronize labels between config and repository.
+
+    :param client: GitHub client
+    :param owner: GitHub repository owner/organization
+    :param repo: GitHub repository name
+    :param labels: label objects
+    :param prune: delete labels that exist in the repo but not in the
+        label inventory manifest file
+    """
+    # Build maps for efficient lookup.
+    label_map = {label.name: label for label in labels}
+    current_labels = client.get_labels(owner, repo)
+    current_label_map = {label.name: label for label in current_labels}
+    # Delete labels if pruning is enabled.
+    if prune:
+        for current_label in current_labels:
+            if current_label.name not in label_map:
+                try:
+                    client.delete_label(owner, repo, current_label.name)
+                except Exception as e:
+                    _LOG.error(
+                        "Error deleting label %s: %s", current_label.name, e
+                    )
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: My (or ours) style is for objects to throw exceptions (typically dasserts) that have meaningful messages.
In this case client.delete_label does the work and if it can't, dasserts and the whole thing stops.

Catching and rethrowing is useful only if you can actually do something (rarely possible!) or you can add more context to the error.
In this case neither is possible so we should not catch and rethrow.

created_at: 2025-03-30T13:33:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
+
+    def __init__(self, token: str):
+        """
+        Initialize the client with a personal access token.
+
+        :param token: GitHub personal access token
+        """
+        self.token = token
+        self.headers = {
+            "Authorization": f"token {token}",
+            "Accept": "application/vnd.github.v3+json",
+        }
+
+    def get_labels(self, owner: str, repo: str) -> List[Label]:
+        """
+        Get all labels from a repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :return: label objects
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        labels = []
+        page = 1
+        per_page = 50
+        while True:
+            response = requests.get(
+                url,
+                headers=self.headers,
+                params={"page": page, "per_page": per_page},
+            )
+            response.raise_for_status()
+            page_labels = response.json()
+            if not page_labels:
+                break
+            for label_data in page_labels:
+                labels.append(
+                    Label(
+                        name=label_data["name"],
+                        description=label_data.get("description", ""),
+                        color=label_data["color"],
+                    )
+                )
+            # Check if we've retrieved all pages.
+            if len(page_labels) < per_page:
+                break
+            page += 1
+        return labels
+
+    def create_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Create a new label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to create
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.post(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label created: %s", label)
+
+    def update_label(self, owner: str, repo: str, label: Label) -> None:
+        """
+        Update an existing label in the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param label: label object to update
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{label.name}"
+        data = {
+            "name": label.name,
+            "description": label.description,
+            "color": label.color,
+        }
+        response = requests.patch(url, headers=self.headers, json=data)
+        response.raise_for_status()
+        _LOG.info("Label updated: %s", label)
+
+    def delete_label(self, owner: str, repo: str, name: str) -> None:
+        """
+        Delete a label from the repository.
+
+        :param owner: GitHub repository owner/organization
+        :param repo: GitHub repository name
+        :param name: label name to delete
+        """
+        url = f"https://api.github.com/repos/{owner}/{repo}/labels/{name}"
+        response = requests.delete(url, headers=self.headers)
+        response.raise_for_status()
+        _LOG.info("Label deleted: %s", name)
+
+
+def load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        sys.exit(1)
+
+
+def save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            yaml_data = [
+                {
+                    "name": label.name,
+                    "description": label.description,
+                    "color": f"#{label.color}",
+                }
+                for label in labels
+            ]
+            yaml.dump(yaml_data, file, default_flow_style=False, sort_keys=False)
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        sys.exit(1)
+
+
+def sync_labels(
+    client: GitHubClient, owner: str, repo: str, labels: List[Label], prune: bool
+) -> None:
+    """
+    Synchronize labels between config and repository.
+
+    :param client: GitHub client
+    :param owner: GitHub repository owner/organization
+    :param repo: GitHub repository name
+    :param labels: label objects
+    :param prune: delete labels that exist in the repo but not in the
+        label inventory manifest file
+    """
+    # Build maps for efficient lookup.
+    label_map = {label.name: label for label in labels}
+    current_labels = client.get_labels(owner, repo)
+    current_label_map = {label.name: label for label in current_labels}
+    # Delete labels if pruning is enabled.
+    if prune:
+        for current_label in current_labels:
+            if current_label.name not in label_map:
+                try:
+                    client.delete_label(owner, repo, current_label.name)
+                except Exception as e:
+                    _LOG.error(
+                        "Error deleting label %s: %s", current_label.name, e
+                    )
+    # Create or update labels.
+    for label in labels:
+        current_label = current_label_map.get(label.name)
+        try:
+            if current_label is None:
+                # Label doesn't exist, create it.
+                client.create_label(owner, repo, label)
+            elif (
+                current_label.description != label.description
+                or current_label.color != label.color
+            ):
+                # Label exists but needs update.
+                client.update_label(owner, repo, label)
+            else:
+                # Label exists and is identical.
+                _LOG.info("Label not changed: %s", label)
+        except Exception as e:
+            _LOG.error("Error processing label %s: %s", label.name, e)
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: Same thing here. This is actually making the error worse, since it's better to get info about the failing point.

Long story short, we use try-except very rarely since it just makes the code more complicated.
created_at: 2025-03-30T13:35:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: heanhsok
body: As I'm refactoring this into its own class, I notice that we're basically just re-inventing the wheel by rewriting what is already implemented in this package https://github.com/PyGithub/PyGithub (which is also already in our //helpers image). What if we scrape this entire class and just use the library? WDYT? @gpsaggese @sonniki 
created_at: 2025-04-01T21:46:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,326 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import sys
+from typing import List
+
+import requests
+import yaml
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self.name = name
+        self.description = description
+        # Remove # prefix if present.
+        self.color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"Label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+
+# #############################################################################
+# GitHubClient
+# #############################################################################
+
+
+class GitHubClient:
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: Using the lib is always the way to go. Feel free to scrape the old code then.
created_at: 2025-04-01T21:50:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: let's call it input_file or label_file.

We are mixing what it is with how it's represented here
created_at: 2025-04-09T23:58:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: The problem with this approach is that the token stays in the bash history.
It's better to pass the name of an env var (e.g., --token GITHUB_TOKEN and then one does os.environ["GITHUB_TOKEN"])
created_at: 2025-04-09T23:59:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import subprocess
+import sys
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: Perfect candidate for Dockerized executable, no?
We can do it now or file a bug and have somebody else (an intern?) clean it up
created_at: 2025-04-10T00:00:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import subprocess
+import sys
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [label.to_dict() for label in labels]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--yaml",
+        "-y",
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: let's avoid the short form since they make the command incomprehensible...
created_at: 2025-04-10T00:01:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import subprocess
+import sys
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [label.to_dict() for label in labels]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--yaml",
+        "-y",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        "-o",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument(
+        "--repo", "-r", required=True, help="GitHub repository name"
+    )
+    parser.add_argument(
+        "--token", "-t", required=True, help="GitHub personal access token"
+    )
+    parser.add_argument(
+        "--prune",
+        "-p",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        "-d",
+        action="store_true",
+        help="Print out the labels and exits immediately",
+    )
+    parser.add_argument(
+        "--backup",
+        "-b",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.yaml)
+    labels_map = {label.name: label for label in labels}
+    # Initialize GH client.
+    client = github.Github(args.token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Build maps for efficient lookup.
+    current_labels = repo.get_labels()
+    current_labels_data = []
+    for label in current_labels:
+        current_labels_data.append(
+            Label(
+                name=label.name,
+                description=label.description if label.description else None,
+                color=label.color,
+            )
+        )
+    current_labels_map = {label.name: label for label in current_labels_data}
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: I would just do it here in a dir `tmp.backup_sync_gh_issue_labels.yaml`?

We use `tmp` for everything that doesn't need to be checked in
created_at: 2025-04-10T00:03:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import subprocess
+import sys
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [label.to_dict() for label in labels]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--yaml",
+        "-y",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        "-o",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument(
+        "--repo", "-r", required=True, help="GitHub repository name"
+    )
+    parser.add_argument(
+        "--token", "-t", required=True, help="GitHub personal access token"
+    )
+    parser.add_argument(
+        "--prune",
+        "-p",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        "-d",
+        action="store_true",
+        help="Print out the labels and exits immediately",
+    )
+    parser.add_argument(
+        "--backup",
+        "-b",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.yaml)
+    labels_map = {label.name: label for label in labels}
+    # Initialize GH client.
+    client = github.Github(args.token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Build maps for efficient lookup.
+    current_labels = repo.get_labels()
+    current_labels_data = []
+    for label in current_labels:
+        current_labels_data.append(
+            Label(
+                name=label.name,
+                description=label.description if label.description else None,
+                color=label.color,
+            )
+        )
+    current_labels_map = {label.name: label for label in current_labels_data}
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
+        file_name = f"labels.{args.owner}.{args.repo}.yaml"
+        file_path = f"{dst_dir}/{file_name}"
+        _save_labels(current_labels_data, file_path)
+        _LOG.info("Labels backed up to %s", file_path)
+    # Show the labels and exit if dry run is enabled.
+    if args.dry_run:
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: This seems more of a "--print_labels".

I would imagine that `--dry_run` shows me what the script is going to do without doing it...
created_at: 2025-04-10T00:04:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import subprocess
+import sys
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [label.to_dict() for label in labels]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--yaml",
+        "-y",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        "-o",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument(
+        "--repo", "-r", required=True, help="GitHub repository name"
+    )
+    parser.add_argument(
+        "--token", "-t", required=True, help="GitHub personal access token"
+    )
+    parser.add_argument(
+        "--prune",
+        "-p",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        "-d",
+        action="store_true",
+        help="Print out the labels and exits immediately",
+    )
+    parser.add_argument(
+        "--backup",
+        "-b",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.yaml)
+    labels_map = {label.name: label for label in labels}
+    # Initialize GH client.
+    client = github.Github(args.token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Build maps for efficient lookup.
+    current_labels = repo.get_labels()
+    current_labels_data = []
+    for label in current_labels:
+        current_labels_data.append(
+            Label(
+                name=label.name,
+                description=label.description if label.description else None,
+                color=label.color,
+            )
+        )
+    current_labels_map = {label.name: label for label in current_labels_data}
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
+        file_name = f"labels.{args.owner}.{args.repo}.yaml"
+        file_path = f"{dst_dir}/{file_name}"
+        _save_labels(current_labels_data, file_path)
+        _LOG.info("Labels backed up to %s", file_path)
+    # Show the labels and exit if dry run is enabled.
+    if args.dry_run:
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: heanhsok
body: Changed the `dry_run` logic to print out the actions that would be taken without executing them
created_at: 2025-04-11T02:11:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import subprocess
+import sys
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: heanhsok
body: We thought about wrapping this with one of the invoke command also. Not sure if we can combine the two approach?
created_at: 2025-04-11T02:15:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import subprocess
+import sys
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: heanhsok
body: Also we are not yet able to run tests of dockerized executable in the CI yet right?
created_at: 2025-04-11T02:17:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import subprocess
+import sys
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: 1) Yes, I have a PR that will fix this.
You can file a follow up bug and add a TODO: Outsourceable

2) The import approach is problematic since if anyone import this (including pytest) stuff gets installed.

I would add an if False around that and ask people to install it manually before running
created_at: 2025-04-13T17:23:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: FYI there is a school of thought that says that these should be static methods of Label, like `load_from_file` and `save_to_file`, since are part of the interface.

The design heuristics is that they are "verbs" applied to labels.

I personally like that approach, since it makes things tighter (e.g., everything is in a namespace).

It can also be a TODO: GSI for an intern

created_at: 2025-04-13T17:28:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: I'm not super fond of this approach of try ... except

My reasoning is that everything can throw exceptions so what are we really achieving? I use try ... except when I expected something could go wrong and I want to handle it to help the caller, since I know the inside of the code and thus I can make a better decision.
Here the only info is that we print an error. But is it worth it the visual noise?
My sensibility says it's now worth it.
created_at: 2025-04-13T17:32:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [
+                Label(
+                    name=label.name,
+                    description=label.description if label.description else None,
+                    color=label.color,
+                ).to_dict()
+                for label in labels
+            ]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--input_file",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument("--repo", required=True, help="GitHub repository name")
+    parser.add_argument(
+        "--token_env_var",
+        required=True,
+        help="Name of the environment variable containing the GitHub token",
+    )
+    parser.add_argument(
+        "--prune",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        action="store_true",
+        help="Print out the actions that would be taken without executing them",
+    )
+    parser.add_argument(
+        "--backup",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.input_file)
+    labels_map = {label.name: label for label in labels}
+    token = os.environ.get(args.token_env_var, None)
+    hdbg.dassert(token)
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: This is equivalent to `os.environ[args.token_env_var]`
created_at: 2025-04-13T17:34:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [
+                Label(
+                    name=label.name,
+                    description=label.description if label.description else None,
+                    color=label.color,
+                ).to_dict()
+                for label in labels
+            ]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--input_file",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument("--repo", required=True, help="GitHub repository name")
+    parser.add_argument(
+        "--token_env_var",
+        required=True,
+        help="Name of the environment variable containing the GitHub token",
+    )
+    parser.add_argument(
+        "--prune",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        action="store_true",
+        help="Print out the actions that would be taken without executing them",
+    )
+    parser.add_argument(
+        "--backup",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.input_file)
+    labels_map = {label.name: label for label in labels}
+    token = os.environ.get(args.token_env_var, None)
+    hdbg.dassert(token)
+    # Initialize GH client.
+    client = github.Github(token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Get current labels from the repo.
+    current_labels = repo.get_labels()
+    current_labels_map = {label.name: label for label in current_labels}
+    # Execute code if not in dry run mode.
+    execute = not args.dry_run
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: I would just leave it in `.`. This is a temp file so we want to make the user's life easier, instead of having him search / use a complex path
created_at: 2025-04-13T17:36:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [
+                Label(
+                    name=label.name,
+                    description=label.description if label.description else None,
+                    color=label.color,
+                ).to_dict()
+                for label in labels
+            ]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--input_file",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument("--repo", required=True, help="GitHub repository name")
+    parser.add_argument(
+        "--token_env_var",
+        required=True,
+        help="Name of the environment variable containing the GitHub token",
+    )
+    parser.add_argument(
+        "--prune",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        action="store_true",
+        help="Print out the actions that would be taken without executing them",
+    )
+    parser.add_argument(
+        "--backup",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.input_file)
+    labels_map = {label.name: label for label in labels}
+    token = os.environ.get(args.token_env_var, None)
+    hdbg.dassert(token)
+    # Initialize GH client.
+    client = github.Github(token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Get current labels from the repo.
+    current_labels = repo.get_labels()
+    current_labels_map = {label.name: label for label in current_labels}
+    # Execute code if not in dry run mode.
+    execute = not args.dry_run
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
+        file_name = f"tmp.labels.{args.owner}.{args.repo}.yaml"
+        file_path = f"{dst_dir}/{file_name}"
+        if execute:
+            _save_labels(current_labels, file_path)
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: else:
   _LOG.warning("Skipping saving labels as per user request")

My rule is when we are doing something "weird" we want to tell the user, so s/he knows what's happen
created_at: 2025-04-13T17:36:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [
+                Label(
+                    name=label.name,
+                    description=label.description if label.description else None,
+                    color=label.color,
+                ).to_dict()
+                for label in labels
+            ]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--input_file",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument("--repo", required=True, help="GitHub repository name")
+    parser.add_argument(
+        "--token_env_var",
+        required=True,
+        help="Name of the environment variable containing the GitHub token",
+    )
+    parser.add_argument(
+        "--prune",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        action="store_true",
+        help="Print out the actions that would be taken without executing them",
+    )
+    parser.add_argument(
+        "--backup",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.input_file)
+    labels_map = {label.name: label for label in labels}
+    token = os.environ.get(args.token_env_var, None)
+    hdbg.dassert(token)
+    # Initialize GH client.
+    client = github.Github(token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Get current labels from the repo.
+    current_labels = repo.get_labels()
+    current_labels_map = {label.name: label for label in current_labels}
+    # Execute code if not in dry run mode.
+    execute = not args.dry_run
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
+        file_name = f"tmp.labels.{args.owner}.{args.repo}.yaml"
+        file_path = f"{dst_dir}/{file_name}"
+        if execute:
+            _save_labels(current_labels, file_path)
+        _LOG.info("Labels backed up to %s", file_path)
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: This should go in the if-then
created_at: 2025-04-13T17:37:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [
+                Label(
+                    name=label.name,
+                    description=label.description if label.description else None,
+                    color=label.color,
+                ).to_dict()
+                for label in labels
+            ]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--input_file",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument("--repo", required=True, help="GitHub repository name")
+    parser.add_argument(
+        "--token_env_var",
+        required=True,
+        help="Name of the environment variable containing the GitHub token",
+    )
+    parser.add_argument(
+        "--prune",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        action="store_true",
+        help="Print out the actions that would be taken without executing them",
+    )
+    parser.add_argument(
+        "--backup",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.input_file)
+    labels_map = {label.name: label for label in labels}
+    token = os.environ.get(args.token_env_var, None)
+    hdbg.dassert(token)
+    # Initialize GH client.
+    client = github.Github(token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Get current labels from the repo.
+    current_labels = repo.get_labels()
+    current_labels_map = {label.name: label for label in current_labels}
+    # Execute code if not in dry run mode.
+    execute = not args.dry_run
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
+        file_name = f"tmp.labels.{args.owner}.{args.repo}.yaml"
+        file_path = f"{dst_dir}/{file_name}"
+        if execute:
+            _save_labels(current_labels, file_path)
+        _LOG.info("Labels backed up to %s", file_path)
+    # Confirm label synchronization.
+    hsystem.query_yes_no(
+        "Are you sure you want to synchronize labels?", abort_on_no=True
+    )
+    # Delete labels if pruning is enabled.
+    if args.prune:
+        for current_label in current_labels:
+            if current_label.name not in labels_map:
+                if execute:
+                    current_label.delete()
+                _LOG.info("Label deleted: %s", current_label.name)
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: Should go in the if-then and you can do

else:
   _LOG.info("The label %s will be deleted without --dry-run"


created_at: 2025-04-13T17:38:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [
+                Label(
+                    name=label.name,
+                    description=label.description if label.description else None,
+                    color=label.color,
+                ).to_dict()
+                for label in labels
+            ]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--input_file",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument("--repo", required=True, help="GitHub repository name")
+    parser.add_argument(
+        "--token_env_var",
+        required=True,
+        help="Name of the environment variable containing the GitHub token",
+    )
+    parser.add_argument(
+        "--prune",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        action="store_true",
+        help="Print out the actions that would be taken without executing them",
+    )
+    parser.add_argument(
+        "--backup",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.input_file)
+    labels_map = {label.name: label for label in labels}
+    token = os.environ.get(args.token_env_var, None)
+    hdbg.dassert(token)
+    # Initialize GH client.
+    client = github.Github(token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Get current labels from the repo.
+    current_labels = repo.get_labels()
+    current_labels_map = {label.name: label for label in current_labels}
+    # Execute code if not in dry run mode.
+    execute = not args.dry_run
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
+        file_name = f"tmp.labels.{args.owner}.{args.repo}.yaml"
+        file_path = f"{dst_dir}/{file_name}"
+        if execute:
+            _save_labels(current_labels, file_path)
+        _LOG.info("Labels backed up to %s", file_path)
+    # Confirm label synchronization.
+    hsystem.query_yes_no(
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: Personally I like to add also a `--force` or `--no-interactive` flag to avoid to be requested for confirmation. The problem is that users don't want always to lock the program.
created_at: 2025-04-13T17:40:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [
+                Label(
+                    name=label.name,
+                    description=label.description if label.description else None,
+                    color=label.color,
+                ).to_dict()
+                for label in labels
+            ]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--input_file",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument("--repo", required=True, help="GitHub repository name")
+    parser.add_argument(
+        "--token_env_var",
+        required=True,
+        help="Name of the environment variable containing the GitHub token",
+    )
+    parser.add_argument(
+        "--prune",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        action="store_true",
+        help="Print out the actions that would be taken without executing them",
+    )
+    parser.add_argument(
+        "--backup",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.input_file)
+    labels_map = {label.name: label for label in labels}
+    token = os.environ.get(args.token_env_var, None)
+    hdbg.dassert(token)
+    # Initialize GH client.
+    client = github.Github(token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Get current labels from the repo.
+    current_labels = repo.get_labels()
+    current_labels_map = {label.name: label for label in current_labels}
+    # Execute code if not in dry run mode.
+    execute = not args.dry_run
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
+        file_name = f"tmp.labels.{args.owner}.{args.repo}.yaml"
+        file_path = f"{dst_dir}/{file_name}"
+        if execute:
+            _save_labels(current_labels, file_path)
+        _LOG.info("Labels backed up to %s", file_path)
+    # Confirm label synchronization.
+    hsystem.query_yes_no(
+        "Are you sure you want to synchronize labels?", abort_on_no=True
+    )
+    # Delete labels if pruning is enabled.
+    if args.prune:
+        for current_label in current_labels:
+            if current_label.name not in labels_map:
+                if execute:
+                    current_label.delete()
+                _LOG.info("Label deleted: %s", current_label.name)
+    # Sync labels.
+    # Create or update labels.
+    for label in labels:
+        current_label = current_labels_map.get(label.name)
+        if current_label is None:
+            # Label doesn't exist, create it.
+            if execute:
+                repo.create_label(
+                    name=label.name,
+                    color=label.color,
+                    description=label.description,
+                )
+            _LOG.info("Label created: %s", label.name)
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: gpsaggese
body: For me these are all _LOG.debug

Unix philosophy https://en.wikipedia.org/wiki/Unix_philosophy recommends "avoid unnecessary output"

My approach is I print very little at INFO level, if a user wants to see what happens can use a switch to print stats or run at DEBUG leve.

In this case I can be ok with being more chatty.
The way I would use these tools is like:
- run with dry-run to see what will happen
- check if what happens is what I want
- then run in one-shot and just know if things were fine or not

Does it make sense?
created_at: 2025-04-13T17:44:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
+- name: P0
+  description: High priority - needs immediate attention
+  color: "#ff0000"
+- name: P1
+  description: Medium priority - address after P0 items
+  color: "#0052cc"
+- name: P2
+  description: Low priority - schedule as resources allow
+  color: "#2d3094"
+# Issue Types.
+- name: Blocking
+  description: This issue needs to be worked on immediately
+  color: "#e11d21"
+- name: Bug
+  description: Something isn't working
+  color: "#d73a4a"
+- name: Enhancement
+  description: New feature or request for improvement
+  color: "#a2eeef"
+- name: Epic
+  description: Large initiative containing multiple issues
+  color: "#465ff9"
+- name: Design
+  description: Technical design decisions and architecture
+  color: "#cc33cc"
+- name: Documentation
+  description: Improvements or additions to documentation
+  color: "#0075ca"
+- name: Cleanup
+  description: Code cleanup, refactoring, or technical debt
+  color: "#c5def5"
+- name: Customer
+  description: Customer related activities with billable hours
+  color: "#ff9001"
+- name: Readings
+  description: Reading a book, article and getting familiar with code
+  color: "#cd9be1"
+# Pull Request Guidelines.
+- name: PR_for_authors
+  description: The PR needs changes
+  color: "#eb35f3"
+- name: PR_for_integrators
+  description: The PR needs to be reviewed and possibly merged
+  color: "#fac906"
+- name: PR_for_reviewers
+  description: The PR needs to be reviewed by RPs
+  color: "#006b75"
+# Issue Status.
+- name: Paused
+  description: An issue was started and then stopped
+  color: "#cccccc"
+- name: To close
+  description: An issue can be potentially closed
+  color: "#e6e6e6"
+# Contribution Types.
+- name: good first issue
+  description: Simple tasks for newcomers
+  color: "#7057ff"
+- name: good second issue
+  description: Slightly more demanding tasks for newcomers
+  color: '#5F5187'
+- name: Outsource
+  description: Anybody can do it
+  color: "#919ebe"
+- name: help wanted
+  description: Extra attention is needed
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: gpsaggese
body: Instead of "help wanted"

Let's add
- Bounty
created_at: 2025-04-13T23:06:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+# Priority Labels.
+- name: P0
+  description: High priority - needs immediate attention
+  color: "#ff0000"
+- name: P1
+  description: Medium priority - address after P0 items
+  color: "#0052cc"
+- name: P2
+  description: Low priority - schedule as resources allow
+  color: "#2d3094"
+# Issue Types.
+- name: Blocking
+  description: This issue needs to be worked on immediately
+  color: "#e11d21"
+- name: Bug
+  description: Something isn't working
+  color: "#d73a4a"
+- name: Enhancement
+  description: New feature or request for improvement
+  color: "#a2eeef"
+- name: Epic
+  description: Large initiative containing multiple issues
+  color: "#465ff9"
+- name: Design
+  description: Technical design decisions and architecture
+  color: "#cc33cc"
+- name: Documentation
+  description: Improvements or additions to documentation
+  color: "#0075ca"
+- name: Cleanup
+  description: Code cleanup, refactoring, or technical debt
+  color: "#c5def5"
+- name: Customer
+  description: Customer related activities with billable hours
+  color: "#ff9001"
+- name: Readings
+  description: Reading a book, article and getting familiar with code
+  color: "#cd9be1"
+# Pull Request Guidelines.
+- name: PR_for_authors
+  description: The PR needs changes
+  color: "#eb35f3"
+- name: PR_for_integrators
+  description: The PR needs to be reviewed and possibly merged
+  color: "#fac906"
+- name: PR_for_reviewers
+  description: The PR needs to be reviewed by RPs
+  color: "#006b75"
+# Issue Status.
+- name: Paused
+  description: An issue was started and then stopped
+  color: "#cccccc"
+- name: To close
+  description: An issue can be potentially closed
+  color: "#e6e6e6"
+# Contribution Types.
+- name: good first issue
path: dev_scripts_helpers/github/labels/gh_issues_labels.yml
user: login: gpsaggese
body: Let's make them all the same style (e.g., no _ but space, etc)
-> Good first issue
created_at: 2025-04-13T23:07:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,244 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> sync_gh_issue_labels.py \
+    --yaml ./labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token *** \
+    --backup
+"""
+
+import argparse
+import logging
+import subprocess
+import sys
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: heanhsok
body: Added TODO in the code 
created_at: 2025-04-14T21:40:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,245 @@
+#!/usr/bin/env python3
+"""
+Synchronize GitHub issue labels from a label inventory manifest file.
+
+Examples
+
+# Basic usage:
+> ./dev_scripts_helpers/github/sync_gh_issue_labels.py \
+    --input_file ./dev_scripts_helpers/github/labels/gh_issues_labels.yml \
+    --owner causify-ai \
+    --repo tutorials \
+    --token_env_var GITHUB_TOKEN \
+    --dry_run \
+    --backup
+"""
+
+import argparse
+import logging
+import os
+import subprocess
+from typing import Dict, List
+
+import yaml
+
+try:
+    import github
+except ModuleNotFoundError:
+    subprocess.call(["sudo", "/venv/bin/pip", "install", "pygithub"])
+    import github
+
+import helpers.hdbg as hdbg
+import helpers.hgit as hgit
+import helpers.hparser as hparser
+import helpers.hsystem as hsystem
+
+_LOG = logging.getLogger(__name__)
+
+
+# #############################################################################
+# Label
+# #############################################################################
+
+
+class Label:
+
+    def __init__(self, name: str, description: str, color: str):
+        """
+        Initialize the label with name, description, and color.
+
+        :param name: label name
+        :param description: label description
+        :param color: label color in hex format
+        """
+        self._name = name
+        self._description = description
+        # Remove '#' prefix from hex code if present.
+        self._color = color.lstrip("#")
+
+    def __repr__(self):
+        return f"label(name='{self.name}', description='{self.description}', color='{self.color}')"
+
+    @property
+    def name(self) -> str:
+        return self._name
+
+    @property
+    def description(self) -> str:
+        return self._description
+
+    @property
+    def color(self) -> str:
+        return self._color
+
+    def to_dict(self) -> Dict[str, str]:
+        """
+        Return label as a dictionary.
+
+        :return: label as a dictionary
+        """
+        return {
+            "name": self._name,
+            "description": self._description,
+            "color": self._color,
+        }
+
+
+def _load_labels(path: str) -> List[Label]:
+    """
+    Load labels from label inventory manifest file.
+
+    :param path: path to label inventory manifest file
+    :return: label objects
+    """
+    try:
+        with open(path, "r") as file:
+            yaml_data = yaml.safe_load(file)
+            labels = [
+                Label(
+                    name=item["name"],
+                    description=item["description"],
+                    color=item["color"],
+                )
+                for item in yaml_data
+            ]
+            return labels
+    except Exception as e:
+        _LOG.error("Error loading label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _save_labels(labels: List[Label], path: str) -> None:
+    """
+    Save labels to the label inventory manifest file.
+
+    :param labels: label objects
+    :param path: path to save the label inventory manifest file to
+    """
+    try:
+        with open(path, "w") as file:
+            labels_data = [
+                Label(
+                    name=label.name,
+                    description=label.description if label.description else None,
+                    color=label.color,
+                ).to_dict()
+                for label in labels
+            ]
+            # Set `default_flow_style=False` to use block style instead of
+            # flow style for better readability.
+            yaml.dump(
+                labels_data, file, default_flow_style=False, sort_keys=False
+            )
+    except Exception as e:
+        _LOG.error("Error saving label inventory manifest file: %s", str(e))
+        raise e
+
+
+def _parse() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
+    )
+    hparser.add_verbosity_arg(parser)
+    parser.add_argument(
+        "--input_file",
+        required=True,
+        help="Path to label inventory manifest file",
+    )
+    parser.add_argument(
+        "--owner",
+        required=True,
+        help="GitHub repository owner/organization",
+    )
+    parser.add_argument("--repo", required=True, help="GitHub repository name")
+    parser.add_argument(
+        "--token_env_var",
+        required=True,
+        help="Name of the environment variable containing the GitHub token",
+    )
+    parser.add_argument(
+        "--prune",
+        action="store_true",
+        help="Delete labels that exist in the repo but not in the label inventory manifest file",
+    )
+    parser.add_argument(
+        "--dry_run",
+        action="store_true",
+        help="Print out the actions that would be taken without executing them",
+    )
+    parser.add_argument(
+        "--backup",
+        action="store_true",
+        help="Backup current labels to a label inventory manifest file",
+    )
+    return parser
+
+
+def _main(parser: argparse.ArgumentParser) -> None:
+    args = parser.parse_args()
+    hdbg.init_logger(verbosity=args.log_level, use_exec_path=True)
+    # Load labels from label inventory manifest file.
+    labels = _load_labels(args.input_file)
+    labels_map = {label.name: label for label in labels}
+    token = os.environ.get(args.token_env_var, None)
+    hdbg.dassert(token)
+    # Initialize GH client.
+    client = github.Github(token)
+    repo = client.get_repo(f"{args.owner}/{args.repo}")
+    # Get current labels from the repo.
+    current_labels = repo.get_labels()
+    current_labels_map = {label.name: label for label in current_labels}
+    # Execute code if not in dry run mode.
+    execute = not args.dry_run
+    # Save the labels if backup is enabled.
+    if args.backup:
+        root_dir = hgit.get_client_root(False)
+        dst_dir = f"{root_dir}/dev_scripts_helpers/github/labels/backup"
+        file_name = f"tmp.labels.{args.owner}.{args.repo}.yaml"
+        file_path = f"{dst_dir}/{file_name}"
+        if execute:
+            _save_labels(current_labels, file_path)
+        _LOG.info("Labels backed up to %s", file_path)
+    # Confirm label synchronization.
+    hsystem.query_yes_no(
+        "Are you sure you want to synchronize labels?", abort_on_no=True
+    )
+    # Delete labels if pruning is enabled.
+    if args.prune:
+        for current_label in current_labels:
+            if current_label.name not in labels_map:
+                if execute:
+                    current_label.delete()
+                _LOG.info("Label deleted: %s", current_label.name)
+    # Sync labels.
+    # Create or update labels.
+    for label in labels:
+        current_label = current_labels_map.get(label.name)
+        if current_label is None:
+            # Label doesn't exist, create it.
+            if execute:
+                repo.create_label(
+                    name=label.name,
+                    color=label.color,
+                    description=label.description,
+                )
+            _LOG.info("Label created: %s", label.name)
path: dev_scripts_helpers/github/sync_gh_issue_labels.py
user: login: heanhsok
body: Got it. The logging here is more like outputs for users to view the actions that have been taken. Thus, I'm leaving it as _LOG.info as you suggested.
created_at: 2025-04-14T21:43:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: heanhsok
created_at: 2025-03-26T00:53:05Z
body: ### Example usage:
```
❯ sync_gh_issue_labels.py \
        --yaml ./labels/gh_issues_labels.yml \
        --owner causify-ai \
        --repo sports_analytics \
        --token *******  \
        --backup
00:46:21 - INFO  hdbg.py init_logger:1013                               Saving log to file '{'/Users/heanh/src/helpers1/dev_scripts_helpers/github/sync_gh_issue_labels.py.log'}'
00:46:21 - INFO  hdbg.py init_logger:1018                               > cmd='/Users/heanh/src/helpers1/dev_scripts_helpers/github/sync_gh_issue_labels.py --yaml ./labels/gh_issues_labels.yml --owner causify-ai --repo sports_analytics --token ******* --backup'
00:46:21 - INFO  sync_gh_issue_labels.py _main:306                      Labels backed up to /Users/heanh/src/helpers1/dev_scripts_helpers/github/labels/backup/labels.causify-ai.sports_analytics.yaml
Are you sure you want to synchronize labels? [y/n] y
00:46:23 - INFO  sync_gh_issue_labels.py sync_labels:240                Label not changed: Label(name='P0', description='High priority - needs immediate attention', color='ff0000')
00:46:23 - INFO  sync_gh_issue_labels.py sync_labels:240                Label not changed: Label(name='P1', description='Medium priority - address after P0 items', color='0052cc')
00:46:23 - INFO  sync_gh_issue_labels.py sync_labels:240                Label not changed: Label(name='P2', description='Low 
....
00:46:24 - INFO  sync_gh_issue_labels.py create_label:127               Label created: Label(name='good second issue', description='Slightly more demanding tasks for newcomers', color='5F5187')
00:46:24 - INFO  sync_gh_issue_labels.py sync_labels:240                Label not changed: Label(name='Outsource', description='Anybody can do it', color='919ebe')
00:46:24 - INFO  sync_gh_issue_labels.py sync_labels:240                Label not changed: Label(name='Customer', description='Customer related activities with billable hours', color='ff9001')
00:46:24 - INFO  sync_gh_issue_labels.py sync_labels:240                Label not changed: Label(name='Readings', description='Reading a book, article and getting familiar with code', color='cd9be1')
00:46:24 - INFO  sync_gh_issue_labels.py _main:312                      Label synchronization completed!
```
author_association: CONTRIBUTOR
user: login: heanhsok
created_at: 2025-03-27T15:15:47Z
body: > Do you want to file follow-ups for the shell script and the docs? Or feel free to do it in this PR, up to you.

Filed the followings as follow-ups so it's easier to review and discuss
- https://github.com/causify-ai/helpers/issues/438
- https://github.com/causify-ai/helpers/issues/439
author_association: CONTRIBUTOR
user: login: heanhsok
created_at: 2025-04-09T15:48:06Z
body: @gpsaggese @sonniki 
Wondering if there's anything left to address in this PR, otherwise should we merge this?
author_association: CONTRIBUTOR
user: login: gpsaggese
created_at: 2025-04-10T00:06:04Z
body: @sonniki I'll add a GSI for factoring a bit this code and adding tests.
author_association: CONTRIBUTOR
user: login: heanhsok
created_at: 2025-04-14T21:46:24Z
body: Merging as checkpoints

I'll file the follow-up GSI and outsourceable issue and reference the code in the master branch
author_association: CONTRIBUTOR