Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
278 changes: 258 additions & 20 deletions splunklib/binding.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,171 @@ def post(
response = self.http.post(path, all_headers, **query)
return response


@_authentication
@_log_duration
def put(self, path_segment, object, owner=None, app=None, sharing=None, headers=None, **query):
"""Performs a PUT operation from the REST path segment with the given object,
namespace and query.

This method is named to match the HTTP method. ``put`` makes at least
one round trip to the server, one additional round trip for each 303
status returned, and at most two additional round trips if
the ``autologin`` field of :func:`connect` is set to ``True``.

If *owner*, *app*, and *sharing* are omitted, this method uses the
default :class:`Context` namespace. All other keyword arguments are
included in the URL as query parameters.

Some of Splunk's endpoints, such as ``receivers/simple`` and
``receivers/stream``, require unstructured data in the PUT body
and all metadata passed as GET-style arguments. If you provide
a ``body`` argument to ``put``, it will be used as the PUT
body, and all other keyword arguments will be passed as
GET-style arguments in the URL.
Comment on lines +881 to +886
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Splunk does not support PUT method in its endpoints, so lets not mention that.

Suggested change
Some of Splunk's endpoints, such as ``receivers/simple`` and
``receivers/stream``, require unstructured data in the PUT body
and all metadata passed as GET-style arguments. If you provide
a ``body`` argument to ``put``, it will be used as the PUT
body, and all other keyword arguments will be passed as
GET-style arguments in the URL.
If you provide a ``body`` argument to ``put``,
it will be used as the PUT body, and all other
keyword arguments will be passed as GET-style
arguments in the URL.


:raises AuthenticationError: Raised when the ``Context`` object is not
logged in.
:raises HTTPError: Raised when an error occurred in a GET operation from
*path_segment*.
:param path_segment: A REST path segment.
:type path_segment: ``string``
:param object: The object to be PUT.
:type object: ``string``
:param owner: The owner context of the namespace (optional).
:type owner: ``string``
:param app: The app context of the namespace (optional).
:type app: ``string``
:param sharing: The sharing mode of the namespace (optional).
:type sharing: ``string``
:param headers: List of extra HTTP headers to send (optional).
:type headers: ``list`` of 2-tuples.
:param query: All other keyword arguments, which are used as query
parameters.
:param body: Parameters to be used in the post body. If specified,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:param body: Parameters to be used in the post body. If specified,
:param body: Parameters to be used in the put body. If specified,

any parameters in the query will be applied to the URL instead of
the body. If a dict is supplied, the key-value pairs will be form
encoded. If a string is supplied, the body will be passed through
in the request unchanged.
:type body: ``dict`` or ``str``
:return: The response from the server.
:rtype: ``dict`` with keys ``body``, ``headers``, ``reason``,
and ``status``

**Example**::

c = binding.connect(...)
c.post('saved/searches', name='boris',
search='search * earliest=-1m | head 1') == \\
{'body': ...a response reader object...,
'headers': [('content-length', '10455'),
('expires', 'Fri, 30 Oct 1998 00:00:00 GMT'),
('server', 'Splunkd'),
('connection', 'close'),
('cache-control', 'no-store, max-age=0, must-revalidate, no-cache'),
('date', 'Fri, 11 May 2012 16:46:06 GMT'),
('content-type', 'text/xml; charset=utf-8')],
'reason': 'Created',
'status': 201}
c.post('nonexistant/path') # raises HTTPError
c.logout()
# raises AuthenticationError:
c.put('saved/searches/boris',
search='search * earliest=-1m | head 1')
"""
Comment on lines +916 to +936
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should change this example, splunk does not really support PUT/PATCH in its endpoints see: https://help.splunk.com/en/splunk-enterprise/leverage-rest-apis/rest-api-reference/10.0/introduction/endpoints-reference-list (that is the reason there were no such methods in the first place)

I think we should have an example that shows that it is used with custom rest endpoints, something like:

# Call an HTTP endpoint, exposed as Custom Rest Endpoint in a Splunk App.
# PUT /servicesNS/-/app_name/custom_rest_endpoint
service.put(
     app="app_name",
     path_segment="custom_rest_endpoint",
     body=json.dumps({"key": "val"}),
     headers=[("Content-Type", "application/json")],
)

if headers is None:
headers = []

path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) + f"/{object}"

