-
Notifications
You must be signed in to change notification settings - Fork 384
Adding helper functions for convenient PUT and PATCH on SDK #586
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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. | ||||||
|
|
||||||
| :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, | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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( | ||||||
|
|
@@ -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} | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: | ||||||
|
|
@@ -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. | ||||||
|
|
||||||
|
|
@@ -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. | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
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.