feldera 0.34.1__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 feldera might be problematic. Click here for more details.
- feldera/__init__.py +11 -0
- feldera/_callback_runner.py +116 -0
- feldera/_helpers.py +104 -0
- feldera/enums.py +234 -0
- feldera/output_handler.py +67 -0
- feldera/pipeline.py +809 -0
- feldera/pipeline_builder.py +109 -0
- feldera/rest/__init__.py +11 -0
- feldera/rest/_httprequests.py +182 -0
- feldera/rest/config.py +26 -0
- feldera/rest/errors.py +58 -0
- feldera/rest/feldera_client.py +605 -0
- feldera/rest/pipeline.py +77 -0
- feldera/rest/sql_table.py +23 -0
- feldera/rest/sql_view.py +23 -0
- feldera/runtime_config.py +78 -0
- feldera-0.34.1.dist-info/METADATA +105 -0
- feldera-0.34.1.dist-info/RECORD +20 -0
- feldera-0.34.1.dist-info/WHEEL +5 -0
- feldera-0.34.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from feldera.rest.feldera_client import FelderaClient
|
|
2
|
+
from feldera.rest.pipeline import Pipeline as InnerPipeline
|
|
3
|
+
from feldera.pipeline import Pipeline
|
|
4
|
+
from feldera.enums import CompilationProfile
|
|
5
|
+
from feldera.runtime_config import RuntimeConfig, Resources
|
|
6
|
+
from feldera.rest.errors import FelderaAPIError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PipelineBuilder:
|
|
10
|
+
"""
|
|
11
|
+
A builder for creating a Feldera Pipeline.
|
|
12
|
+
|
|
13
|
+
:param client: The `.FelderaClient` instance
|
|
14
|
+
:param name: The name of the pipeline
|
|
15
|
+
:param description: The description of the pipeline
|
|
16
|
+
:param sql: The SQL code of the pipeline
|
|
17
|
+
:param udf_rust: Rust code for UDFs
|
|
18
|
+
:param udf_toml: Rust dependencies required by UDFs (in the TOML format)
|
|
19
|
+
:param compilation_profile: The compilation profile to use
|
|
20
|
+
:param runtime_config: The runtime config to use
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
client: FelderaClient,
|
|
26
|
+
name: str,
|
|
27
|
+
sql: str,
|
|
28
|
+
udf_rust: str = "",
|
|
29
|
+
udf_toml: str = "",
|
|
30
|
+
description: str = "",
|
|
31
|
+
compilation_profile: CompilationProfile = CompilationProfile.OPTIMIZED,
|
|
32
|
+
runtime_config: RuntimeConfig = RuntimeConfig(resources=Resources()),
|
|
33
|
+
):
|
|
34
|
+
self.client: FelderaClient = client
|
|
35
|
+
self.name: str | None = name
|
|
36
|
+
self.description: str = description
|
|
37
|
+
self.sql: str = sql
|
|
38
|
+
self.udf_rust: str = udf_rust
|
|
39
|
+
self.udf_toml: str = udf_toml
|
|
40
|
+
self.compilation_profile: CompilationProfile = compilation_profile
|
|
41
|
+
self.runtime_config: RuntimeConfig = runtime_config
|
|
42
|
+
|
|
43
|
+
def create(self) -> Pipeline:
|
|
44
|
+
"""
|
|
45
|
+
Create the pipeline if it does not exist.
|
|
46
|
+
|
|
47
|
+
:return: The created pipeline
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
if self.name is None or self.sql is None:
|
|
51
|
+
raise ValueError("Name and SQL are required to create a pipeline")
|
|
52
|
+
|
|
53
|
+
if self.client.get_pipeline(self.name) is not None:
|
|
54
|
+
raise RuntimeError(f"Pipeline with name {self.name} already exists")
|
|
55
|
+
|
|
56
|
+
inner = InnerPipeline(
|
|
57
|
+
self.name,
|
|
58
|
+
description=self.description,
|
|
59
|
+
sql=self.sql,
|
|
60
|
+
udf_rust=self.udf_rust,
|
|
61
|
+
udf_toml=self.udf_toml,
|
|
62
|
+
program_config={
|
|
63
|
+
"profile": self.compilation_profile.value,
|
|
64
|
+
},
|
|
65
|
+
runtime_config=self.runtime_config.__dict__,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
inner = self.client.create_pipeline(inner)
|
|
69
|
+
pipeline = Pipeline(self.client)
|
|
70
|
+
pipeline._inner = inner
|
|
71
|
+
|
|
72
|
+
return pipeline
|
|
73
|
+
|
|
74
|
+
def create_or_replace(self) -> Pipeline:
|
|
75
|
+
"""
|
|
76
|
+
Creates a pipeline if it does not exist and replaces it if it exists.
|
|
77
|
+
|
|
78
|
+
If the pipeline exists and is running, it will be stopped and replaced.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
if self.name is None or self.sql is None:
|
|
82
|
+
raise ValueError("Name and SQL are required to create a pipeline")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
# shutdown the pipeline if it exists and is running
|
|
86
|
+
self.client.shutdown_pipeline(self.name)
|
|
87
|
+
except FelderaAPIError:
|
|
88
|
+
# pipeline doesn't exist, no worries
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
inner = InnerPipeline(
|
|
92
|
+
self.name,
|
|
93
|
+
description=self.description,
|
|
94
|
+
sql=self.sql,
|
|
95
|
+
udf_rust=self.udf_rust,
|
|
96
|
+
udf_toml=self.udf_toml,
|
|
97
|
+
program_config={
|
|
98
|
+
"profile": self.compilation_profile.value,
|
|
99
|
+
},
|
|
100
|
+
runtime_config=dict(
|
|
101
|
+
(k, v) for k, v in self.runtime_config.__dict__.items() if v is not None
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
inner = self.client.create_or_update_pipeline(inner)
|
|
106
|
+
pipeline = Pipeline(self.client)
|
|
107
|
+
pipeline._inner = inner
|
|
108
|
+
|
|
109
|
+
return pipeline
|
feldera/rest/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is the lower level REST client for Feldera.
|
|
3
|
+
|
|
4
|
+
This is a thin wrapper around the Feldera REST API.
|
|
5
|
+
|
|
6
|
+
It is recommended to use the higher level abstractions in the `feldera` package,
|
|
7
|
+
instead of using the REST client directly.
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from feldera.rest.feldera_client import FelderaClient as FelderaClient
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from feldera.rest.config import Config
|
|
4
|
+
|
|
5
|
+
from feldera.rest.errors import (
|
|
6
|
+
FelderaAPIError,
|
|
7
|
+
FelderaTimeoutError,
|
|
8
|
+
FelderaCommunicationError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import requests
|
|
13
|
+
from typing import Callable, Optional, Any, Union, Mapping, Sequence, List
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def json_serialize(body: Any) -> str:
|
|
17
|
+
return json.dumps(body) if body else "" if body == "" else "null"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HttpRequests:
|
|
21
|
+
def __init__(self, config: Config) -> None:
|
|
22
|
+
self.config = config
|
|
23
|
+
self.headers = {"User-Agent": "feldera-python-sdk/v1"}
|
|
24
|
+
if self.config.api_key:
|
|
25
|
+
self.headers["Authorization"] = f"Bearer {self.config.api_key}"
|
|
26
|
+
|
|
27
|
+
def send_request(
|
|
28
|
+
self,
|
|
29
|
+
http_method: Callable,
|
|
30
|
+
path: str,
|
|
31
|
+
body: Optional[
|
|
32
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
33
|
+
] = None,
|
|
34
|
+
content_type: str = "application/json",
|
|
35
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
36
|
+
stream: bool = False,
|
|
37
|
+
serialize: bool = True,
|
|
38
|
+
) -> Any:
|
|
39
|
+
"""
|
|
40
|
+
:param http_method: The HTTP method to use. Takes the equivalent `requests.*` module. (Example: `requests.get`)
|
|
41
|
+
:param path: The path to send the request to.
|
|
42
|
+
:param body: The HTTP request body.
|
|
43
|
+
:param content_type: The value for `Content-Type` HTTP header. "application/json" by default.
|
|
44
|
+
:param params: The query parameters part of this request.
|
|
45
|
+
:param stream: True if the response is expected to be a HTTP stream.
|
|
46
|
+
:param serialize: True if the body needs to be serialized to JSON.
|
|
47
|
+
"""
|
|
48
|
+
self.headers["Content-Type"] = content_type
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
timeout = self.config.timeout
|
|
52
|
+
headers = self.headers
|
|
53
|
+
|
|
54
|
+
request_path = self.config.url + "/" + self.config.version + path
|
|
55
|
+
|
|
56
|
+
logging.debug(
|
|
57
|
+
"sending %s request to: %s with headers: %s, and params: %s",
|
|
58
|
+
http_method.__name__,
|
|
59
|
+
request_path,
|
|
60
|
+
str(headers),
|
|
61
|
+
str(params),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if http_method.__name__ == "get":
|
|
65
|
+
request = http_method(
|
|
66
|
+
request_path,
|
|
67
|
+
timeout=timeout,
|
|
68
|
+
headers=headers,
|
|
69
|
+
params=params,
|
|
70
|
+
stream=stream,
|
|
71
|
+
)
|
|
72
|
+
elif isinstance(body, bytes):
|
|
73
|
+
request = http_method(
|
|
74
|
+
request_path,
|
|
75
|
+
timeout=timeout,
|
|
76
|
+
headers=headers,
|
|
77
|
+
data=body,
|
|
78
|
+
params=params,
|
|
79
|
+
stream=stream,
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
request = http_method(
|
|
83
|
+
request_path,
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
headers=headers,
|
|
86
|
+
data=json_serialize(body) if serialize else body,
|
|
87
|
+
params=params,
|
|
88
|
+
stream=stream,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
resp = self.__validate(request, stream=stream)
|
|
92
|
+
logging.debug("got response: %s", str(resp))
|
|
93
|
+
return resp
|
|
94
|
+
|
|
95
|
+
except requests.exceptions.Timeout as err:
|
|
96
|
+
raise FelderaTimeoutError(str(err)) from err
|
|
97
|
+
except requests.exceptions.ConnectionError as err:
|
|
98
|
+
raise FelderaCommunicationError(str(err)) from err
|
|
99
|
+
|
|
100
|
+
def get(
|
|
101
|
+
self,
|
|
102
|
+
path: str,
|
|
103
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
104
|
+
stream: bool = False,
|
|
105
|
+
) -> Any:
|
|
106
|
+
return self.send_request(requests.get, path, params=params, stream=stream)
|
|
107
|
+
|
|
108
|
+
def post(
|
|
109
|
+
self,
|
|
110
|
+
path: str,
|
|
111
|
+
body: Optional[
|
|
112
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
113
|
+
] = None,
|
|
114
|
+
content_type: Optional[str] = "application/json",
|
|
115
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
116
|
+
stream: bool = False,
|
|
117
|
+
serialize: bool = True,
|
|
118
|
+
) -> Any:
|
|
119
|
+
return self.send_request(
|
|
120
|
+
requests.post,
|
|
121
|
+
path,
|
|
122
|
+
body,
|
|
123
|
+
content_type,
|
|
124
|
+
params,
|
|
125
|
+
stream=stream,
|
|
126
|
+
serialize=serialize,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def patch(
|
|
130
|
+
self,
|
|
131
|
+
path: str,
|
|
132
|
+
body: Optional[
|
|
133
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
134
|
+
] = None,
|
|
135
|
+
content_type: Optional[str] = "application/json",
|
|
136
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
137
|
+
) -> Any:
|
|
138
|
+
return self.send_request(requests.patch, path, body, content_type, params)
|
|
139
|
+
|
|
140
|
+
def put(
|
|
141
|
+
self,
|
|
142
|
+
path: str,
|
|
143
|
+
body: Optional[
|
|
144
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
145
|
+
] = None,
|
|
146
|
+
content_type: Optional[str] = "application/json",
|
|
147
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
148
|
+
) -> Any:
|
|
149
|
+
return self.send_request(requests.put, path, body, content_type, params)
|
|
150
|
+
|
|
151
|
+
def delete(
|
|
152
|
+
self,
|
|
153
|
+
path: str,
|
|
154
|
+
body: Optional[
|
|
155
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str]]
|
|
156
|
+
] = None,
|
|
157
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
158
|
+
) -> Any:
|
|
159
|
+
return self.send_request(requests.delete, path, body, params=params)
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def __to_json(request: requests.Response) -> Any:
|
|
163
|
+
if request.content == b"":
|
|
164
|
+
return request
|
|
165
|
+
return request.json()
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def __validate(request: requests.Response, stream=False) -> Any:
|
|
169
|
+
try:
|
|
170
|
+
request.raise_for_status()
|
|
171
|
+
|
|
172
|
+
if stream:
|
|
173
|
+
return request
|
|
174
|
+
if request.headers.get("content-type") == "text/plain":
|
|
175
|
+
return request.text
|
|
176
|
+
elif request.headers.get("content-type") == "application/octet-stream":
|
|
177
|
+
return request.content
|
|
178
|
+
|
|
179
|
+
resp = HttpRequests.__to_json(request)
|
|
180
|
+
return resp
|
|
181
|
+
except requests.exceptions.HTTPError as err:
|
|
182
|
+
raise FelderaAPIError(str(err), request) from err
|
feldera/rest/config.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Config:
|
|
5
|
+
"""
|
|
6
|
+
:class:`.FelderaClient`'s credentials and configuration parameters
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
url: str,
|
|
12
|
+
api_key: Optional[str] = None,
|
|
13
|
+
version: Optional[str] = None,
|
|
14
|
+
timeout: Optional[float] = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
"""
|
|
17
|
+
:param url: The url to the Feldera API (ex: https://try.feldera.com)
|
|
18
|
+
:param api_key: The optional API key to access Feldera
|
|
19
|
+
:param version: The version of the API to use
|
|
20
|
+
:param timeout: The timeout for the HTTP requests
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
self.url: str = url
|
|
24
|
+
self.api_key: Optional[str] = api_key
|
|
25
|
+
self.version: Optional[str] = version or "v0"
|
|
26
|
+
self.timeout: Optional[float] = timeout
|
feldera/rest/errors.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from requests import Response
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class FelderaError(Exception):
|
|
6
|
+
"""
|
|
7
|
+
Generic class for Feldera error handling
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str) -> None:
|
|
11
|
+
self.message = message
|
|
12
|
+
super().__init__(self.message)
|
|
13
|
+
|
|
14
|
+
def __str__(self) -> str:
|
|
15
|
+
return f"FelderaError. Error message: {self.message}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FelderaAPIError(FelderaError):
|
|
19
|
+
"""Error sent by Feldera API"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, error: str, request: Response) -> None:
|
|
22
|
+
self.status_code = request.status_code
|
|
23
|
+
self.error = error
|
|
24
|
+
self.error_code = None
|
|
25
|
+
self.message = None
|
|
26
|
+
self.details = None
|
|
27
|
+
|
|
28
|
+
err_msg = ""
|
|
29
|
+
|
|
30
|
+
if request.text:
|
|
31
|
+
try:
|
|
32
|
+
json_data = json.loads(request.text)
|
|
33
|
+
|
|
34
|
+
self.error_code = json_data.get("error_code")
|
|
35
|
+
if self.error_code:
|
|
36
|
+
err_msg += f"\nError Code: {self.error_code}"
|
|
37
|
+
self.message = json_data.get("message")
|
|
38
|
+
if self.message:
|
|
39
|
+
err_msg += f"\nMessage: {self.message}"
|
|
40
|
+
self.details = json_data.get("details")
|
|
41
|
+
except Exception:
|
|
42
|
+
self.message = request.text
|
|
43
|
+
|
|
44
|
+
super().__init__(err_msg)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FelderaTimeoutError(FelderaError):
|
|
48
|
+
"""Error when Feldera operation takes longer than expected"""
|
|
49
|
+
|
|
50
|
+
def __init__(self, err: str) -> None:
|
|
51
|
+
super().__init__(f"Timeout connecting to Feldera: {err}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class FelderaCommunicationError(FelderaError):
|
|
55
|
+
"""Error when connection to Feldera"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, err: str) -> None:
|
|
58
|
+
super().__init__(f"Cannot connect to Feldera API: {err}")
|