gooddata-flight-server 1.29.2.dev2__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 +230 -0
  26. gooddata_flight_server/server/flight_rpc/flight_service.py +281 -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.29.2.dev2.data/scripts/gooddata-flight-server +10 -0
  45. gooddata_flight_server-1.29.2.dev2.dist-info/LICENSE.txt +7 -0
  46. gooddata_flight_server-1.29.2.dev2.dist-info/METADATA +737 -0
  47. gooddata_flight_server-1.29.2.dev2.dist-info/RECORD +49 -0
  48. gooddata_flight_server-1.29.2.dev2.dist-info/WHEEL +5 -0
  49. gooddata_flight_server-1.29.2.dev2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,158 @@
1
+ # (C) 2024 GoodData Corporation
2
+ """
3
+ Logging configuration helper functions
4
+ """
5
+
6
+ import os
7
+ from logging.config import fileConfig
8
+ from typing import Any, Optional, Union
9
+
10
+ import orjson
11
+ import structlog
12
+ from opentelemetry import trace
13
+ from structlog.typing import EventDict, WrappedLogger
14
+
15
+
16
+ def _resolve_config(logging_ini: str, for_module: Optional[str]) -> str:
17
+ if os.path.isabs(logging_ini):
18
+ return logging_ini
19
+ else:
20
+ conf_dir = os.path.dirname(for_module) if for_module is not None else os.path.curdir
21
+
22
+ return os.path.join(conf_dir, logging_ini)
23
+
24
+
25
+ class _ORJsonRenderer:
26
+ def __init__(self, output_bytes: bool = False):
27
+ self._output_bytes = output_bytes
28
+
29
+ @staticmethod
30
+ def default(obj: Any) -> Any:
31
+ # sets are not serializable by default, so convert them to lists
32
+ if isinstance(obj, set):
33
+ return list(obj)
34
+ # otherwise, do nothing, this will make the default logic to kick in
35
+
36
+ def __call__(self, logger: Any, name: str, event_dict: Any) -> Union[str, bytes]:
37
+ if self._output_bytes:
38
+ return orjson.dumps(event_dict, default=_ORJsonRenderer.default)
39
+
40
+ return orjson.dumps(event_dict, default=_ORJsonRenderer.default).decode("UTF-8")
41
+
42
+
43
+ class _OtelTraceContextInjector:
44
+ """
45
+ Injects trace id, span id and parent span id obtained from current OpenTelemetry span.
46
+ """
47
+
48
+ __slots__ = ("_trace_id_key", "_span_id_key", "_parent_span_id_key")
49
+
50
+ def __init__(self, trace_ctx_keys: Optional[dict[str, str]] = None) -> None:
51
+ _keys = trace_ctx_keys or {}
52
+
53
+ # do one-time lookup of the actual key names under which the different
54
+ # otel info should be hammered. this exists here because server allows
55
+ # admins to provide mapping between default trace/span/parent span
56
+ # and desired key names.
57
+ #
58
+ # the mapping exists to improve fit into different contexts where
59
+ # server is integrated. the default 'trace_id' and 'span_id' keys may
60
+ # thwart observability experience if the rest of the system uses say 'traceId'
61
+ # and 'spanId'.
62
+ self._trace_id_key = _keys.get("trace_id", "trace_id")
63
+ self._span_id_key = _keys.get("span_id", "span_id")
64
+ self._parent_span_id_key = _keys.get("parent_span_id", "parent_span_id")
65
+
66
+ def __call__(self, _: WrappedLogger, __: str, event_dict: EventDict) -> EventDict:
67
+ span = trace.get_current_span()
68
+ if span == trace.INVALID_SPAN:
69
+ # bail out right away if the current span is not valid
70
+ #
71
+ # most typically, this happens if the log is being emitted while there is no
72
+ # current span set (e.g. code runs outside a trace span -> very valid scenario)
73
+ return event_dict
74
+
75
+ span_ctx = span.get_span_context()
76
+ event_dict[self._trace_id_key] = f"{span_ctx.trace_id:x}"
77
+ event_dict[self._span_id_key] = f"{span_ctx.span_id:x}"
78
+
79
+ parent_ctx: Optional[trace.SpanContext] = getattr(span, "parent", None)
80
+ if parent_ctx:
81
+ event_dict[self._parent_span_id_key] = f"{parent_ctx.span_id:x}"
82
+
83
+ return event_dict
84
+
85
+
86
+ def _configure_structlog(
87
+ dev_log: bool,
88
+ event_key: str,
89
+ add_trace_ctx: bool = False,
90
+ trace_ctx_keys: Optional[dict[str, str]] = None,
91
+ ) -> None:
92
+ common_processors: list[Any] = [
93
+ structlog.stdlib.filter_by_level,
94
+ structlog.contextvars.merge_contextvars,
95
+ ]
96
+
97
+ if add_trace_ctx:
98
+ common_processors.append(_OtelTraceContextInjector(trace_ctx_keys))
99
+
100
+ common_processors.extend(
101
+ [
102
+ structlog.stdlib.add_logger_name,
103
+ structlog.stdlib.add_log_level,
104
+ structlog.stdlib.PositionalArgumentsFormatter(),
105
+ structlog.processors.TimeStamper(fmt="iso"),
106
+ structlog.processors.StackInfoRenderer(),
107
+ structlog.processors.format_exc_info,
108
+ structlog.processors.UnicodeDecoder(),
109
+ ]
110
+ )
111
+
112
+ if dev_log:
113
+ processors: Any = common_processors + [structlog.dev.ConsoleRenderer()]
114
+ else:
115
+ processors = common_processors + [
116
+ structlog.processors.EventRenamer(event_key),
117
+ _ORJsonRenderer(),
118
+ ]
119
+
120
+ structlog.configure(
121
+ processors=processors,
122
+ wrapper_class=structlog.stdlib.BoundLogger,
123
+ logger_factory=structlog.stdlib.LoggerFactory(),
124
+ cache_logger_on_first_use=True,
125
+ )
126
+
127
+
128
+ def init_logging(
129
+ logging_ini: str,
130
+ dev_log: bool = False,
131
+ event_key: str = "event",
132
+ for_module: Optional[str] = None,
133
+ add_trace_ctx: bool = False,
134
+ trace_ctx_keys: Optional[dict[str, str]] = None,
135
+ ) -> str:
136
+ """
137
+ Initializes python logging from the file on the provided path. If the path is absolute, then it is
138
+ used as is. Otherwise lookup is done for a file relative to the provided module if it is specified; if not
139
+ specified, lookup is done for file relative to the current directory.
140
+
141
+ :param logging_ini: path to logging ini file
142
+ :param dev_log: specify true to configure logging in development mode; this mode is more convenient
143
+ during dev testing but not so suitable for production
144
+ :param event_key: name of the event key (default event)
145
+ :param for_module: optionally specify, module pathname; relative file config will be resolved
146
+ in regard to this module directory; otherwise goes against current dir
147
+ :param add_trace_ctx: optionally specify whether logging pipeline should include processor that hammers in
148
+ otel tracing information (trace id, span id)
149
+ :param trace_ctx_keys: optionally specify mapping for key names under which trace context will be dumped into
150
+ log events. default values are 'trace_id', 'span_id' and 'parent_span_id'. The mapping allows to override
151
+ those key names
152
+ :return: pathname of log file that was used to configure
153
+ """
154
+ log_config = _resolve_config(logging_ini, for_module)
155
+ fileConfig(log_config)
156
+ _configure_structlog(dev_log, event_key, add_trace_ctx, trace_ctx_keys)
157
+
158
+ return log_config
@@ -0,0 +1,98 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import importlib
3
+ from functools import wraps
4
+ from inspect import signature
5
+ from typing import Optional
6
+
7
+ from gooddata_flight_server.exceptions import FlightMethodsModuleError
8
+ from gooddata_flight_server.server.base import (
9
+ FlightServerMethods,
10
+ FlightServerMethodsFactory,
11
+ ServerContext,
12
+ )
13
+
14
+
15
+ def flight_server_methods(fun: FlightServerMethodsFactory) -> FlightServerMethodsFactory:
16
+ """
17
+ Decorator that marks a function as the FlightServerMethodsFactory to be used.
18
+ Any module that should provide the methods factory should contain exactly one function decorated with this decorator.
19
+ """
20
+
21
+ @wraps(fun)
22
+ def wrapped(ctx: ServerContext) -> FlightServerMethods:
23
+ return fun(ctx)
24
+
25
+ wrapped.__is_flight_server_methods__ = True # type: ignore
26
+ return wrapped
27
+
28
+
29
+ def _is_flight_server_methods(obj: object) -> bool:
30
+ """
31
+ Checks whether the object is a FlightServerMethods.
32
+ """
33
+
34
+ return getattr(obj, "__is_flight_server_methods__", False)
35
+
36
+
37
+ def _is_valid_flight_methods_factory(obj: object) -> bool:
38
+ """
39
+ Check whether the object is a valid FlightServerMethodsFactory.
40
+ """
41
+
42
+ return callable(obj) and len(signature(obj).parameters) == 1
43
+
44
+
45
+ def _only_valid_flight_methods_factory(
46
+ module_name: str, factories: list[FlightServerMethodsFactory]
47
+ ) -> FlightServerMethodsFactory:
48
+ """
49
+ Validate the list of factories and return the only valid one.
50
+ """
51
+ if len(factories) == 0:
52
+ raise FlightMethodsModuleError(
53
+ f"No flight methods factory found in the module {module_name}. "
54
+ "Make sure the module exports exactly one function decorated with the `@flight_server_methods` decorator "
55
+ "that conforms to the FlightMethodsFactory protocol."
56
+ )
57
+
58
+ if len(factories) > 1:
59
+ raise FlightMethodsModuleError(
60
+ f"Multiple flight methods factories ({len(factories)}) found in the module {module_name}"
61
+ "Make sure the module exports exactly one function decorated with the `@flight_server_methods` decorator "
62
+ "that conforms to the FlightMethodsFactory protocol."
63
+ )
64
+
65
+ factory = factories[0]
66
+
67
+ if not _is_valid_flight_methods_factory(factory):
68
+ raise FlightMethodsModuleError(
69
+ f"Invalid flight methods factory in the module {module_name}. "
70
+ "Make sure the function conforms to the FlightMethodsFactory protocol: "
71
+ "that it takes the correct number of arguments and returns an instance of FlightServerMethods."
72
+ )
73
+
74
+ return factory
75
+
76
+
77
+ def get_methods_factory(module_name: str, root: Optional[str] = None) -> FlightServerMethodsFactory:
78
+ """
79
+ Get the method factory from the given module.
80
+ The module should contain exactly one method decorated with @flight_server_methods.
81
+
82
+ :param module_name: name of the module containing the methods abstract factory.
83
+ :param root: root package of the module: this is used to resolve relative module names (mainly useful in tests).
84
+ :return: a FlightServerMethodsFactory
85
+ """
86
+
87
+ try:
88
+ module = importlib.import_module(module_name, package=root)
89
+ except ModuleNotFoundError as e:
90
+ raise FlightMethodsModuleError(f"Flight methods module {module_name} not found.") from e
91
+
92
+ factories: list[FlightServerMethodsFactory] = [
93
+ member.__wrapped__ # unwrap the actual instance from the marker decorator to keep the type
94
+ for member in module.__dict__.values()
95
+ if _is_flight_server_methods(member)
96
+ ]
97
+
98
+ return _only_valid_flight_methods_factory(module_name, factories)
@@ -0,0 +1,142 @@
1
+ # (C) 2024 GoodData Corporation
2
+ import os
3
+ import platform
4
+ import socket
5
+ import sys
6
+ from typing import Optional
7
+
8
+ from opentelemetry import trace
9
+ from opentelemetry.sdk.resources import (
10
+ OS_DESCRIPTION,
11
+ OS_TYPE,
12
+ PROCESS_PARENT_PID,
13
+ PROCESS_PID,
14
+ PROCESS_RUNTIME_DESCRIPTION,
15
+ PROCESS_RUNTIME_NAME,
16
+ PROCESS_RUNTIME_VERSION,
17
+ SERVICE_INSTANCE_ID,
18
+ SERVICE_NAME,
19
+ SERVICE_NAMESPACE,
20
+ SERVICE_VERSION,
21
+ Resource,
22
+ )
23
+ from opentelemetry.sdk.trace import TracerProvider
24
+ from opentelemetry.sdk.trace.export import (
25
+ BatchSpanProcessor,
26
+ ConsoleSpanExporter,
27
+ SpanExporter,
28
+ )
29
+
30
+ from gooddata_flight_server._version import __version__
31
+ from gooddata_flight_server.config.config import OtelConfig, OtelExporterType
32
+
33
+ SERVER_TRACER: trace.Tracer = trace.ProxyTracer("gooddata_flight_server")
34
+ """
35
+ Tracer to use for all spans created within the server - all code in this package should
36
+ use this one single tracer.
37
+ """
38
+
39
+
40
+ def _create_console_span_exporter() -> SpanExporter:
41
+ return ConsoleSpanExporter()
42
+
43
+
44
+ def _create_zipkin_span_exporter() -> SpanExporter:
45
+ from opentelemetry.exporter.zipkin.json import (
46
+ ZipkinExporter, # type: ignore
47
+ )
48
+
49
+ return ZipkinExporter()
50
+
51
+
52
+ def _create_otlp_grpc_exporter() -> SpanExporter:
53
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
54
+ OTLPSpanExporter, # type: ignore
55
+ )
56
+
57
+ return OTLPSpanExporter()
58
+
59
+
60
+ def _create_otlp_http_exporter() -> SpanExporter:
61
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
62
+ OTLPSpanExporter, # type: ignore
63
+ )
64
+
65
+ return OTLPSpanExporter()
66
+
67
+
68
+ def _create_exporter(config: OtelConfig) -> SpanExporter:
69
+ if config.exporter_type == OtelExporterType.Console:
70
+ return _create_console_span_exporter()
71
+ elif config.exporter_type == OtelExporterType.Zipkin:
72
+ return _create_zipkin_span_exporter()
73
+ elif config.exporter_type == OtelExporterType.OtlpGrpc:
74
+ return _create_otlp_grpc_exporter()
75
+ elif config.exporter_type == OtelExporterType.OtlpHttp:
76
+ return _create_otlp_http_exporter()
77
+
78
+ raise AssertionError(f"Unsupported exporter type '{config.exporter_type}'.")
79
+
80
+
81
+ def _default_service_instance_id() -> str:
82
+ return socket.gethostbyaddr("127.0.0.1")[0] if platform.system() == "Darwin" else socket.getfqdn()
83
+
84
+
85
+ def _create_resource(config: OtelConfig) -> Resource:
86
+ # all PROCESS_RUNTIME_* attribute values including the code below picked as-is
87
+ # from recommendations written in:
88
+ #
89
+ # https://opentelemetry.io/docs/specs/semconv/resource/process/#python-runtimes
90
+ vinfo = sys.implementation.version
91
+ process_runtime_version = ".".join(
92
+ map(
93
+ str,
94
+ vinfo[:3] if vinfo.releaselevel == "final" and not vinfo.serial else vinfo,
95
+ )
96
+ )
97
+
98
+ service_instance_id = config.service_instance_id or _default_service_instance_id()
99
+
100
+ return Resource.create(
101
+ attributes={
102
+ SERVICE_NAME: config.service_name,
103
+ SERVICE_VERSION: __version__,
104
+ SERVICE_INSTANCE_ID: service_instance_id,
105
+ SERVICE_NAMESPACE: config.service_namespace or "",
106
+ OS_TYPE: platform.system().lower(),
107
+ OS_DESCRIPTION: platform.platform(terse=True),
108
+ PROCESS_PID: os.getpid(),
109
+ PROCESS_PARENT_PID: os.getppid(),
110
+ PROCESS_RUNTIME_NAME: sys.implementation.name,
111
+ PROCESS_RUNTIME_VERSION: process_runtime_version,
112
+ PROCESS_RUNTIME_DESCRIPTION: sys.version,
113
+ "os.version": platform.release(),
114
+ }
115
+ )
116
+
117
+
118
+ def initialize_otel_tracing(config: Optional[OtelConfig]) -> None:
119
+ """
120
+ Initializes OpenTelemetry tracing according to the provided `config`:
121
+
122
+ - If the config is specified, the method creates the desired exporter and
123
+ initializes the tracer provider to pipe traces to it
124
+
125
+ - If the config is None, the method will initialize tracer provider to
126
+ NoOp implementation.
127
+
128
+ :param config: server's open telemetry configuration
129
+ :return: None
130
+ """
131
+ if config is None or config.exporter_type is None:
132
+ trace.set_tracer_provider(trace.NoOpTracerProvider())
133
+ return
134
+
135
+ resource = _create_resource(config)
136
+ span_exporter = _create_exporter(config)
137
+
138
+ span_processor = BatchSpanProcessor(span_exporter=span_exporter)
139
+
140
+ tracer_provider = TracerProvider(resource=resource)
141
+ tracer_provider.add_span_processor(span_processor=span_processor)
142
+ trace.set_tracer_provider(tracer_provider=tracer_provider)
@@ -0,0 +1,10 @@
1
+ #!python
2
+ # (C) 2024 GoodData Corporation
3
+ import re
4
+ import sys
5
+
6
+ import gooddata_flight_server.cli as server
7
+
8
+ if __name__ == "__main__":
9
+ sys.argv[0] = re.sub(r"(-script\.pyw?|\.exe)?$", "", sys.argv[0])
10
+ sys.exit(server.server_cli())
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2024 GoodData Corporation
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.