Skip to content

Commit a4f32af

Browse files
committed
Adding PUT and PATCH method to Context
1 parent d8f02d0 commit a4f32af

File tree

2 files changed

+444
-22
lines changed

2 files changed

+444
-22
lines changed

splunklib/binding.py

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

865+
866+
@_authentication
867+
@_log_duration
868+
def put(self, path_segment, object, owner=None, app=None, sharing=None, headers=None, **query):
869+
"""Performs a PUT operation from the REST path segment with the given object,
870+
namespace and query.
871+
872+
This method is named to match the HTTP method. ``put`` makes at least
873+
one round trip to the server, one additional round trip for each 303
874+
status returned, and at most two additional round trips if
875+
the ``autologin`` field of :func:`connect` is set to ``True``.
876+
877+
If *owner*, *app*, and *sharing* are omitted, this method uses the
878+
default :class:`Context` namespace. All other keyword arguments are
879+
included in the URL as query parameters.
880+
881+
Some of Splunk's endpoints, such as ``receivers/simple`` and
882+
``receivers/stream``, require unstructured data in the PUT body
883+
and all metadata passed as GET-style arguments. If you provide
884+
a ``body`` argument to ``put``, it will be used as the PUT
885+
body, and all other keyword arguments will be passed as
886+
GET-style arguments in the URL.
887+
888+
:raises AuthenticationError: Raised when the ``Context`` object is not
889+
logged in.
890+
:raises HTTPError: Raised when an error occurred in a GET operation from
891+
*path_segment*.
892+
:param path_segment: A REST path segment.
893+
:type path_segment: ``string``
894+
:param object: The object to be PUT.
895+
:type object: ``string``
896+
:param owner: The owner context of the namespace (optional).
897+
:type owner: ``string``
898+
:param app: The app context of the namespace (optional).
899+
:type app: ``string``
900+
:param sharing: The sharing mode of the namespace (optional).
901+
:type sharing: ``string``
902+
:param headers: List of extra HTTP headers to send (optional).
903+
:type headers: ``list`` of 2-tuples.
904+
:param query: All other keyword arguments, which are used as query
905+
parameters.
906+
:param body: Parameters to be used in the post body. If specified,
907+
any parameters in the query will be applied to the URL instead of
908+
the body. If a dict is supplied, the key-value pairs will be form
909+
encoded. If a string is supplied, the body will be passed through
910+
in the request unchanged.
911+
:type body: ``dict`` or ``str``
912+
:return: The response from the server.
913+
:rtype: ``dict`` with keys ``body``, ``headers``, ``reason``,
914+
and ``status``
915+
916+
**Example**::
917+
918+
c = binding.connect(...)
919+
c.post('saved/searches', name='boris',
920+
search='search * earliest=-1m | head 1') == \\
921+
{'body': ...a response reader object...,
922+
'headers': [('content-length', '10455'),
923+
('expires', 'Fri, 30 Oct 1998 00:00:00 GMT'),
924+
('server', 'Splunkd'),
925+
('connection', 'close'),
926+
('cache-control', 'no-store, max-age=0, must-revalidate, no-cache'),
927+
('date', 'Fri, 11 May 2012 16:46:06 GMT'),
928+
('content-type', 'text/xml; charset=utf-8')],
929+
'reason': 'Created',
930+
'status': 201}
931+
c.post('nonexistant/path') # raises HTTPError
932+
c.logout()
933+
# raises AuthenticationError:
934+
c.put('saved/searches/boris',
935+
search='search * earliest=-1m | head 1')
936+
"""
937+
if headers is None:
938+
headers = []
939+
940+
path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) + f"/{object}"
941+
942+
logger.debug("PUT request to %s (body: %s)", path, mask_sensitive_data(query))
943+
all_headers = headers + self.additional_headers + self._auth_headers
944+
response = self.http.put(path, all_headers, **query)
945+
return response
946+
947+
948+
@_authentication
949+
@_log_duration
950+
def patch(self, path_segment, object, owner=None, app=None, sharing=None, headers=None, **query):
951+
"""Performs a PATCH operation from the REST path segment with the given object,
952+
namespace and query.
953+
954+
This method is named to match the HTTP method. ``patch`` makes at least
955+
one round trip to the server, one additional round trip for each 303
956+
status returned, and at most two additional round trips if
957+
the ``autologin`` field of :func:`connect` is set to ``True``.
958+
959+
If *owner*, *app*, and *sharing* are omitted, this method uses the
960+
default :class:`Context` namespace. All other keyword arguments are
961+
included in the URL as query parameters.
962+
963+
Some of Splunk's endpoints, such as ``receivers/simple`` and
964+
``receivers/stream``, require unstructured data in the PATCH body
965+
and all metadata passed as GET-style arguments. If you provide
966+
a ``body`` argument to ``patch``, it will be used as the PATCH
967+
body, and all other keyword arguments will be passed as
968+
GET-style arguments in the URL.
969+
970+
:raises AuthenticationError: Raised when the ``Context`` object is not
971+
logged in.
972+
:raises HTTPError: Raised when an error occurred in a GET operation from
973+
*path_segment*.
974+
:param path_segment: A REST path segment.
975+
:type path_segment: ``string``
976+
:param object: The object to be PUT.
977+
:type object: ``string``
978+
:param owner: The owner context of the namespace (optional).
979+
:type owner: ``string``
980+
:param app: The app context of the namespace (optional).
981+
:type app: ``string``
982+
:param sharing: The sharing mode of the namespace (optional).
983+
:type sharing: ``string``
984+
:param headers: List of extra HTTP headers to send (optional).
985+
:type headers: ``list`` of 2-tuples.
986+
:param query: All other keyword arguments, which are used as query
987+
parameters.
988+
:param body: Parameters to be used in the post body. If specified,
989+
any parameters in the query will be applied to the URL instead of
990+
the body. If a dict is supplied, the key-value pairs will be form
991+
encoded. If a string is supplied, the body will be passed through
992+
in the request unchanged.
993+
:type body: ``dict`` or ``str``
994+
:return: The response from the server.
995+
:rtype: ``dict`` with keys ``body``, ``headers``, ``reason``,
996+
and ``status``
997+
998+
**Example**::
999+
1000+
c = binding.connect(...)
1001+
c.post('saved/searches', name='boris',
1002+
search='search * earliest=-1m | head 1') == \\
1003+
{'body': ...a response reader object...,
1004+
'headers': [('content-length', '10455'),
1005+
('expires', 'Fri, 30 Oct 1998 00:00:00 GMT'),
1006+
('server', 'Splunkd'),
1007+
('connection', 'close'),
1008+
('cache-control', 'no-store, max-age=0, must-revalidate, no-cache'),
1009+
('date', 'Fri, 11 May 2012 16:46:06 GMT'),
1010+
('content-type', 'text/xml; charset=utf-8')],
1011+
'reason': 'Created',
1012+
'status': 201}
1013+
c.post('nonexistant/path') # raises HTTPError
1014+
c.logout()
1015+
# raises AuthenticationError:
1016+
c.patch('saved/searches/boris',
1017+
search='search * earliest=-1m | head 1')
1018+
"""
1019+
if headers is None:
1020+
headers = []
1021+
1022+
path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) + f"/{object}"
1023+
1024+
logger.debug("PATCH request to %s (body: %s)", path, mask_sensitive_data(query))
1025+
all_headers = headers + self.additional_headers + self._auth_headers
1026+
response = self.http.patch(path, all_headers, **query)
1027+
return response
1028+
1029+
8651030
@_authentication
8661031
@_log_duration
8671032
def request(
@@ -939,11 +1104,11 @@ def request(
9391104
mask_sensitive_data(body),
9401105
)
9411106
if body:
942-
body = _encode(**body)
943-
9441107
if method == "GET":
945-
path = path + UrlEncoded("?" + body, skip_encode=True)
946-
message = {"method": method, "headers": all_headers}
1108+
body = _encode(**body)
1109+
path = path + UrlEncoded('?' + body, skip_encode=True)
1110+
message = {'method': method,
1111+
'headers': all_headers}
9471112
else:
9481113
message = {"method": method, "headers": all_headers, "body": body}
9491114
else:
@@ -1301,6 +1466,40 @@ def __init__(
13011466
self.retries = retries
13021467
self.retryDelay = retryDelay
13031468

1469+
def _prepare_request_body_and_url(self, url, headers, **kwargs):
1470+
"""Helper function to prepare the request body and URL.
1471+
1472+
:param url: The URL.
1473+
:type url: ``string``
1474+
:param headers: A list of pairs specifying the headers for the HTTP request.
1475+
:type headers: ``list``
1476+
:param kwargs: Additional keyword arguments (optional).
1477+
:type kwargs: ``dict``
1478+
:returns: A tuple containing the updated URL, headers, and body.
1479+
:rtype: ``tuple``
1480+
"""
1481+
if headers is None:
1482+
headers = []
1483+
1484+
# We handle GET-style arguments and an unstructured body. This is here
1485+
# to support the receivers/stream endpoint.
1486+
if 'body' in kwargs:
1487+
# We only use application/x-www-form-urlencoded if there is no other
1488+
# Content-Type header present. This can happen in cases where we
1489+
# send requests as application/json, e.g. for KV Store.
1490+
if len([x for x in headers if x[0].lower() == "content-type"]) == 0:
1491+
headers.append(("Content-Type", "application/x-www-form-urlencoded"))
1492+
1493+
body = kwargs.pop('body')
1494+
if isinstance(body, dict):
1495+
body = _encode(**body).encode('utf-8')
1496+
if len(kwargs) > 0:
1497+
url = url + UrlEncoded('?' + _encode(**kwargs), skip_encode=True)
1498+
else:
1499+
body = _encode(**kwargs).encode('utf-8')
1500+
1501+
return url, headers, body
1502+
13041503
def delete(self, url, headers=None, **kwargs):
13051504
"""Sends a DELETE request to a URL.
13061505
@@ -1375,28 +1574,66 @@ def post(self, url, headers=None, **kwargs):
13751574
its structure).
13761575
:rtype: ``dict``
13771576
"""
1378-
if headers is None:
1379-
headers = []
1577+
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
1578+
message = {
1579+
'method': "POST",
1580+
'headers': headers,
1581+
'body': body
1582+
}
1583+
return self.request(url, message)
13801584

1381-
# We handle GET-style arguments and an unstructured body. This is here
1382-
# to support the receivers/stream endpoint.
1383-
if "body" in kwargs:
1384-
# We only use application/x-www-form-urlencoded if there is no other
1385-
# Content-Type header present. This can happen in cases where we
1386-
# send requests as application/json, e.g. for KV Store.
1387-
if len([x for x in headers if x[0].lower() == "content-type"]) == 0:
1388-
headers.append(("Content-Type", "application/x-www-form-urlencoded"))
1585+
def put(self, url, headers=None, **kwargs):
1586+
"""Sends a PUT request to a URL.
13891587
1390-
body = kwargs.pop("body")
1391-
if isinstance(body, dict):
1392-
body = _encode(**body).encode("utf-8")
1393-
if len(kwargs) > 0:
1394-
url = url + UrlEncoded("?" + _encode(**kwargs), skip_encode=True)
1395-
else:
1396-
body = _encode(**kwargs).encode("utf-8")
1397-
message = {"method": "POST", "headers": headers, "body": body}
1588+
:param url: The URL.
1589+
:type url: ``string``
1590+
:param headers: A list of pairs specifying the headers for the HTTP
1591+
response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``).
1592+
:type headers: ``list``
1593+
:param kwargs: Additional keyword arguments (optional). If the argument
1594+
is ``body``, the value is used as the body for the request, and the
1595+
keywords and their arguments will be URL encoded. If there is no
1596+
``body`` keyword argument, all the keyword arguments are encoded
1597+
into the body of the request in the format ``x-www-form-urlencoded``.
1598+
:type kwargs: ``dict``
1599+
:returns: A dictionary describing the response (see :class:`HttpLib` for
1600+
its structure).
1601+
:rtype: ``dict``
1602+
"""
1603+
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
1604+
message = {
1605+
'method': "PUT",
1606+
'headers': headers,
1607+
'body': body
1608+
}
13981609
return self.request(url, message)
1610+
1611+
def patch(self, url, headers=None, **kwargs):
1612+
"""Sends a PATCH request to a URL.
13991613
1614+
:param url: The URL.
1615+
:type url: ``string``
1616+
:param headers: A list of pairs specifying the headers for the HTTP
1617+
response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``).
1618+
:type headers: ``list``
1619+
:param kwargs: Additional keyword arguments (optional). If the argument
1620+
is ``body``, the value is used as the body for the request, and the
1621+
keywords and their arguments will be URL encoded. If there is no
1622+
``body`` keyword argument, all the keyword arguments are encoded
1623+
into the body of the request in the format ``x-www-form-urlencoded``.
1624+
:type kwargs: ``dict``
1625+
:returns: A dictionary describing the response (see :class:`HttpLib` for
1626+
its structure).
1627+
:rtype: ``dict``
1628+
"""
1629+
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
1630+
message = {
1631+
'method': "PATCH",
1632+
'headers': headers,
1633+
'body': body
1634+
}
1635+
return self.request(url, message)
1636+
14001637
def request(self, url, message, **kwargs):
14011638
"""Issues an HTTP request to a URL.
14021639

0 commit comments

Comments
 (0)