Skip to content

Commit f1b5dc0

Browse files
mateusz834seiimonn
andauthored
Add patch and put methods to the service (#693)
Co-authored-by: Simon Noser <simon@noser.tech>
1 parent ab2530b commit f1b5dc0

File tree

3 files changed

+536
-21
lines changed

3 files changed

+536
-21
lines changed

splunklib/binding.py

Lines changed: 230 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,158 @@ def post(
862862
response = self.http.post(path, all_headers, **query)
863863
return response
864864

865+
@_authentication
866+
@_log_duration
867+
def put(
868+
self,
869+
path_segment,
870+
owner=None,
871+
app=None,
872+
sharing=None,
873+
headers=None,
874+
**query,
875+
):
876+
"""Performs a PUT operation from the REST path segment with the given object,
877+
namespace and query.
878+
879+
This method is named to match the HTTP method. ``put`` makes at least
880+
one round trip to the server, one additional round trip for each 303
881+
status returned, and at most two additional round trips if
882+
the ``autologin`` field of :func:`connect` is set to ``True``.
883+
884+
If *owner*, *app*, and *sharing* are omitted, this method uses the
885+
default :class:`Context` namespace. All other keyword arguments are
886+
included in the URL as query parameters.
887+
888+
If you provide a ``body`` argument to ``put``, it will be used as the PUT body,
889+
and all other keyword arguments will be passed as GET-style arguments in the URL.
890+
891+
:raises AuthenticationError: Raised when the ``Context`` object is not
892+
logged in.
893+
:raises HTTPError: Raised when an error occurred in a PUT operation from
894+
*path_segment*.
895+
:param path_segment: A REST path segment.
896+
:type path_segment: ``string``
897+
:param owner: The owner context of the namespace (optional).
898+
:type owner: ``string``
899+
:param app: The app context of the namespace (optional).
900+
:type app: ``string``
901+
:param sharing: The sharing mode of the namespace (optional).
902+
:type sharing: ``string``
903+
:param headers: List of extra HTTP headers to send (optional).
904+
:type headers: ``list`` of 2-tuples.
905+
:param query: All other keyword arguments, which are used as query
906+
parameters.
907+
:param body: Parameters to be used in the put body. If specified,
908+
any parameters in the query will be applied to the URL instead of
909+
the body. If a dict is supplied, the key-value pairs will be form
910+
encoded. If a string is supplied, the body will be passed through
911+
in the request unchanged.
912+
:type body: ``dict`` or ``str``
913+
:return: The response from the server.
914+
:rtype: ``dict`` with keys ``body``, ``headers``, ``reason``,
915+
and ``status``
916+
917+
**Example**::
918+
919+
c = binding.connect(...)
920+
# Call an HTTP endpoint, exposed as Custom Rest Endpoint in a Splunk App.
921+
# PUT /servicesNS/-/app_name/custom_rest_endpoint
922+
c.put(
923+
app="app_name",
924+
path_segment="custom_rest_endpoint",
925+
body=json.dumps({"key": "val"}),
926+
headers=[("Content-Type", "application/json")],
927+
)
928+
"""
929+
if headers is None:
930+
headers = []
931+
932+
path = self.authority + self._abspath(
933+
path_segment, owner=owner, app=app, sharing=sharing
934+
)
935+
936+
logger.debug("PUT request to %s (body: %s)", path, mask_sensitive_data(query))
937+
all_headers = headers + self.additional_headers + self._auth_headers
938+
response = self.http.put(path, all_headers, **query)
939+
return response
940+
941+
@_authentication
942+
@_log_duration
943+
def patch(
944+
self,
945+
path_segment,
946+
owner=None,
947+
app=None,
948+
sharing=None,
949+
headers=None,
950+
**query,
951+
):
952+
"""Performs a PATCH operation from the REST path segment with the given object,
953+
namespace and query.
954+
955+
This method is named to match the HTTP method. ``patch`` makes at least
956+
one round trip to the server, one additional round trip for each 303
957+
status returned, and at most two additional round trips if
958+
the ``autologin`` field of :func:`connect` is set to ``True``.
959+
960+
If *owner*, *app*, and *sharing* are omitted, this method uses the
961+
default :class:`Context` namespace. All other keyword arguments are
962+
included in the URL as query parameters.
963+
964+
If you provide a ``body`` argument to ``patch``, it will be used as the PATCH body,
965+
and all other keyword arguments will be passed as GET-style arguments in the URL.
966+
967+
:raises AuthenticationError: Raised when the ``Context`` object is not
968+
logged in.
969+
:raises HTTPError: Raised when an error occurred in a PATCH operation from
970+
*path_segment*.
971+
:param path_segment: A REST path segment.
972+
:type path_segment: ``string``
973+
:param owner: The owner context of the namespace (optional).
974+
:type owner: ``string``
975+
:param app: The app context of the namespace (optional).
976+
:type app: ``string``
977+
:param sharing: The sharing mode of the namespace (optional).
978+
:type sharing: ``string``
979+
:param headers: List of extra HTTP headers to send (optional).
980+
:type headers: ``list`` of 2-tuples.
981+
:param query: All other keyword arguments, which are used as query
982+
parameters.
983+
:param body: Parameters to be used in the patch body. If specified,
984+
any parameters in the query will be applied to the URL instead of
985+
the body. If a dict is supplied, the key-value pairs will be form
986+
encoded. If a string is supplied, the body will be passed through
987+
in the request unchanged.
988+
:type body: ``dict`` or ``str``
989+
:return: The response from the server.
990+
:rtype: ``dict`` with keys ``body``, ``headers``, ``reason``,
991+
and ``status``
992+
993+
**Example**::
994+
995+
c = binding.connect(...)
996+
# Call an HTTP endpoint, exposed as Custom Rest Endpoint in a Splunk App.
997+
# PATCH /servicesNS/-/app_name/custom_rest_endpoint
998+
c.patch(
999+
app="app_name",
1000+
path_segment="custom_rest_endpoint",
1001+
body=json.dumps({"key": "val"}),
1002+
headers=[("Content-Type", "application/json")],
1003+
)
1004+
"""
1005+
if headers is None:
1006+
headers = []
1007+
1008+
path = self.authority + self._abspath(
1009+
path_segment, owner=owner, app=app, sharing=sharing
1010+
)
1011+
1012+
logger.debug("PATCH request to %s (body: %s)", path, mask_sensitive_data(query))
1013+
all_headers = headers + self.additional_headers + self._auth_headers
1014+
response = self.http.patch(path, all_headers, **query)
1015+
return response
1016+
8651017
@_authentication
8661018
@_log_duration
8671019
def request(
@@ -1305,6 +1457,40 @@ def __init__(
13051457
self.retries = retries
13061458
self.retryDelay = retryDelay
13071459

1460+
def _prepare_request_body_and_url(self, url, headers, **kwargs):
1461+
"""Helper function to prepare the request body and URL.
1462+
1463+
:param url: The URL.
1464+
:type url: ``string``
1465+
:param headers: A list of pairs specifying the headers for the HTTP request.
1466+
:type headers: ``list``
1467+
:param kwargs: Additional keyword arguments (optional).
1468+
:type kwargs: ``dict``
1469+
:returns: A tuple containing the updated URL, headers, and body.
1470+
:rtype: ``tuple``
1471+
"""
1472+
if headers is None:
1473+
headers = []
1474+
1475+
# We handle GET-style arguments and an unstructured body. This is here
1476+
# to support the receivers/stream endpoint.
1477+
if "body" in kwargs:
1478+
# We only use application/x-www-form-urlencoded if there is no other
1479+
# Content-Type header present. This can happen in cases where we
1480+
# send requests as application/json, e.g. for KV Store.
1481+
if len([x for x in headers if x[0].lower() == "content-type"]) == 0:
1482+
headers.append(("Content-Type", "application/x-www-form-urlencoded"))
1483+
1484+
body = kwargs.pop("body")
1485+
if isinstance(body, dict):
1486+
body = _encode(**body).encode("utf-8")
1487+
if len(kwargs) > 0:
1488+
url = url + UrlEncoded("?" + _encode(**kwargs), skip_encode=True)
1489+
else:
1490+
body = _encode(**kwargs).encode("utf-8")
1491+
1492+
return url, headers, body
1493+
13081494
def delete(self, url, headers=None, **kwargs):
13091495
"""Sends a DELETE request to a URL.
13101496
@@ -1379,26 +1565,52 @@ def post(self, url, headers=None, **kwargs):
13791565
its structure).
13801566
:rtype: ``dict``
13811567
"""
1382-
if headers is None:
1383-
headers = []
1568+
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
1569+
message = {"method": "POST", "headers": headers, "body": body}
1570+
return self.request(url, message)
13841571

1385-
# We handle GET-style arguments and an unstructured body. This is here
1386-
# to support the receivers/stream endpoint.
1387-
if "body" in kwargs:
1388-
# We only use application/x-www-form-urlencoded if there is no other
1389-
# Content-Type header present. This can happen in cases where we
1390-
# send requests as application/json, e.g. for KV Store.
1391-
if len([x for x in headers if x[0].lower() == "content-type"]) == 0:
1392-
headers.append(("Content-Type", "application/x-www-form-urlencoded"))
1572+
def put(self, url, headers=None, **kwargs):
1573+
"""Sends a PUT request to a URL.
13931574
1394-
body = kwargs.pop("body")
1395-
if isinstance(body, dict):
1396-
body = _encode(**body).encode("utf-8")
1397-
if len(kwargs) > 0:
1398-
url = url + UrlEncoded("?" + _encode(**kwargs), skip_encode=True)
1399-
else:
1400-
body = _encode(**kwargs).encode("utf-8")
1401-
message = {"method": "POST", "headers": headers, "body": body}
1575+
:param url: The URL.
1576+
:type url: ``string``
1577+
:param headers: A list of pairs specifying the headers for the HTTP
1578+
response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``).
1579+
:type headers: ``list``
1580+
:param kwargs: Additional keyword arguments (optional). If the argument
1581+
is ``body``, the value is used as the body for the request, and the
1582+
keywords and their arguments will be URL encoded. If there is no
1583+
``body`` keyword argument, all the keyword arguments are encoded
1584+
into the body of the request in the format ``x-www-form-urlencoded``.
1585+
:type kwargs: ``dict``
1586+
:returns: A dictionary describing the response (see :class:`HttpLib` for
1587+
its structure).
1588+
:rtype: ``dict``
1589+
"""
1590+
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
1591+
message = {"method": "PUT", "headers": headers, "body": body}
1592+
return self.request(url, message)
1593+
1594+
def patch(self, url, headers=None, **kwargs):
1595+
"""Sends a PATCH request to a URL.
1596+
1597+
:param url: The URL.
1598+
:type url: ``string``
1599+
:param headers: A list of pairs specifying the headers for the HTTP
1600+
response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``).
1601+
:type headers: ``list``
1602+
:param kwargs: Additional keyword arguments (optional). If the argument
1603+
is ``body``, the value is used as the body for the request, and the
1604+
keywords and their arguments will be URL encoded. If there is no
1605+
``body`` keyword argument, all the keyword arguments are encoded
1606+
into the body of the request in the format ``x-www-form-urlencoded``.
1607+
:type kwargs: ``dict``
1608+
:returns: A dictionary describing the response (see :class:`HttpLib` for
1609+
its structure).
1610+
:rtype: ``dict``
1611+
"""
1612+
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
1613+
message = {"method": "PATCH", "headers": headers, "body": body}
14021614
return self.request(url, message)
14031615

14041616
def request(self, url, message, **kwargs):

0 commit comments

Comments
 (0)