logger.debug("PUT request to %s (body: %s)", path, mask_sensitive_data(query))
all_headers = headers + self.additional_headers + self._auth_headers
response = self.http.put(path, all_headers, **query)
return response


@_authentication
@_log_duration
def patch(self, path_segment, object, owner=None, app=None, sharing=None, headers=None, **query):
"""Performs a PATCH operation from the REST path segment with the given object,
namespace and query.

This method is named to match the HTTP method. ``patch`` makes at least
one round trip to the server, one additional round trip for each 303
status returned, and at most two additional round trips if
the ``autologin`` field of :func:`connect` is set to ``True``.

If *owner*, *app*, and *sharing* are omitted, this method uses the
default :class:`Context` namespace. All other keyword arguments are
included in the URL as query parameters.

Some of Splunk's endpoints, such as ``receivers/simple`` and
``receivers/stream``, require unstructured data in the PATCH body
and all metadata passed as GET-style arguments. If you provide
a ``body`` argument to ``patch``, it will be used as the PATCH
body, and all other keyword arguments will be passed as
GET-style arguments in the URL.
Comment on lines +963 to +968
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.


:raises AuthenticationError: Raised when the ``Context`` object is not
logged in.
:raises HTTPError: Raised when an error occurred in a GET operation from
*path_segment*.
:param path_segment: A REST path segment.
:type path_segment: ``string``
:param object: The object to be PUT.
:type object: ``string``
:param owner: The owner context of the namespace (optional).
:type owner: ``string``
:param app: The app context of the namespace (optional).
:type app: ``string``
:param sharing: The sharing mode of the namespace (optional).
:type sharing: ``string``
:param headers: List of extra HTTP headers to send (optional).
:type headers: ``list`` of 2-tuples.
:param query: All other keyword arguments, which are used as query
parameters.
:param body: Parameters to be used in the post body. If specified,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:param body: Parameters to be used in the post body. If specified,
:param body: Parameters to be used in the patch body. If specified,

any parameters in the query will be applied to the URL instead of
the body. If a dict is supplied, the key-value pairs will be form
encoded. If a string is supplied, the body will be passed through
in the request unchanged.
:type body: ``dict`` or ``str``
:return: The response from the server.
:rtype: ``dict`` with keys ``body``, ``headers``, ``reason``,
and ``status``

**Example**::

c = binding.connect(...)
c.post('saved/searches', name='boris',
search='search * earliest=-1m | head 1') == \\
{'body': ...a response reader object...,
'headers': [('content-length', '10455'),
('expires', 'Fri, 30 Oct 1998 00:00:00 GMT'),
('server', 'Splunkd'),
('connection', 'close'),
('cache-control', 'no-store, max-age=0, must-revalidate, no-cache'),
('date', 'Fri, 11 May 2012 16:46:06 GMT'),
('content-type', 'text/xml; charset=utf-8')],
'reason': 'Created',
'status': 201}
c.post('nonexistant/path') # raises HTTPError
c.logout()
# raises AuthenticationError:
c.patch('saved/searches/boris',
search='search * earliest=-1m | head 1')
Comment on lines +1000 to +1017
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, lets change it, as above.

"""
if headers is None:
headers = []

path = self.authority + self._abspath(path_segment, owner=owner, app=app, sharing=sharing) + f"/{object}"

logger.debug("PATCH request to %s (body: %s)", path, mask_sensitive_data(query))
all_headers = headers + self.additional_headers + self._auth_headers
response = self.http.patch(path, all_headers, **query)
return response


