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.

@@ -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
@@ -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}")