Skip to content

Commit 8d2a8e1

Browse files
authored
Merge pull request #1 from seiimonn/Context-PUT/PATCH
Context put/patch
2 parents ba04e1b + 96b5df6 commit 8d2a8e1

File tree

2 files changed

+436
-17
lines changed

2 files changed

+436
-17
lines changed

splunklib/binding.py

Lines changed: 251 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,171 @@ def post(self, path_segment, owner=None, app=None, sharing=None, headers=None, *
809809
response = self.http.post(path, all_headers, **query)
810810
return response
811811

812+
813+
@_authentication
814+
@_log_duration
815+
def put(self, path_segment, object, owner=None, app=None, sharing=None, headers=None, **query):
816+
"""Performs a PUT operation from the REST path segment with the given object,
817+
namespace and query.
818+
819+
This method is named to match the HTTP method. ``put`` makes at least
820+
one round trip to the server, one additional round trip for each 303
821+
status returned, and at most two additional round trips if
822+
the ``autologin`` field of :func:`connect` is set to ``True``.
823+
824+
If *owner*, *app*, and *sharing* are omitted, this method uses the
825+
default :class:`Context` namespace. All other keyword arguments are
826+
included in the URL as query parameters.
827+
828+
Some of Splunk's endpoints, such as ``receivers/simple`` and
829+
``receivers/stream``, require unstructured data in the PUT body
830+
and all metadata passed as GET-style arguments. If you provide
831+
a ``body`` argument to ``put``, it will be used as the PUT
832+
body, and all other keyword arguments will be passed as
833+
GET-style arguments in the URL.
834+
835+
:raises AuthenticationError: Raised when the ``Context`` object is not
836+
logged in.
837+
:raises HTTPError: Raised when an error occurred in a GET operation from
838+
*path_segment*.
839+
:param path_segment: A REST path segment.
840+
:type path_segment: ``string``
841+
:param object: The object to be PUT.
842+
:type object: ``string``
843+
:param owner: The owner context of the namespace (optional).
844+
:type owner: ``string``
845+
:param app: The app context of the namespace (optional).
846+
:type app: ``string``
847+
:param sharing: The sharing mode of the namespace (optional).
848+
:type sharing: ``string``
849+
:param headers: List of extra HTTP headers to send (optional).
850+
:type headers: ``list`` of 2-tuples.
851+
:param query: All other keyword arguments, which are used as query
852+
parameters.
853+
:param body: Parameters to be used in the post body. If specified,
854+
any parameters in the query will be applied to the URL instead of
855+
the body. If a dict is supplied, the key-value pairs will be form
856+
encoded. If a string is supplied, the body will be passed through
857+
in the request unchanged.
858+
:type body: ``dict`` or ``str``
859+
:return: The response from the server.
860+
:rtype: ``dict`` with keys ``body``, ``headers``, ``reason``,
861+
and ``status``
862+
863+
**Example**::
864+
865+
c = binding.connect(...)
866+
c.post('saved/searches', name='boris',
867+
search='search * earliest=-1m | head 1') == \\
868+
{'body': ...a response reader object...,
869+
'headers': [('content-length', '10455'),
870+
('expires', 'Fri, 30 Oct 1998 00:00:00 GMT'),
871+
('server', 'Splunkd'),
872+
('connection', 'close'),
873+
('cache-control', 'no-store, max-age=0, must-revalidate, no-cache'),
874+
('date', 'Fri, 11 May 2012 16:46:06 GMT'),
875+
('content-type', 'text/xml; charset=utf-8')],
876+
'reason': 'Created',
877+
'status': 201}
878+
c.post('nonexistant/path') # raises HTTPError
879+
c.logout()
880+
# raises AuthenticationError:
881+
c.put('saved/searches/boris',
882+
search='search * earliest=-1m | head 1')
883+
"""
884+
if headers is None:
885+
headers = []
886+
887+
path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) + f"/{object}"
888+
889+
logger.debug("PUT request to %s (body: %s)", path, mask_sensitive_data(query))
890+
all_headers = headers + self.additional_headers + self._auth_headers
891+
response = self.http.put(path, all_headers, **query)
892+
return response
893+
894+
895+
@_authentication
896+
@_log_duration
897+
def patch(self, path_segment, object, owner=None, app=None, sharing=None, headers=None, **query):
898+
"""Performs a PATCH operation from the REST path segment with the given object,
899+
namespace and query.
900+
901+
This method is named to match the HTTP method. ``patch`` makes at least
902+
one round trip to the server, one additional round trip for each 303
903+
status returned, and at most two additional round trips if
904+
the ``autologin`` field of :func:`connect` is set to ``True``.
905+
906+
If *owner*, *app*, and *sharing* are omitted, this method uses the
907+
default :class:`Context` namespace. All other keyword arguments are
908+
included in the URL as query parameters.
909+
910+
Some of Splunk's endpoints, such as ``receivers/simple`` and
911+
``receivers/stream``, require unstructured data in the PATCH body
912+
and all metadata passed as GET-style arguments. If you provide
913+
a ``body`` argument to ``patch``, it will be used as the PATCH
914+
body, and all other keyword arguments will be passed as
915+
GET-style arguments in the URL.
916+
917+
:raises AuthenticationError: Raised when the ``Context`` object is not
918+
logged in.
919+
:raises HTTPError: Raised when an error occurred in a GET operation from
920+
*path_segment*.
921+
:param path_segment: A REST path segment.
922+
:type path_segment: ``string``
923+
:param object: The object to be PUT.
924+
:type object: ``string``
925+
:param owner: The owner context of the namespace (optional).
926+
:type owner: ``string``
927+
:param app: The app context of the namespace (optional).
928+
:type app: ``string``
929+
:param sharing: The sharing mode of the namespace (optional).
930+
:type sharing: ``string``
931+
:param headers: List of extra HTTP headers to send (optional).
932+
:type headers: ``list`` of 2-tuples.
933+
:param query: All other keyword arguments, which are used as query
934+
parameters.
935+
:param body: Parameters to be used in the post body. If specified,
936+
any parameters in the query will be applied to the URL instead of
937+
the body. If a dict is supplied, the key-value pairs will be form
938+
encoded. If a string is supplied, the body will be passed through
939+
in the request unchanged.
940+
:type body: ``dict`` or ``str``
941+
:return: The response from the server.
942+
:rtype: ``dict`` with keys ``body``, ``headers``, ``reason``,
943+
and ``status``
944+
945+
**Example**::
946+
947+
c = binding.connect(...)
948+
c.post('saved/searches', name='boris',
949+
search='search * earliest=-1m | head 1') == \\
950+
{'body': ...a response reader object...,
951+
'headers': [('content-length', '10455'),
952+
('expires', 'Fri, 30 Oct 1998 00:00:00 GMT'),
953+
('server', 'Splunkd'),
954+
('connection', 'close'),
955+
('cache-control', 'no-store, max-age=0, must-revalidate, no-cache'),
956+
('date', 'Fri, 11 May 2012 16:46:06 GMT'),
957+
('content-type', 'text/xml; charset=utf-8')],
958+
'reason': 'Created',
959+
'status': 201}
960+
c.post('nonexistant/path') # raises HTTPError
961+
c.logout()
962+
# raises AuthenticationError:
963+
c.patch('saved/searches/boris',
964+
search='search * earliest=-1m | head 1')
965+
"""
966+
if headers is None:
967+
headers = []
968+
969+
path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) + f"/{object}"
970+
971+
logger.debug("PATCH request to %s (body: %s)", path, mask_sensitive_data(query))
972+
all_headers = headers + self.additional_headers + self._auth_headers
973+
response = self.http.patch(path, all_headers, **query)
974+
return response
975+
976+
812977
@_authentication
813978
@_log_duration
814979
def request(self, path_segment, method="GET", headers=None, body={},
@@ -1210,6 +1375,40 @@ def __init__(self, custom_handler=None, verify=False, key_file=None, cert_file=N
12101375
self.retries = retries
12111376
self.retryDelay = retryDelay
12121377

1378+
def _prepare_request_body_and_url(self, url, headers, **kwargs):
1379+
"""Helper function to prepare the request body and URL.
1380+
1381+
:param url: The URL.
1382+
:type url: ``string``
1383+
:param headers: A list of pairs specifying the headers for the HTTP request.
1384+
:type headers: ``list``
1385+
:param kwargs: Additional keyword arguments (optional).
1386+
:type kwargs: ``dict``
1387+
:returns: A tuple containing the updated URL, headers, and body.
1388+
:rtype: ``tuple``
1389+
"""
1390+
if headers is None:
1391+
headers = []
1392+
1393+
# We handle GET-style arguments and an unstructured body. This is here
1394+
# to support the receivers/stream endpoint.
1395+
if 'body' in kwargs:
1396+
# We only use application/x-www-form-urlencoded if there is no other
1397+
# Content-Type header present. This can happen in cases where we
1398+
# send requests as application/json, e.g. for KV Store.
1399+
if len([x for x in headers if x[0].lower() == "content-type"]) == 0:
1400+
headers.append(("Content-Type", "application/x-www-form-urlencoded"))
1401+
1402+
body = kwargs.pop('body')
1403+
if isinstance(body, dict):
1404+
body = _encode(**body).encode('utf-8')
1405+
if len(kwargs) > 0:
1406+
url = url + UrlEncoded('?' + _encode(**kwargs), skip_encode=True)
1407+
else:
1408+
body = _encode(**kwargs).encode('utf-8')
1409+
1410+
return url, headers, body
1411+
12131412
def delete(self, url, headers=None, **kwargs):
12141413
"""Sends a DELETE request to a URL.
12151414
@@ -1282,31 +1481,66 @@ def post(self, url, headers=None, **kwargs):
12821481
its structure).
12831482
:rtype: ``dict``
12841483
"""
1285-
if headers is None: headers = []
1484+
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
1485+
message = {
1486+
'method': "POST",
1487+
'headers': headers,
1488+
'body': body
1489+
}
1490+
return self.request(url, message)
12861491

1287-
# We handle GET-style arguments and an unstructured body. This is here
1288-
# to support the receivers/stream endpoint.
1289-
if 'body' in kwargs:
1290-
# We only use application/x-www-form-urlencoded if there is no other
1291-
# Content-Type header present. This can happen in cases where we
1292-
# send requests as application/json, e.g. for KV Store.
1293-
if len([x for x in headers if x[0].lower() == "content-type"]) == 0:
1294-
headers.append(("Content-Type", "application/x-www-form-urlencoded"))
1492+
def put(self, url, headers=None, **kwargs):
1493+
"""Sends a PUT request to a URL.
12951494
1296-
body = kwargs.pop('body')
1297-
if isinstance(body, dict):
1298-
body = _encode(**body).encode('utf-8')
1299-
if len(kwargs) > 0:
1300-
url = url + UrlEncoded('?' + _encode(**kwargs), skip_encode=True)
1301-
else:
1302-
body = _encode(**kwargs).encode('utf-8')
1495+
:param url: The URL.
1496+
:type url: ``string``
1497+
:param headers: A list of pairs specifying the headers for the HTTP
1498+
response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``).
1499+
:type headers: ``list``
1500+
:param kwargs: Additional keyword arguments (optional). If the argument
1501+
is ``body``, the value is used as the body for the request, and the
1502+
keywords and their arguments will be URL encoded. If there is no
1503+
``body`` keyword argument, all the keyword arguments are encoded
1504+
into the body of the request in the format ``x-www-form-urlencoded``.
1505+
:type kwargs: ``dict``
1506+
:returns: A dictionary describing the response (see :class:`HttpLib` for
1507+
its structure).
1508+
:rtype: ``dict``
1509+
"""
1510+
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
13031511
message = {
1304-
'method': "POST",
1512+
'method': "PUT",
13051513
'headers': headers,
13061514
'body': body
13071515
}
13081516
return self.request(url, message)
1517+
1518+
def patch(self, url, headers=None, **kwargs):
1519+
"""Sends a PATCH request to a URL.
13091520
1521+
:param url: The URL.
1522+
:type url: ``string``
1523+
:param headers: A list of pairs specifying the headers for the HTTP
1524+
response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``).
1525+
:type headers: ``list``
1526+
:param kwargs: Additional keyword arguments (optional). If the argument
1527+
is ``body``, the value is used as the body for the request, and the
1528+
keywords and their arguments will be URL encoded. If there is no
1529+
``body`` keyword argument, all the keyword arguments are encoded
1530+
into the body of the request in the format ``x-www-form-urlencoded``.
1531+
:type kwargs: ``dict``
1532+
:returns: A dictionary describing the response (see :class:`HttpLib` for
1533+
its structure).
1534+
:rtype: ``dict``
1535+
"""
1536+
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
1537+
message = {
1538+
'method': "PATCH",
1539+
'headers': headers,
1540+
'body': body
1541+
}
1542+
return self.request(url, message)
1543+
13101544
def request(self, url, message, **kwargs):
13111545
"""Issues an HTTP request to a URL.
13121546

0 commit comments

Comments
 (0)