Repository: netbox-community/pynetbox

Issue:
Choices are not returned when the API token can PUT but not POST on an endpoint
### pynetbox version

v7.1.0

### NetBox version

v3.4.3

### Python version

3.9

### Steps to Reproduce

Using an API token that cannot POST on the `/dcim/devices/` endpoint for instance, try getting choices using

```python
nb.dcim.devices.choices()
```

### Expected Behavior

A list of choices is returned.

### Observed Behavior

```
ValueError: Unexpected format in the OPTIONS response at https://netbox.example.com/api/dcim/devices
```

Discussion:
@BenoitKnecht: I can submit a PR for a simple fix that would get the choices from the `.actions.PUT` sections returned by the `OPTIONS` request when `.actions.POST` is missing.@markkuleinio: Just wondering: what is the use case for fetching the choices list when there is no possibility anyway to use the choice values to create/edit objects?

Or do I miss something about POST vs PUT@BenoitKnecht: > Just wondering: what is the use case for fetching the choices list when there is no possibility anyway to use the choice values to create/edit objects?
> 
> Or do I miss something about POST vs PUT

@markkuleinio With POST you create new objects, and with PUT you update existing objects. So fetching choices is useful in that latter case too. :slightly_smiling_face: @markkuleinio: Right, so the use case is a user that doesn't have create access but has modify access.@BenoitKnecht: Hey, could someone review (and ideally merge) the PR above (#592)? I'm still hitting this bug whenever I use Ansible to interact with Netbox.

Solution patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index acab216..30c3b41 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -1,7 +1,7 @@
 ---
 name: 🐛 Bug Report
 description: Report a reproducible bug in the current release of pynetbox
-labels: ["type: bug"]
+labels: ["type: bug", "status: needs triage"]
 body:
   - type: markdown
     attributes:
@@ -11,7 +11,7 @@ body:
     attributes:
       label: pynetbox version
       description: What version of pynetbox are you currently running?
-      placeholder: v7.4.0
+      placeholder: v7.4.1
     validations:
       required: true
   - type: input
diff --git a/.github/ISSUE_TEMPLATE/documentation_change.yaml b/.github/ISSUE_TEMPLATE/documentation_change.yaml
index dda25d8..ea96c5e 100644
--- a/.github/ISSUE_TEMPLATE/documentation_change.yaml
+++ b/.github/ISSUE_TEMPLATE/documentation_change.yaml
@@ -1,7 +1,7 @@
 ---
 name: 📖 Documentation Change
 description: Suggest an addition or modification to the pynetbox documentation
-labels: ["type: documentation"]
+labels: ["type: documentation", "status: needs triage"]
 body:
   - type: dropdown
     attributes:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
index a38ac5c..bec05e4 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yaml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -1,7 +1,7 @@
 ---
 name: ✨ Feature Request
 description: Propose a new pynetbox feature or enhancement
-labels: ["type: feature"]
+labels: ["type: feature", "status: needs triage"]
 body:
   - type: markdown
     attributes:
diff --git a/README.md b/README.md
index 0e3d9ff..c87edf0 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ Each pyNetBox Version listed below has been tested with its corresponding NetBox
 
 | NetBox Version | Plugin Version |
 |:--------------:|:--------------:|
-|      4.0.6     |     7.4.0      |
+|      4.0.6     |     7.4.1      |
 |      4.0.0     |     7.3.4      |
 |      3.7       |     7.3.0      |
 |      3.6       |     7.2.0      |
diff --git a/pynetbox/__init__.py b/pynetbox/__init__.py
index 00e30d8..ee38c65 100644
--- a/pynetbox/__init__.py
+++ b/pynetbox/__init__.py
@@ -1,4 +1,4 @@
 from pynetbox.core.api import Api as api
 from pynetbox.core.query import AllocationError, ContentError, RequestError
 
-__version__ = "7.4.0"
+__version__ = "7.4.1"
diff --git a/pynetbox/core/endpoint.py b/pynetbox/core/endpoint.py
index dfd989e..a0f7bef 100644
--- a/pynetbox/core/endpoint.py
+++ b/pynetbox/core/endpoint.py
@@ -587,9 +587,10 @@ class Endpoint:
             token=self.api.token,
             http_session=self.api.http_session,
         ).options()
-        try:
-            post_data = req["actions"]["POST"]
-        except KeyError:
+
+        actions = req.get("actions", {})
+        post_data = actions.get("POST") or actions.get("PUT")
+        if post_data is None:
             raise ValueError(
                 "Unexpected format in the OPTIONS response at {}".format(self.url)
             )
diff --git a/requirements.txt b/requirements.txt
index 92e2fce..c8f5d0f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
-requests>=2.20.0,<3.0
+requests>=2.32.3,<3.0
+urllib3>=2.2.3,<3
 packaging


Test patch:
diff --git a/tests/unit/test_endpoint.py b/tests/unit/test_endpoint.py
index 344b116..fe6a495 100644
--- a/tests/unit/test_endpoint.py
+++ b/tests/unit/test_endpoint.py
@@ -60,6 +60,59 @@ class EndPointTestCase(unittest.TestCase):
             self.assertEqual(choices["letter"][1]["display_name"], "B")
             self.assertEqual(choices["letter"][1]["value"], 2)
 
+    def test_choices_put(self):
+        with patch("pynetbox.core.query.Request.options", return_value=Mock()) as mock:
+            api = Mock(base_url="http://localhost:8000/api")
+            app = Mock(name="test")
+            mock.return_value = {
+                "actions": {
+                    "PUT": {
+                        "letter": {
+                            "choices": [
+                                {"display_name": "A", "value": 1},
+                                {"display_name": "B", "value": 2},
+                                {"display_name": "C", "value": 3},
+                            ]
+                        }
+                    }
+                }
+            }
+            test_obj = Endpoint(api, app, "test")
+            choices = test_obj.choices()
+            self.assertEqual(choices["letter"][0]["display_name"], "A")
+            self.assertEqual(choices["letter"][0]["value"], 1)
+
+    def test_choices_precedence(self):
+        with patch("pynetbox.core.query.Request.options", return_value=Mock()) as mock:
+            api = Mock(base_url="http://localhost:8000/api")
+            app = Mock(name="test")
+            mock.return_value = {
+                "actions": {
+                    "POST": {
+                        "letter": {
+                            "choices": [
+                                {"display_name": "A", "value": 1},
+                                {"display_name": "B", "value": 2},
+                                {"display_name": "C", "value": 3},
+                            ]
+                        }
+                    },
+                    "PUT": {
+                        "letter": {
+                            "choices": [
+                                {"display_name": "D", "value": 4},
+                                {"display_name": "E", "value": 5},
+                                {"display_name": "F", "value": 6},
+                            ]
+                        }
+                    },
+                }
+            }
+            test_obj = Endpoint(api, app, "test")
+            choices = test_obj.choices()
+            self.assertEqual(choices["letter"][2]["display_name"], "C")
+            self.assertEqual(choices["letter"][2]["value"], 3)
+
     def test_get_with_filter(self):
         with patch(
             "pynetbox.core.query.Request._make_call", return_value=Mock()