Skip to content
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

Exit synadm early on fatal errors, refactor & fix query() #168

Open
wants to merge 23 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4183d86
Exit synadm on fatal requests exceptions
JOJ0 Jan 24, 2025
ce1e977
Sort api module imports with isort
JOJ0 Jan 24, 2025
18b4991
Refactor query args handling more pythonic
JOJ0 Jan 24, 2025
a66f894
Refactor query kwargs handling more pythonic
JOJ0 Jan 24, 2025
dd2c2ec
Move query args before keyword args to please linter
JOJ0 Jan 24, 2025
2a06f91
Add query function type hints, return only
JOJ0 Jan 24, 2025
8fc2a70
Add query function type hints, arguments only
JOJ0 Jan 24, 2025
c5093f7
Fix query function docstring (arguments only)
JOJ0 Jan 24, 2025
5524e71
fix ConnectionError not being caught
JacksonChen666 Jan 29, 2025
11c1cff
add optional messages to log_fatal_exit
JacksonChen666 Jan 29, 2025
0f9bcd0
more detailed message for connection error
JacksonChen666 Jan 29, 2025
4f8a9f8
don't catch InvalidURL or MissingSchema exceptions
JacksonChen666 Jan 29, 2025
e1324c2
print traceback upon log_fatal_exit
JacksonChen666 Jan 29, 2025
0f42b30
separate message from error logging
JacksonChen666 Jan 29, 2025
67f3b9f
add args docs
JacksonChen666 Jan 29, 2025
0a6ab35
Apply suggestions from code review
JacksonChen666 Jan 31, 2025
dcade53
Apply another suggestion
JacksonChen666 Jan 31, 2025
7df42c9
reword format_exception string join part
JacksonChen666 Jan 31, 2025
5f76e98
fix typo
JacksonChen666 Jan 31, 2025
252d599
rewrite return section of docs for query api
JacksonChen666 Jan 31, 2025
4bb093d
Explicitely catch requests ConnectionError
JOJ0 Jan 31, 2025
8fa1503
Sort imports again with isort
JOJ0 Jan 31, 2025
cfa5d15
make format_exception work with python 3.9
JacksonChen666 Feb 7, 2025
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
95 changes: 67 additions & 28 deletions synadm/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,43 @@
https://matrix.org/docs/spec/#matrix-apis.
"""

import requests
from http.client import HTTPConnection
import datetime
import json
import urllib.parse
import re
import traceback
import urllib.parse
from http.client import HTTPConnection
from typing import Any, Dict, List, Optional, Union

import requests


def log_fatal_exit(error, logger, message=None):
"""Log a fatal error and exit synadm.

Args:
error: A Python exception.
logger: A Python logger, with info and fatal methods
message: Message to use instead of "synadm exited due to a fatal
error."
"""
if message is None:
message = "synadm exited due to a fatal error."

# format_exception() returns a list of strings (with new lines). join it
# into a single string again for readability.
JOJ0 marked this conversation as resolved.
Show resolved Hide resolved
#
# error specified multiple times to ensure it works with python 3.9 and
# versions after that which accept things differently
logger.info("".join(traceback.format_exception(error, error,
error.__traceback__)))

logger.fatal(
"%s: %s.\n\n%s",
type(error).__name__,
error, message
)
raise SystemExit(1) from error


class ApiRequest:
Expand Down Expand Up @@ -73,44 +104,49 @@ def __init__(self, log, user, token, base_url, path, timeout, debug,
HTTPConnection.debuglevel = 1
self.verify = verify

def query(self, method, urlpart, params=None, data=None, token=None,
base_url_override=None, verify=None, *args, **kwargs):
def query(
self,
method: str,
urlpart: str,
*args: Any,
params: Optional[Dict[str, Any]] = None,
data: Optional[Dict[str, Any]] = None,
token: Optional[str] = None,
base_url_override: Optional[bool] = False,
verify: Optional[bool] = True,
**kwargs: Dict[str, Any],
) -> Optional[Union[Dict[str, Any], List[Dict[str, Any]]]]:
"""Generic wrapper around requests methods.

Handles requests methods, logging and exceptions, and URL encoding.

Args:
urlpart (string): The path to the API endpoint, excluding
self.base_url and self.path (the part after
proto://fqdn:port/path). It will be passed to Python's
str.format, so the string should not be already formatted
(as f-strings or with str.format) as to sanitize the URL.
params (dict, optional): URL parameters (?param1&param2). Defaults
to None.
data (dict, optional): Request body used in POST, PUT, DELETE
requests. Defaults to None.
base_url_override (bool): The default setting of self.base_url set
method: The http method to use (get, post, put, ...)
urlpart: The path to the API endpoint, excluding self.base_url and
self.path (the part after proto://fqdn:port/path). It will be
passed to Python's str.format, so the string should not be
already formatted (as f-strings or with str.format) as to
sanitize the URL.
params: URL parameters (?param1&param2)..
data: Request body used in POST, PUT, DELETE requests.
token: An optional token overriding the configured one.
base_url_override: The default setting of self.base_url set
on initialization can be overwritten using this argument.
verify(bool): Mandatory SSL verification is turned on by default
and can be turned off using this method.
verify: Mandatory SSL verification is on by default and can be
turned off using this method.
*args: Arguments that will be URL encoded and passed to Python's
str.format.
**kwargs: Keyword arguments that will be URL encoded (only the
values) and passed to Python's str.format.

Returns:
string or None: Usually a JSON string containing
the response of the API; responses that are not 200(OK) (usally
error messages returned by the API) will also be returned as
JSON strings. On exceptions the error type and description are
logged and None is returned.
A dict, list or None. If it's a dict or list, it is a JSON
decoded response. Whether it is a dict or a list depends on what
the server responds with. It returns a None if any exceptions
are encountered.
"""
args = list(args)
kwargs = dict(kwargs)
for i in range(len(args)):
args[i] = urllib.parse.quote(args[i], safe="")
for i in kwargs.keys():
kwargs[i] = urllib.parse.quote(kwargs[i], safe="")
args = [urllib.parse.quote(arg, safe="") for arg in args]
kwargs = {k: urllib.parse.quote(v, safe="") for k, v in kwargs.items()}
urlpart = urlpart.format(*args, **kwargs)

if base_url_override:
Expand Down Expand Up @@ -139,6 +175,9 @@ def query(self, method, urlpart, params=None, data=None, token=None,
self.log.warning(f"{host_descr} returned status code "
f"{resp.status_code}")
return resp.json()
except requests.exceptions.ConnectionError as error:
log_fatal_exit(error, self.log, "Connection error. Please check "
"that Synapse can be reached.")
except Exception as error:
self.log.error("%s while querying %s: %s",
type(error).__name__, host_descr, error)
Expand Down