@_authentication
@_log_duration
def request(
Expand Down Expand Up @@ -942,8 +1107,9 @@ def request(
body = _encode(**body)

if method == "GET":
path = path + UrlEncoded("?" + body, skip_encode=True)
message = {"method": method, "headers": all_headers}
path = path + UrlEncoded('?' + body, skip_encode=True)
message = {'method': method,
'headers': all_headers}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drop that change. It seems to only make the code non-formatted.

else:
message = {"method": method, "headers": all_headers, "body": body}
else:
Expand Down Expand Up @@ -1301,6 +1467,40 @@ def __init__(
self.retries = retries
self.retryDelay = retryDelay

def _prepare_request_body_and_url(self, url, headers, **kwargs):
"""Helper function to prepare the request body and URL.

:param url: The URL.
:type url: ``string``
:param headers: A list of pairs specifying the headers for the HTTP request.
:type headers: ``list``
:param kwargs: Additional keyword arguments (optional).
:type kwargs: ``dict``
:returns: A tuple containing the updated URL, headers, and body.
:rtype: ``tuple``
"""
if headers is None:
headers = []

# We handle GET-style arguments and an unstructured body. This is here
# to support the receivers/stream endpoint.
if 'body' in kwargs:
# We only use application/x-www-form-urlencoded if there is no other
# Content-Type header present. This can happen in cases where we
# send requests as application/json, e.g. for KV Store.
if len([x for x in headers if x[0].lower() == "content-type"]) == 0:
headers.append(("Content-Type", "application/x-www-form-urlencoded"))

body = kwargs.pop('body')
if isinstance(body, dict):
body = _encode(**body).encode('utf-8')
if len(kwargs) > 0:
url = url + UrlEncoded('?' + _encode(**kwargs), skip_encode=True)
else:
body = _encode(**kwargs).encode('utf-8')

return url, headers, body

def delete(self, url, headers=None, **kwargs):
"""Sends a DELETE request to a URL.

Expand Down Expand Up @@ -1375,28 +1575,66 @@ def post(self, url, headers=None, **kwargs):
its structure).
:rtype: ``dict``
"""
if headers is None:
headers = []
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
message = {
'method': "POST",
'headers': headers,
'body': body
}
return self.request(url, message)

# We handle GET-style arguments and an unstructured body. This is here
# to support the receivers/stream endpoint.
if "body" in kwargs:
# We only use application/x-www-form-urlencoded if there is no other
# Content-Type header present. This can happen in cases where we
# send requests as application/json, e.g. for KV Store.
if len([x for x in headers if x[0].lower() == "content-type"]) == 0:
headers.append(("Content-Type", "application/x-www-form-urlencoded"))
def put(self, url, headers=None, **kwargs):
"""Sends a PUT request to a URL.

body = kwargs.pop("body")
if isinstance(body, dict):
body = _encode(**body).encode("utf-8")
if len(kwargs) > 0:
url = url + UrlEncoded("?" + _encode(**kwargs), skip_encode=True)
else:
body = _encode(**kwargs).encode("utf-8")
message = {"method": "POST", "headers": headers, "body": body}
:param url: The URL.
:type url: ``string``
:param headers: A list of pairs specifying the headers for the HTTP
response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``).
:type headers: ``list``
:param kwargs: Additional keyword arguments (optional). If the argument
is ``body``, the value is used as the body for the request, and the
keywords and their arguments will be URL encoded. If there is no
``body`` keyword argument, all the keyword arguments are encoded
into the body of the request in the format ``x-www-form-urlencoded``.
:type kwargs: ``dict``
:returns: A dictionary describing the response (see :class:`HttpLib` for
its structure).
:rtype: ``dict``
"""
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
message = {
'method': "PUT",
'headers': headers,
'body': body
}
return self.request(url, message)

def patch(self, url, headers=None, **kwargs):
"""Sends a PATCH request to a URL.

:param url: The URL.
:type url: ``string``
:param headers: A list of pairs specifying the headers for the HTTP
response (for example, ``[('Content-Type': 'text/cthulhu'), ('Token': 'boris')]``).
:type headers: ``list``
:param kwargs: Additional keyword arguments (optional). If the argument
is ``body``, the value is used as the body for the request, and the
keywords and their arguments will be URL encoded. If there is no
``body`` keyword argument, all the keyword arguments are encoded
into the body of the request in the format ``x-www-form-urlencoded``.
:type kwargs: ``dict``
:returns: A dictionary describing the response (see :class:`HttpLib` for
its structure).
:rtype: ``dict``
"""
url, headers, body = self._prepare_request_body_and_url(url, headers, **kwargs)
message = {
'method': "PATCH",
'headers': headers,
'body': body
}
return self.request(url, message)

def request(self, url, message, **kwargs):
"""Issues an HTTP request to a URL.

Expand Down
Loading