-
Notifications
You must be signed in to change notification settings - Fork 49
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
Is there a way to access fault data on API requests, instead of returning None
?
#54
Comments
Hi, did you try with the latest release and setting |
@Chavithra Thanks for the tip, will try later this week. |
@Chavithra Best wishes for the New Year. I've tried the
These seem to match with the following code section: degiro-connector/degiro_connector/trading/actions/action_confirm_order.py Lines 140 to 148 in e950396
It consists of 3 calls to
Also the received dict from
Update: I now understand that It's far more easy to perform fault handling on error codes (such as different HTTP status codes), than on (localised) error messages. Would it be an objection for you to add the status code to the raw response? response_dict = {"status_code": status_code, "text": response_raw.json()} To contain both properties of the
Another benefit of this would be that instead of polluting the Terminal with 3 times I'm happy to assist you and also update the docu. |
Hi @funnel20, Thanks Happy New Year to you too ! How about returning one of the following ?
I think the Also I am wondering :
|
@Chavithra Having that said, most users are probably making apps to automate their trading. Meaning they put their money at risk. I've thought about possible solutions to minimise the migration impact, which allow to supply a migration script, as you have done previously for breaking changes. Current situationFirst I'd like to have a proper understanding of the meaning of parameter
This current implementation does not allow proper fault handling, since both options for
Not all API calls support parameter ProposalI'm new to Python, but am an iOS developer for 13 years. In iOS it's common that most API calls return 2 objects:
Only one of the 2 contains data depending of a successful or erroneous result, the other is always Based on this, my proposal would be the following, as indicated by this pseudo-code example: def confirm_order(raw: bool = False) -> Tuple[Any, dict]:
"""
When `raw` is set to `False` (default), a Tuple is returned with 2 arguments:
`response`
`error`
On a successful call `response` contains the requested data, while `error` is `None`.
When a fault occurs, `response` is `None` and `error` contains the details.
When `raw` is set to `True`, the `requests.Response` object is returned as single output.
This also contains error data, so no need for a separate argument.
"""
# Make HTTP request to DeGiro:
requests = ...
# Return result:
if raw:
# Return the entire `Response`` object as only argument:
return requests.Response
else:
if requests.Response.status_code == 200:
# When OK, parse `Response` and return as first dict, while setting error argument to None:
return {"order_id" : "123456"}, None
else:
# Parse DeGiro fault data from body (or the `ConnectionError` in case there is even no response) and return as second dict. The response dict should be None:
return None, {"status_code" : requests.Response.status_code, "error" : "Some error from DeGiro"} Of course we can discuss the exact content of the new For the default response, error = confirm_order()
if response:
print(response)
# Do the happy flow actions
else:
print(error)
# Handle the error For response = confirm_order(raw=True)
status_code = getattr(response, "status_code")
if status_code == 200:
print(response.json())
# Do the happy flow actions
else:
print(status_code)
# Handle the error Migration GuideThis proposed new output is close to the existing implementation for both values of
Since these migration steps are straightforward, I expect that both can be applied in a migration script to help your users migrate automatically. NoteWhen you agree upon expanding the default output for |
The pythonic way to do error handling would be to throw an exception, and the exception object could contain the status code and error message. An alternative to providing a migration script could be to have exceptions enabled by an option, and log a warning message when the option is not enabled (i.e. "Deprecation warning: error handling with exceptions not enabled, see [some documentation link]"). Then, in perhaps 2-3 months, make exceptions the default and remove the optional argument. |
Thanks @e9henrik for your better suggestion. Like mentioned I'm new to Python, so if exceptions are the way to go; perfect. |
Sure. Exceptions are regular python objects. See for example https://www.programiz.com/python-programming/user-defined-exception |
Awesome, thanks for teaching me something new this morning. |
Great idea. This would even prevent breaking changes. It can just be a boolean that defaults to |
Hi @Chavithra , what do you think of the solution by @e9henrik with exceptions and an additional input parameter to prevent breaking changes (and a migration guide/script)? |
Hi @funnel20, I think its a good idea. I won't have the time this month to implement it. Feel free to join me on Thanks |
Hi @Chavithra , I don't have (or want) Discord. Since we're no longer talking about breaking changes, is it an idea that we can formulate the requirements here. Once we have mutual understanding and an agreement, I'm happy to code a PR for just 1 function, e.g. Trading API |
Hello @funnel20, alright will try to formulate a specification tonight (or during the week if I can't tonight). |
Here is my requirement proposal. CurrentlyWhen an action fails we are :
New behaviourWe will add a new parameter to an action. When this parameter is enabled, on action fail we will :
When this parameter is disabled, we will :
The raised
This parameter must be disabled by default (for now) : to allow seamless update for projects using the current version of the library. |
Hi @Chavithra , this looks good, especially since it will be a seamless update for exiting projects. Two questions:
|
Hi @funnel20,
|
|
I've been prototyping today and discovered that the exceptions that are raised by the Proposed changesBased on the existing code of version 2.0.14 (for example for class API:
def get_data(self, raise_exception:bool=False) -> bool:
try:
# Make GET request to server and use JSON response body:
response_raw = requests.request("GET", "https://api.github.com/users/google")
response_raw.raise_for_status()
response_dict = response_raw.json()
except requests.exceptions.HTTPError as exception:
self.handle_exception(exception, raise_exception, True)
return None
except Exception as exception:
self.handle_exception(exception, raise_exception)
return None
# Do something with `response_dict`...
return response_raw.status_code == 200 Notes
These 2 exceptions are handled generically in method
def handle_exception(self, exception:Exception, raise_exception:bool, is_http_error:bool=False):
# Show details in terminal.
# This is the current behaviour of version 2.0.14, see: https://github.com/b-aeby/degiro-connector/blob/c9a9a236eb89a50f0141a16603d028eaef3e8c58/degiro_connector/trading/actions/action_update_order.py#L126-L129
if is_http_error:
status_code = getattr(exception.response, "status_code", "No status_code found.")
text = getattr(exception.response, "text", "No text found.")
logging.error(status_code)
logging.error(text)
else:
logging.error(exception)
if raise_exception:
# Forward standard exceptions, such as `HTTPError`, `ConnectTimeout`, `ConnectionError` and `JSONDecodeError`:
raise(exception)
else:
# Migration idea: Teach user about new feature:
logging.info("Tip: set property `raise_exception` to `True` when calling this function and catch these fault details as exception `{0}`.".format(ExceptionUtils.get_full_class_name(exception))) Notes
Existing usageExisting users will still use the function call without argument: # Init API:
api = API()
# Make an API call:
my_data = api.get_data()
logging.info("Received data from server:\n{0}".format(my_data)) This will either:
New usageAdd parameter # Init API:
api = API()
# Make an API call:
try:
my_data = api.get_data(raise_exception=True)
logging.info("Received data from server:\n{0}".format(my_data))
except requests.exceptions.HTTPError as exception:
logging.error("HTTPError: {0}".format(exception))
logging.error("Server responded with HTTP Status code {0} and {1} on {2} request.".format(ExceptionUtils.http_status_code(exception), exception.response, exception.request))
except requests.exceptions.ConnectTimeout as exception:
logging.error("ConnectTimeout: {0}".format(exception))
except requests.exceptions.ConnectionError as exception:
logging.error("ConnectionError: {0}".format(exception))
except Exception as exception:
logging.error("Exception: Failed to connect to server with exception: {0}".format(exception)) This will either:
Notes
Working example codeHere is the code for a working example. It shows the current output in the Terminal, and the new output when using import logging
import requests
class ExceptionUtils:
def get_full_class_name(obj) -> str:
"""
Returns the full class name of an object.
Example
-------
Get the full class name of a specific `Exception`, for instance from the `requests` library:
```
try:
response = requests.request("GET", "https://wwz.google.com")
except Exception as exception:
print(get_full_class_name(exception))
```
This will print `requests.exceptions.ConnectionError`.
"""
# See: https://stackoverflow.com/a/58045927/2439941
module = obj.__class__.__module__
if module is None or module == str.__class__.__module__:
return obj.__class__.__name__
return module + '.' + obj.__class__.__name__
def http_status_code(exception:Exception) -> int:
"""
Convenience method to return the HTTP reponse status code from an `Exception`.
When there is no `response` object or it has no information about the status code, `0` is returned.
For the description of the HTTP status codes, see: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes.
"""
if hasattr(exception, 'response') and hasattr(exception.response, 'status_code'):
return exception.response.status_code
else:
return 0
class API:
def get_data(self, raise_exception:bool=False) -> bool:
try:
# To force the following exceptions:
# - HTTPError : change method from "GET" to "POST"
# - ConnectionError : change "github" in `url` to "git44hub"
# - ConnectTimeout : change `timeout` from 30.001 to 0.001
# - JSONDecodeError : change `url` from "https://api.github.com/users/google" to "https://api.github.com/zen"
# Make GET request to server and use JSON response body:
response_raw = requests.request("GET", url="https://api.github.com/users/google", timeout=30.001)
response_raw.raise_for_status()
response_dict = response_raw.json()
except requests.exceptions.HTTPError as exception:
self.handle_exception(exception, raise_exception, True)
return None
except Exception as exception:
self.handle_exception(exception, raise_exception)
return None
# Do something with `response_dict`...
return response_raw.status_code == 200
def handle_exception(self, exception:Exception, raise_exception:bool, is_http_error:bool=False):
# Show details in terminal.
# This is the current behaviour of version 2.0.14, see: https://github.com/b-aeby/degiro-connector/blob/c9a9a236eb89a50f0141a16603d028eaef3e8c58/degiro_connector/trading/actions/action_update_order.py#L126-L129
if is_http_error:
status_code = getattr(exception.response, "status_code", "No status_code found.")
text = getattr(exception.response, "text", "No text found.")
logging.error(status_code)
logging.error(text)
else:
logging.error(exception)
if raise_exception:
# Forward standard exceptions, such as `HTTPError`, `ConnectTimeout`, `ConnectionError` and `JSONDecodeError`:
raise(exception)
else:
# Migration idea: Teach user about new feature:
logging.info("Tip: set property `raise_exception` to `True` when calling this function and catch these fault details as exception `{0}`.".format(ExceptionUtils.get_full_class_name(exception)))
def main():
# SETUP LOGGING LEVEL
logging.basicConfig(level=logging.INFO)
# Init API:
api = API()
# Make an API call:
# 1) Seamless migration for existing usage without the new optional `raise_exception`
my_data = api.get_data()
logging.info("Received data from server:\n{0}".format(my_data))
# 2) New usage with the new optional `raise_exception` to parse all kind of faults:
logging.info("Use `raise_exception`")
try:
my_data = api.get_data(raise_exception=True)
logging.info("Received data from server:\n{0}".format(my_data))
except requests.exceptions.HTTPError as exception:
logging.error("HTTPError: {0}".format(exception))
logging.error("Server responded with HTTP Status code {0} and {1} on {2} request.".format(ExceptionUtils.http_status_code(exception), exception.response, exception.request))
except requests.exceptions.ConnectTimeout as exception:
logging.error("ConnectTimeout: {0}".format(exception))
except requests.exceptions.ConnectionError as exception:
logging.error("ConnectionError: {0}".format(exception))
except Exception as exception:
logging.error("Exception: Failed to connect to server with exception: {0}".format(exception))
# Run main
if __name__ == "__main__":
main() @Chavithra Please let me know what you think about this. |
That's a great work @funnel20 ! |
Is it possible to share more examples on the degiro api? How you ise it? I would love to integrate it with backtrader, any ideas how to do that? Regards |
@samardzicd Please respect the GitHub rules by commenting on-topic to an issue. You ask a general question that has nothing to do with this particular issue. |
My bad, will respect the process. |
@funnel20 is right : opening another issue for theses points will help the tracking and make sure I won't forget about it Will love to have this repo used in Let us know which examples you would like (in another issue). |
@funnel20 seems good to me. I think this should be implemented in an actual PR for instance for
Might also be able to add
Thanks |
@Chavithra Awesome. I'd love to create a PR.
|
* Corrected Pythonian names * Added missing `free_space_new`
* Corrected Pythonian names
Removed duplicate `free_space_new`
@Chavithra Any comments on my proposal would be appreciated. And if you agree, please answer my questions on how to integrate this in the current project. |
For example the Trading API might return a negative response while creating or updating orders, see for example the error response in #50 (comment)
However, this is only observed in the CRITICAL Logging.
The functions
check_order()
,confirm_order()
andupdate_order()
will return justNone
in case of an error.It's important to know what the cause of the error is. Is it for example an error with the order (such as the example above), which is refused by DeGiro?
Or is it caused by a server error (HTTP Status codes) or even no internet connection (HTTP Connection error)?
This data seems already present in the underlying code of the Connector, as can be seen in the Logging messages.
Would it be possible to use this in the response of the functions?
For example the standard output of these functions can be a dict with 3 parameters. Here the response of a successful request:
where
result
can also contain a dict, for example with the current response fromcheck_order()
In case of a DeGiro error, it will look like:
In case of a Network error, it will look like:
This information is useful for our script, to decide what fault action to take. E.g.
Please let me know if this is possible.
The text was updated successfully, but these errors were encountered: