codecov[bot]: # [Codecov](https://codecov.io/gh/slarse/labelbot/pull/26?src=pr&el=h1) Report
> Merging [#26](https://codecov.io/gh/slarse/labelbot/pull/26?src=pr&el=desc) into [master](https://codecov.io/gh/slarse/labelbot/commit/f41ded54f0647bc4dacfd0c92bb829f958c21bba?src=pr&el=desc) will **decrease** coverage by `0.58%`.
> The diff coverage is `20.68%`.

[![Impacted file tree graph](https://codecov.io/gh/slarse/labelbot/pull/26/graphs/tree.svg?width=650&token=vwkCZPYZ7N&height=150&src=pr)](https://codecov.io/gh/slarse/labelbot/pull/26?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #26      +/-   ##
==========================================
- Coverage   65.93%   65.34%   -0.59%     
==========================================
  Files           4        4              
  Lines          91      101      +10     
  Branches        2        3       +1     
==========================================
+ Hits           60       66       +6     
- Misses         31       35       +4
```


| [Impacted Files](https://codecov.io/gh/slarse/labelbot/pull/26?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [labelbot/bot.py](https://codecov.io/gh/slarse/labelbot/pull/26/diff?src=pr&el=tree#diff-bGFiZWxib3QvYm90LnB5) | `0% <0%> (ø)` | :arrow_up: |
| [labelbot/github\_api.py](https://codecov.io/gh/slarse/labelbot/pull/26/diff?src=pr&el=tree#diff-bGFiZWxib3QvZ2l0aHViX2FwaS5weQ==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/slarse/labelbot/pull/26?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/slarse/labelbot/pull/26?src=pr&el=footer). Last update [f41ded5...0443d8a](https://codecov.io/gh/slarse/labelbot/pull/26?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/labelbot/bot.py b/labelbot/bot.py
index b9cc5e3..34c6916 100644
--- a/labelbot/bot.py
+++ b/labelbot/bot.py
@@ -4,39 +4,39 @@ import python_jwt
 import os
 import boto3
 import botocore
-import labelbot.auth as auth
-import labelbot.github_api as github_api
-
-
-
-
-def get_pem(bucket_name, key):
-    """Reads key from s3"""
-    s3 = boto3.resource('s3')
-    s3.Bucket(bucket_name).download_file(key, '/tmp/key.pem')
-    with open('/tmp/key.pem', "rb") as f:
-        pem = f.read()
-    return pem
+from labelbot import auth
+from labelbot import github_api
+from labelbot import parse
 
 
 def lambda_handler(event, context):
-    # TODO implement
-    
     body = json.loads(event["body"])
-    installation_id = body['installation']['id']
-    owner = body['repository']['owner']['login']
-    repo = body['repository']['name']
-    number = body['issue']['number']
-    issue_url = body['issue']['url']
-    issue_body = body['issue']['body']
+    installation_id = body["installation"]["id"]
+    owner = body["repository"]["owner"]["login"]
+    repo = body["repository"]["name"]
+    issue_nr = body["issue"]["number"]
+    issue_body = body["issue"]["body"]
+    current_labels = [label["name"] for label in body["issue"]["labels"]]
 
-    app_id = int(os.environ['APP_ID'])
-    bucket_name = os.environ['BUCKET_NAME']
-    bucket_key = os.environ['BUCKET_KEY']
-    pem = files.get_pem(bucket_name, bucket_key)
+    app_id = int(os.environ["APP_ID"])
+    bucket_name = os.environ["BUCKET_NAME"]
+    bucket_key = os.environ["BUCKET_KEY"]
+    pem = get_pem(bucket_name, bucket_key)
 
+    jwt_token = auth.generate_jwt_token(pem, app_id)
+    access_token = auth.generate_installation_access_token(jwt_token, installation_id)
 
+    success = github_api.set_allowed_labels(
+        owner, repo, issue_nr, issue_body, current_labels, access_token
+    )
 
-    return {"statusCode": 200, "body": json.dumps("temp")}
+    return {"statusCode": 200 if success else 403, "body": json.dumps("temp")}
 
 
+def get_pem(bucket_name, key):
+    """Reads key from s3"""
+    s3 = boto3.resource("s3")
+    s3.Bucket(bucket_name).download_file(key, "/tmp/key.pem")
+    with open("/tmp/key.pem", "rb") as f:
+        pem = f.read()
+    return pem
diff --git a/labelbot/github_api.py b/labelbot/github_api.py
index dff6bc4..d1a052b 100644
--- a/labelbot/github_api.py
+++ b/labelbot/github_api.py
@@ -9,6 +9,8 @@ import base64
 from typing import Sequence, List
 import requests
 
+from labelbot import parse
+
 BASE_URL = "https://api.github.com"
 ALLOWED_LABELS_FILE = ".allowed-labels"
 
@@ -31,6 +33,32 @@ def _allowed_labels_url(owner, repo):
     return f"{BASE_URL}/repos/{owner}/{repo}/contents/{ALLOWED_LABELS_FILE}"
 
 
+def set_allowed_labels(
+    owner: str,
+    repo: str,
+    issue_nr: int,
+    issue_body: str,
+    current_labels: List[str],
+    access_token: str,
+) -> bool:
+    """Set the current labels plus any requested labels in the issue body that
+    are also allowed by the .allowed-labels file.
+
+    Args:
+        owner: User/Organization that owns the repo.
+        repo: Name of the repo.
+        issue_nr: Number of the issue.
+        access_token: An installation access token for the repo.
+
+    """
+    allowed_labels = parse.parse_allowed_labels(
+        get_file_contents(owner, repo, ALLOWED_LABELS_FILE, access_token)
+    )
+    wanted_labels = parse.parse_wanted_labels(issue_body)
+    labels_to_set = set(current_labels) | (set(allowed_labels) & set(wanted_labels))
+    return set_labels(labels_to_set, owner, repo, issue_nr, access_token)
+
+
 def set_labels(
     labels: Sequence[str], owner: str, repo: str, issue_nr: int, access_token: str
 ) -> bool:

Function for labeling an issue with allowed labels
It should do everything (delegating to other functions for subtasks).

* Fetch the `.allowed-labels` file and parse the allowed labels #12
    - Abort if there is no `.allowed-labels` file
    - Maybe set an `allowed-labels-missing` label on the issue or something?
* Fetch the issue body and parse the requested labels #15 
* Based on the allowed labels, determine a set `new` of labels that should be set
    - Maybe set a `bad-label-request` label on an issue if one or more requested labels are not allowed?
* Fetch the set of labels `current` that are already on the issue.
* Set the labels `current + new` on the issue.
diff --git a/tests/test_github_api.py b/tests/test_github_api.py
index 4b09282..8ae56c5 100644
--- a/tests/test_github_api.py
+++ b/tests/test_github_api.py
@@ -1,10 +1,12 @@
 import json
+import contextlib
+from unittest.mock import patch
+
+import responses
 import pytest
 
 from labelbot import github_api
 
-import responses
-
 OWNER = "someone"
 REPO = "best-repo"
 ISSUE_NR = 231
@@ -114,6 +116,49 @@ class TestGetFileContents:
         assert f"could not fetch {github_api.ALLOWED_LABELS_FILE}" in str(exc_info)
 
 
+class TestSetAllowedLabels:
+    """Tests for set_allowed_labels."""
+
+    @contextlib.contextmanager
+    def _mocked_apis(self, allowed_labels, wanted_labels, set_labels_result):
+        """All apis mocked out returning the specified values. Yields the
+        set_labels mock.
+        """
+        with patch(
+            "labelbot.github_api.parse.parse_allowed_labels",
+            autospec=True,
+            return_value=allowed_labels,
+        ), patch(
+            "labelbot.github_api.parse.parse_wanted_labels",
+            autospec=True,
+            return_value=wanted_labels,
+        ), patch(
+            "labelbot.github_api.get_file_contents", autospec=True, return_value=""
+        ):
+            with patch(
+                "labelbot.github_api.set_labels", autospec=True, return_value=True
+            ) as set_labels:
+                yield set_labels
+
+    def test_happy_path(self):
+        current_labels = ["enhancement"]
+        allowed_labels = ["bug", "feature request"]
+        wanted_labels = ["feature request", "help", "feature"]
+        expected_labels = {"enhancement", "feature request"}
+
+        with self._mocked_apis(
+            allowed_labels, wanted_labels, set_labels_result=True
+        ) as set_labels:
+            res = github_api.set_allowed_labels(
+                OWNER, REPO, ISSUE_NR, "", current_labels, ACCESS_TOKEN
+            )
+
+        assert res
+        set_labels.assert_called_once_with(
+            expected_labels, OWNER, REPO, ISSUE_NR, ACCESS_TOKEN
+        )
+
+
 ALLOWED_LABELS_CONTENT = "# labels that the labelbot are allowed to set\n# at the behest of users without read-access\nhelp\nbug\nfeature request\n"
 
 JSON_ALLOWED_LABELS_PAYLOAD = json.dumps(

pip install -e .[TEST]
parse_log_pytest
pytest
pytest
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///croot/attrs_1668696182826/work
boto3==1.33.13
botocore==1.33.13
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
charset-normalizer==3.4.1
codecov==2.1.13
coverage==7.2.7
cryptography==44.0.2
Deprecated==1.2.18
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
jmespath==1.0.1
jwcrypto==1.5.1
-e git+https://github.com/slarse/labelbot.git@f41ded54f0647bc4dacfd0c92bb829f958c21bba#egg=labelbot
packaging @ file:///croot/packaging_1671697413597/work
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pycparser==2.21
pytest==7.1.2
pytest-cov==4.1.0
python-dateutil==2.9.0.post0
python-jwt==4.1.0
PyYAML==6.0.1
requests==2.31.0
responses==0.23.3
s3transfer==0.8.2
six==1.17.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
types-PyYAML==6.0.12.12
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
urllib3==1.26.20
wrapt==1.16.0
zipp @ file:///croot/zipp_1672387121353/work

name: labelbot
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - boto3==1.33.13
      - botocore==1.33.13
      - cffi==1.15.1
      - charset-normalizer==3.4.1
      - codecov==2.1.13
      - coverage==7.2.7
      - cryptography==44.0.2
      - deprecated==1.2.18
      - idna==3.10
      - jmespath==1.0.1
      - jwcrypto==1.5.1
      - pycparser==2.21
      - pytest-cov==4.1.0
      - python-dateutil==2.9.0.post0
      - python-jwt==4.1.0
      - pyyaml==6.0.1
      - requests==2.31.0
      - responses==0.23.3
      - s3transfer==0.8.2
      - six==1.17.0
      - types-pyyaml==6.0.12.12
      - urllib3==1.26.20
      - wrapt==1.16.0
prefix: /opt/conda/envs/labelbot

tests/test_github_api.py::TestSetAllowedLabels::test_happy_path
tests/test_github_api.py::TestSetLabels::test_returns_true_on_200_ok tests/test_github_api.py::TestSetLabels::test_returns_false_on_404 tests/test_github_api.py::TestGetLabels::test_returns_correct_labels tests/test_github_api.py::TestGetLabels::test_handles_incorrect_auth tests/test_github_api.py::TestGetFileContents::test_correctly_fetches_file_contents tests/test_github_api.py::TestGetFileContents::test_handles_incorrect_auth
labelbot/bot.py labelbot/github_api.py
labelbot/bot.py labelbot/github_api.py