gooddata-flight-server 1.34.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of gooddata-flight-server might be problematic. Click here for more details.

Files changed (49) hide show
  1. gooddata_flight_server/__init__.py +23 -0
  2. gooddata_flight_server/_version.py +7 -0
  3. gooddata_flight_server/cli.py +137 -0
  4. gooddata_flight_server/config/__init__.py +1 -0
  5. gooddata_flight_server/config/config.py +536 -0
  6. gooddata_flight_server/errors/__init__.py +1 -0
  7. gooddata_flight_server/errors/error_code.py +209 -0
  8. gooddata_flight_server/errors/error_info.py +475 -0
  9. gooddata_flight_server/exceptions.py +16 -0
  10. gooddata_flight_server/health/__init__.py +1 -0
  11. gooddata_flight_server/health/health_check_http_server.py +103 -0
  12. gooddata_flight_server/health/server_health_monitor.py +83 -0
  13. gooddata_flight_server/metrics.py +16 -0
  14. gooddata_flight_server/py.typed +1 -0
  15. gooddata_flight_server/server/__init__.py +1 -0
  16. gooddata_flight_server/server/auth/__init__.py +1 -0
  17. gooddata_flight_server/server/auth/auth_middleware.py +83 -0
  18. gooddata_flight_server/server/auth/token_verifier.py +62 -0
  19. gooddata_flight_server/server/auth/token_verifier_factory.py +55 -0
  20. gooddata_flight_server/server/auth/token_verifier_impl.py +41 -0
  21. gooddata_flight_server/server/base.py +63 -0
  22. gooddata_flight_server/server/default.logging.ini +28 -0
  23. gooddata_flight_server/server/flight_rpc/__init__.py +1 -0
  24. gooddata_flight_server/server/flight_rpc/flight_middleware.py +162 -0
  25. gooddata_flight_server/server/flight_rpc/flight_server.py +228 -0
  26. gooddata_flight_server/server/flight_rpc/flight_service.py +279 -0
  27. gooddata_flight_server/server/flight_rpc/server_methods.py +200 -0
  28. gooddata_flight_server/server/server_base.py +321 -0
  29. gooddata_flight_server/server/server_main.py +116 -0
  30. gooddata_flight_server/tasks/__init__.py +1 -0
  31. gooddata_flight_server/tasks/base.py +21 -0
  32. gooddata_flight_server/tasks/metrics.py +115 -0
  33. gooddata_flight_server/tasks/task.py +193 -0
  34. gooddata_flight_server/tasks/task_error.py +60 -0
  35. gooddata_flight_server/tasks/task_executor.py +96 -0
  36. gooddata_flight_server/tasks/task_result.py +363 -0
  37. gooddata_flight_server/tasks/temporal_container.py +247 -0
  38. gooddata_flight_server/tasks/thread_task_executor.py +639 -0
  39. gooddata_flight_server/utils/__init__.py +1 -0
  40. gooddata_flight_server/utils/libc_utils.py +35 -0
  41. gooddata_flight_server/utils/logging.py +158 -0
  42. gooddata_flight_server/utils/methods_discovery.py +98 -0
  43. gooddata_flight_server/utils/otel_tracing.py +142 -0
  44. gooddata_flight_server-1.34.1.dev1.data/scripts/gooddata-flight-server +10 -0
  45. gooddata_flight_server-1.34.1.dev1.dist-info/LICENSE.txt +7 -0
  46. gooddata_flight_server-1.34.1.dev1.dist-info/METADATA +749 -0
  47. gooddata_flight_server-1.34.1.dev1.dist-info/RECORD +49 -0
  48. gooddata_flight_server-1.34.1.dev1.dist-info/WHEEL +5 -0
  49. gooddata_flight_server-1.34.1.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1 @@
1
+ # (C) 2024 GoodData Corporation
@@ -0,0 +1,103 @@
1
+ # (C) 2024 GoodData Corporation
2
+ from functools import partial
3
+ from http import HTTPStatus
4
+ from http.server import BaseHTTPRequestHandler, HTTPServer
5
+ from threading import Thread
6
+ from typing import Any
7
+
8
+ import structlog
9
+
10
+ from gooddata_flight_server.health.server_health_monitor import (
11
+ ModuleHealthStatus,
12
+ ServerHealthMonitor,
13
+ )
14
+
15
+ LIVENESS_ENDPOINT_PATH = "/live"
16
+ READINESS_ENDPOINT_PATH = "/ready"
17
+
18
+ SERVER_MODULE_DEBUG_NAME = "server"
19
+
20
+ LOGGER = structlog.get_logger("gooddata_flight_server.health_check_http_server")
21
+
22
+
23
+ class _HealthCheckHandler(BaseHTTPRequestHandler):
24
+ def __init__(
25
+ self,
26
+ *args: Any,
27
+ server_health_monitor: ServerHealthMonitor,
28
+ **kwargs: Any,
29
+ ):
30
+ self._server_health_monitor = server_health_monitor
31
+ super().__init__(*args, **kwargs)
32
+
33
+ def log_message(self, format: str, *args: list) -> None:
34
+ pass
35
+
36
+ def do_GET(self) -> None: # noqa: N802
37
+ if self.path == READINESS_ENDPOINT_PATH:
38
+ self.send_response(HTTPStatus.NO_CONTENT if self.is_ready() else HTTPStatus.INTERNAL_SERVER_ERROR)
39
+ elif self.path == LIVENESS_ENDPOINT_PATH:
40
+ self.send_response(HTTPStatus.NO_CONTENT if self.is_alive() else HTTPStatus.INTERNAL_SERVER_ERROR)
41
+ else:
42
+ self.send_response(HTTPStatus.NOT_FOUND)
43
+ self.end_headers()
44
+
45
+ def is_ready(self) -> bool:
46
+ """
47
+ Readiness check inspecting flight server startup state.
48
+
49
+ :return: True if instance is ready otherwise False
50
+ """
51
+ LOGGER.debug("checking_flight_server_ready")
52
+ if (
53
+ SERVER_MODULE_DEBUG_NAME in self._server_health_monitor.module_statuses
54
+ and self._server_health_monitor.module_statuses[SERVER_MODULE_DEBUG_NAME] == ModuleHealthStatus.OK
55
+ ):
56
+ LOGGER.debug("flight_server_ready")
57
+ return True
58
+ else:
59
+ LOGGER.warning("flight_server_not_ready")
60
+ return False
61
+
62
+ def is_alive(self) -> bool:
63
+ """
64
+ Liveness check inspecting all running modules statuses.
65
+
66
+ :return: True if server is healthy otherwise False
67
+ """
68
+ LOGGER.debug("checking_flight_server_healthy")
69
+
70
+ for (
71
+ module,
72
+ status,
73
+ ) in self._server_health_monitor.module_statuses.items():
74
+ if status == ModuleHealthStatus.NOT_OK:
75
+ LOGGER.warning("unhealthy_module", module=module)
76
+ return False
77
+
78
+ LOGGER.debug("flight_server_healthy")
79
+ return True
80
+
81
+
82
+ class HealthCheckHttpServer:
83
+ """
84
+ HTTP server implementation for health checks mainly usable in k8s environment for readiness
85
+ and liveness probes. Exposes $LIVENESS_ENDPOINT_PATH and $READINESS_ENDPOINT_PATH returning HTTP 200 in
86
+ case of success otherwise HTTP 500.
87
+ """
88
+
89
+ def __init__(
90
+ self,
91
+ host: str,
92
+ port: int,
93
+ server_health_monitor: ServerHealthMonitor,
94
+ ):
95
+ handler = partial(_HealthCheckHandler, server_health_monitor=server_health_monitor)
96
+ httpd = HTTPServer((host, port), handler)
97
+
98
+ def serve_forever(httpd_instance: HTTPServer) -> None:
99
+ with httpd_instance:
100
+ LOGGER.info("health_check_started", host=host, port=port)
101
+ httpd_instance.serve_forever()
102
+
103
+ Thread(target=serve_forever, args=(httpd,), daemon=True).start()
@@ -0,0 +1,83 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import enum
3
+ import threading
4
+ import time
5
+
6
+ import structlog
7
+
8
+ from gooddata_flight_server.metrics import ServerMetrics
9
+ from gooddata_flight_server.utils.libc_utils import LibcUtils
10
+
11
+
12
+ class ModuleHealthStatus(enum.Enum):
13
+ """
14
+ This enum lists health status of a module
15
+ """
16
+
17
+ OK = "ok"
18
+ NOT_OK = "not_ok"
19
+
20
+
21
+ class ServerHealthMonitor:
22
+ """
23
+ Server health monitor and maintenance.
24
+
25
+ The monitor includes a thread doing regular maintenance - namely periodically performing
26
+ malloc trim() to make system throw away the garbage. This is essential to survive in runtime environments
27
+ that impose memory (RSS) limits and kill the server if it exceeds it - the malloc does not
28
+ free the used memory back to the system; the RSS keeps growing and growing until the server
29
+ gets killed. The trim() call makes malloc drop all unneeded allocations.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ trim_interval: int = 30,
35
+ ) -> None:
36
+ self._logger = structlog.get_logger("gooddata_flight_server.maintenance")
37
+ self._libc = LibcUtils()
38
+ self._trim_interval = trim_interval
39
+ self._module_statuses: dict[str, ModuleHealthStatus] = {}
40
+
41
+ self._thread = threading.Thread(
42
+ name="gooddata_flight_server.maintenance",
43
+ target=self._maintenance,
44
+ daemon=True,
45
+ )
46
+ self._thread.start()
47
+
48
+ self._logger.info("server_health_monitor_started")
49
+
50
+ def _maintenance(self) -> None:
51
+ last_trim = time.time()
52
+
53
+ while True:
54
+ if time.time() - last_trim > self._trim_interval:
55
+ try:
56
+ trim_start = time.perf_counter()
57
+ self._libc.malloc_trim()
58
+ duration = time.perf_counter() - trim_start
59
+
60
+ ServerMetrics.TRIM_SUMMARY.observe(duration)
61
+ self._logger.debug(
62
+ "server_maintenance",
63
+ op="malloc_trim",
64
+ duration=duration,
65
+ )
66
+ except Exception:
67
+ ServerMetrics.TRIM_ERROR_COUNT.inc()
68
+ self._logger.error("malloc_trim_failed", exc_info=True)
69
+
70
+ last_trim = time.time()
71
+
72
+ time.sleep(1.0)
73
+
74
+ @property
75
+ def module_statuses(self) -> dict[str, ModuleHealthStatus]:
76
+ return self._module_statuses
77
+
78
+ def set_module_status(self, name: str, status: ModuleHealthStatus) -> None:
79
+ """
80
+ :param name: name of the module to which the status belongs
81
+ :param status: health status of the module
82
+ """
83
+ self._module_statuses[name] = status
@@ -0,0 +1,16 @@
1
+ # (C) 2024 GoodData Corporation
2
+
3
+ from prometheus_client import Counter, Summary
4
+
5
+
6
+ # TODO: metric prefix should be configurable
7
+ class ServerMetrics:
8
+ TRIM_SUMMARY = Summary(
9
+ "gdfs_malloc_trim",
10
+ "Summary of malloc trim call durations.",
11
+ )
12
+
13
+ TRIM_ERROR_COUNT = Counter(
14
+ "gdfs_malloc_trim_error",
15
+ "Number of times malloc_trim has failed. Repeated failures means big trouble incoming.",
16
+ )
@@ -0,0 +1 @@
1
+ Mark package as supporting typing. See https://www.python.org/dev/peps/pep-0561/ for details.
@@ -0,0 +1 @@
1
+ # (C) 2024 GoodData Corporation
@@ -0,0 +1 @@
1
+ # (C) 2024 GoodData Corporation
@@ -0,0 +1,83 @@
1
+ # (C) 2024 GoodData Corporation
2
+ from typing import Any, Optional
3
+
4
+ import pyarrow.flight
5
+ import structlog
6
+
7
+ from gooddata_flight_server.server.auth.token_verifier import (
8
+ TokenVerificationStrategy,
9
+ )
10
+
11
+
12
+ class TokenAuthMiddleware(pyarrow.flight.ServerMiddleware):
13
+ MiddlewareName = "auth_token"
14
+
15
+ def __init__(self, token: str, token_data: Any) -> None:
16
+ super().__init__()
17
+
18
+ self._token = token
19
+ self._token_data = token_data
20
+
21
+ @property
22
+ def token_data(self) -> Any:
23
+ return self._token_data
24
+
25
+
26
+ _DEFAULT_AUTH_TOKEN_HEADER = "Authorization"
27
+ _LOGGER = structlog.get_logger("gooddata_flight_server.auth")
28
+ _BEARER_END_IDX = 7
29
+
30
+
31
+ class TokenAuthMiddlewareFactory(pyarrow.flight.ServerMiddlewareFactory):
32
+ def __init__(
33
+ self,
34
+ token_header_name: Optional[str],
35
+ strategy: TokenVerificationStrategy,
36
+ ):
37
+ super().__init__()
38
+
39
+ self._token_header_name = token_header_name
40
+ self._strategy = strategy
41
+
42
+ def _extract_token(self, headers: dict[str, list[str]]) -> str:
43
+ def _auth_header_value(lookup: str) -> str:
44
+ _lookup = lookup.lower()
45
+ values = [value for header, values in headers.items() if header.lower() == _lookup for value in values]
46
+
47
+ if len(values) > 1:
48
+ raise pyarrow.flight.FlightUnauthenticatedError(
49
+ f"Authentication failed because the authentication header '{lookup}' is specified multiple times."
50
+ )
51
+
52
+ if not len(values):
53
+ raise pyarrow.flight.FlightUnauthenticatedError(
54
+ "Authentication failed because the authentication header bearing the token was not included "
55
+ "on the call."
56
+ )
57
+
58
+ return values[0]
59
+
60
+ if self._token_header_name is None:
61
+ token = _auth_header_value(_DEFAULT_AUTH_TOKEN_HEADER)
62
+ if not token.startswith("Bearer "):
63
+ raise pyarrow.flight.FlightUnauthenticatedError(
64
+ "Authentication failed because the 'Authorization' header does not start with 'Bearer '"
65
+ )
66
+
67
+ return token[_BEARER_END_IDX:].strip()
68
+
69
+ token = _auth_header_value(self._token_header_name)
70
+ return token.strip()
71
+
72
+ def start_call(self, info: pyarrow.flight.CallInfo, headers: dict[str, list[str]]) -> Optional[TokenAuthMiddleware]:
73
+ try:
74
+ token = self._extract_token(headers)
75
+ result = self._strategy.verify(call_info=info, token=token)
76
+
77
+ return TokenAuthMiddleware(token=token, token_data=result)
78
+ except pyarrow.flight.FlightUnauthenticatedError as e:
79
+ _LOGGER.info("authentication_failed", reason=str(e))
80
+ raise
81
+ except pyarrow.flight.FlightUnauthorizedError as e:
82
+ _LOGGER.info("authorization_failed", reason=str(e))
83
+ raise
@@ -0,0 +1,62 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import abc
3
+ from typing import Any
4
+
5
+ import pyarrow.flight
6
+
7
+ from gooddata_flight_server.server.base import ServerContext
8
+
9
+
10
+ class TokenVerificationStrategy(abc.ABC):
11
+ """
12
+ Token verification strategy is used by server's token authentication
13
+ middleware to perform the actual verification:
14
+
15
+ - The middleware is responsible for extracting the token
16
+ - The strategy is responsible for verifying that the token is valid and
17
+ if applicable return any information carried in the token.
18
+ - The middleware is responsible for holding onto value returned by the
19
+ verifier and make it available to call handling code.
20
+ """
21
+
22
+ @abc.abstractmethod
23
+ def verify(self, call_info: pyarrow.flight.CallInfo, token: str) -> Any:
24
+ """
25
+ Perform token verification.
26
+
27
+ - If the token is not valid, this method should raise either pyarrow.flight.FlightUnauthenticated
28
+ - If the token is valid but fails authorization for current call, this method
29
+ should raise pyarrow.Flight.FlightUnauthorized
30
+
31
+ Otherwise, the method should return either nothing or a custom value describing
32
+ the contents of the token - as it sees fit.
33
+
34
+ For example a JWT Token Verification strategy may extract claims and return them in a
35
+ dictionary.
36
+
37
+ :param call_info: current call info
38
+ :param token: token to verify
39
+ :return: either nothing or a custom value with info extracted from the token
40
+ :raises pyarrow.flight.FlightUnauthenticated - when the token is invalid / fails validation
41
+ :raises pyarrow.flight.FlightUnauthorized - when the token is valid but the caller holding
42
+ the token is not authorized to make the call
43
+ """
44
+ raise NotImplementedError
45
+
46
+ @classmethod
47
+ def create(cls, ctx: ServerContext) -> "TokenVerificationStrategy":
48
+ """
49
+ This method is called exactly once during the server startup to obtain
50
+ a singleton of the concrete verification strategy to be used by
51
+ the server.
52
+
53
+ A typical use case here is to inspect the settings included in the server
54
+ context and obtain any essential configuration from the `settings`.
55
+
56
+ The strategy _may_ use this opportunity to perform any long-running
57
+ and blocking one-time initialization.
58
+
59
+ :param ctx: server context where the verification strategy will be used
60
+ :return: an instance of the strategy.
61
+ """
62
+ return cls()
@@ -0,0 +1,55 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import importlib
3
+
4
+ import structlog
5
+
6
+ from gooddata_flight_server.exceptions import ServerStartupInterrupted
7
+ from gooddata_flight_server.server.auth.token_verifier import TokenVerificationStrategy
8
+ from gooddata_flight_server.server.auth.token_verifier_impl import EnumeratedTokenVerification
9
+ from gooddata_flight_server.server.base import ServerContext
10
+
11
+ _LOGGER = structlog.get_logger("gooddata_flight_server.auth")
12
+
13
+
14
+ def _import_verification_strategy(module_name: str) -> type[TokenVerificationStrategy]:
15
+ _LOGGER.info("load_token_verification", module_name=module_name)
16
+ module = importlib.import_module(module_name)
17
+
18
+ for member in module.__dict__.values():
19
+ if not isinstance(member, type) or not issubclass(member, TokenVerificationStrategy):
20
+ # filter out module members which are not classes that implement the
21
+ # TokenVerificationStrategy interface
22
+ continue
23
+
24
+ if member == TokenVerificationStrategy:
25
+ # the TokenVerificationStrategy class is likely imported in the module -
26
+ # don't want that to interfere
27
+ continue
28
+
29
+ return member
30
+
31
+ raise ServerStartupInterrupted(
32
+ f"The module '{module_name}' specified in 'token_verification' setting does not "
33
+ f"include an implementation of {TokenVerificationStrategy.__name__}."
34
+ )
35
+
36
+
37
+ def _find_token_verification_class(ctx: ServerContext) -> type[TokenVerificationStrategy]:
38
+ # config reader must ensure that there is always some value here
39
+ assert ctx.config.token_verification is not None
40
+
41
+ if ctx.config.token_verification == "EnumeratedTokenVerification":
42
+ return EnumeratedTokenVerification
43
+
44
+ return _import_verification_strategy(ctx.config.token_verification)
45
+
46
+
47
+ def create_token_verification_strategy(ctx: ServerContext) -> TokenVerificationStrategy:
48
+ try:
49
+ cls = _find_token_verification_class(ctx)
50
+
51
+ _LOGGER.info("auth_token_strategy_init", cls=cls.__name__)
52
+ return cls.create(ctx)
53
+ except Exception:
54
+ _LOGGER.critical("auth_token_init_failed", exc_info=True)
55
+ raise
@@ -0,0 +1,41 @@
1
+ # (C) 2024 GoodData Corporation
2
+ from typing import Any
3
+
4
+ import pyarrow.flight
5
+ from dynaconf import ValidationError
6
+
7
+ from gooddata_flight_server.server.auth.token_verifier import TokenVerificationStrategy
8
+ from gooddata_flight_server.server.base import ServerContext
9
+
10
+ _TOKEN_ENUMERATION_SECTION = "enumerated_tokens"
11
+ _TOKEN_SETTING = "tokens"
12
+
13
+
14
+ class EnumeratedTokenVerification(TokenVerificationStrategy):
15
+ """
16
+ A simple token verification strategy that successfully verifies
17
+ a token if it matches one of the tokens specified in the settings.
18
+ """
19
+
20
+ def __init__(self, allowed_tokens: set[str]) -> None:
21
+ self._tokens = allowed_tokens
22
+
23
+ def verify(self, call_info: pyarrow.flight.CallInfo, token: str) -> Any:
24
+ if token not in self._tokens:
25
+ raise pyarrow.flight.FlightUnauthenticatedError("Authentication token is not valid.")
26
+
27
+ return None
28
+
29
+ @classmethod
30
+ def create(cls, ctx: ServerContext) -> "TokenVerificationStrategy":
31
+ tokens = list(ctx.settings.get(f"{_TOKEN_ENUMERATION_SECTION}.{_TOKEN_SETTING}") or [])
32
+ if not len(tokens):
33
+ raise ValidationError(
34
+ f"The 'EnumeratedTokenVerification' requires that you configure "
35
+ f"which tokens are allowed to use. You have to include section "
36
+ f"[{EnumeratedTokenVerification}] with a '{_TOKEN_SETTING}' setting that contains "
37
+ f"list of tokens. Alternatively, you can specify environment variable "
38
+ f"GOODDATA_FLIGHT_ENUMERATED_TOKENS__TOKENS."
39
+ )
40
+
41
+ return EnumeratedTokenVerification(set(tokens))
@@ -0,0 +1,63 @@
1
+ # (C) 2024 GoodData Corporation
2
+ from dataclasses import dataclass
3
+ from typing import Protocol
4
+
5
+ import pyarrow.flight
6
+ from dynaconf import Dynaconf
7
+
8
+ from gooddata_flight_server.config.config import ServerConfig
9
+ from gooddata_flight_server.health.server_health_monitor import (
10
+ ServerHealthMonitor,
11
+ )
12
+ from gooddata_flight_server.server.flight_rpc.server_methods import (
13
+ FlightServerMethods,
14
+ )
15
+ from gooddata_flight_server.tasks.task_executor import TaskExecutor
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class ServerContext:
20
+ """
21
+ Server's context.
22
+ """
23
+
24
+ settings: Dynaconf
25
+ """
26
+ All settings parsed from configuration files and/or environment variables provided at server startup.
27
+ """
28
+
29
+ config: ServerConfig
30
+ """
31
+ Server's configuration
32
+ """
33
+
34
+ location: pyarrow.flight.Location
35
+ """
36
+ Server's Flight RPC location - this location connectable by clients.
37
+ """
38
+
39
+ health: ServerHealthMonitor
40
+ """
41
+ Server's health monitor. Components may register their health into the monitor.
42
+
43
+ This monitor is integrated with health check server where the overall status
44
+ is reported using liveness/readiness endpoints.
45
+
46
+ If a component is unhealthy, the whole server will be reported as such.
47
+ """
48
+
49
+ task_executor: TaskExecutor
50
+ """
51
+ Task Executor can and should be used to implement long running Tasks which generate
52
+ Flight data.
53
+ """
54
+
55
+
56
+ class FlightServerMethodsFactory(Protocol):
57
+ """
58
+ Factory function for server methods. This can be provided to the
59
+ GoodDataFlightServer - the server will invoke the function at the right
60
+ point and then integrate the FlightServerMethods into its Flight RPC service.
61
+ """
62
+
63
+ def __call__(self, ctx: ServerContext) -> FlightServerMethods: ...
@@ -0,0 +1,28 @@
1
+ # (C) 2024 GoodData Corporation
2
+ [loggers]
3
+ keys = root, gooddata_flight_server
4
+
5
+ [handlers]
6
+ keys = gooddata_flight_server_stream_handler
7
+
8
+ [formatters]
9
+ keys = gooddata_flight_server_formatter
10
+
11
+ [logger_root]
12
+ level = INFO
13
+ handlers = gooddata_flight_server_stream_handler
14
+
15
+ [logger_gooddata_flight_server]
16
+ level = INFO
17
+ qualname = gooddata_flight_server
18
+ handlers =
19
+
20
+
21
+ [handler_gooddata_flight_server_stream_handler]
22
+ class = StreamHandler
23
+ level = DEBUG
24
+ formatter = gooddata_flight_server_formatter
25
+ args = (sys.stderr,)
26
+
27
+ [formatter_gooddata_flight_server_formatter]
28
+ format = %(message)s
@@ -0,0 +1 @@
1
+ # (C) 2024 GoodData Corporation