sentry-relay 0.9.9__tar.gz

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.
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.1
2
+ Name: sentry-relay
3
+ Version: 0.9.9
4
+ Summary: A python library to access sentry relay functionality.
5
+ Author: Sentry
6
+ Author-email: hello@sentry.io
7
+ License: FSL-1.0-Apache-2.0
8
+ Platform: any
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
File without changes
Binary file
@@ -0,0 +1,23 @@
1
+ __all__ = []
2
+
3
+
4
+ def _import_all():
5
+ import pkgutil
6
+
7
+ glob = globals()
8
+ for _, modname, _ in pkgutil.iter_modules(__path__):
9
+ if modname[:1] == "_":
10
+ continue
11
+ mod = __import__("sentry_relay.%s" % modname, glob, glob, ["__name__"])
12
+ if not hasattr(mod, "__all__"):
13
+ continue
14
+ __all__.extend(mod.__all__)
15
+ for name in mod.__all__:
16
+ obj = getattr(mod, name)
17
+ if hasattr(obj, "__module__"):
18
+ obj.__module__ = "sentry_relay"
19
+ glob[name] = obj
20
+
21
+
22
+ _import_all()
23
+ del _import_all
@@ -0,0 +1,149 @@
1
+ import json
2
+ import uuid
3
+ from typing import Callable, Any
4
+
5
+ from sentry_relay._lowlevel import lib
6
+ from sentry_relay.utils import (
7
+ RustObject,
8
+ encode_str,
9
+ decode_str,
10
+ decode_uuid,
11
+ rustcall,
12
+ make_buf,
13
+ )
14
+ from sentry_relay.exceptions import UnpackErrorBadSignature
15
+
16
+
17
+ __all__ = [
18
+ "PublicKey",
19
+ "SecretKey",
20
+ "generate_key_pair",
21
+ "create_register_challenge",
22
+ "validate_register_response",
23
+ "is_version_supported",
24
+ ]
25
+
26
+
27
+ class PublicKey(RustObject):
28
+ __dealloc_func__ = lib.relay_publickey_free
29
+
30
+ @classmethod
31
+ def parse(cls, string):
32
+ s = encode_str(string)
33
+ ptr = rustcall(lib.relay_publickey_parse, s)
34
+ return cls._from_objptr(ptr)
35
+
36
+ def verify(self, buf, sig, max_age=None):
37
+ buf = make_buf(buf)
38
+ sig = encode_str(sig)
39
+ if max_age is None:
40
+ return self._methodcall(lib.relay_publickey_verify, buf, sig)
41
+ return self._methodcall(lib.relay_publickey_verify_timestamp, buf, sig, max_age)
42
+
43
+ def unpack(
44
+ self,
45
+ buf,
46
+ sig,
47
+ max_age=None,
48
+ json_loads: Callable[[str | bytes], Any] = json.loads,
49
+ ):
50
+ if not self.verify(buf, sig, max_age):
51
+ raise UnpackErrorBadSignature("invalid signature")
52
+ return json_loads(buf)
53
+
54
+ def __str__(self):
55
+ return decode_str(self._methodcall(lib.relay_publickey_to_string), free=True)
56
+
57
+ def __repr__(self):
58
+ return f"<{self.__class__.__name__} {str(self)!r}>"
59
+
60
+
61
+ class SecretKey(RustObject):
62
+ __dealloc_func__ = lib.relay_secretkey_free
63
+
64
+ @classmethod
65
+ def parse(cls, string):
66
+ s = encode_str(string)
67
+ ptr = rustcall(lib.relay_secretkey_parse, s)
68
+ return cls._from_objptr(ptr)
69
+
70
+ def sign(self, value):
71
+ buf = make_buf(value)
72
+ return decode_str(self._methodcall(lib.relay_secretkey_sign, buf), free=True)
73
+
74
+ def pack(self, data):
75
+ # TODO(@anonrig): Look into separators requirement
76
+ packed = json.dumps(data, separators=(",", ":")).encode()
77
+ return packed, self.sign(packed)
78
+
79
+ def __str__(self):
80
+ return decode_str(self._methodcall(lib.relay_secretkey_to_string), free=True)
81
+
82
+ def __repr__(self):
83
+ return f"<{self.__class__.__name__} {str(self)!r}>"
84
+
85
+
86
+ def generate_key_pair():
87
+ rv = rustcall(lib.relay_generate_key_pair)
88
+ return (
89
+ SecretKey._from_objptr(rv.secret_key),
90
+ PublicKey._from_objptr(rv.public_key),
91
+ )
92
+
93
+
94
+ def generate_relay_id():
95
+ return decode_uuid(rustcall(lib.relay_generate_relay_id))
96
+
97
+
98
+ def create_register_challenge(
99
+ data,
100
+ signature,
101
+ secret,
102
+ max_age=60,
103
+ json_loads: Callable[[str | bytes], Any] = json.loads,
104
+ ):
105
+ challenge_json = rustcall(
106
+ lib.relay_create_register_challenge,
107
+ make_buf(data),
108
+ encode_str(signature),
109
+ encode_str(secret),
110
+ max_age,
111
+ )
112
+
113
+ challenge = json_loads(decode_str(challenge_json, free=True))
114
+ return {
115
+ "relay_id": uuid.UUID(challenge["relay_id"]),
116
+ "token": challenge["token"],
117
+ }
118
+
119
+
120
+ def validate_register_response(
121
+ data,
122
+ signature,
123
+ secret,
124
+ max_age=60,
125
+ json_loads: Callable[[str | bytes], Any] = json.loads,
126
+ ):
127
+ response_json = rustcall(
128
+ lib.relay_validate_register_response,
129
+ make_buf(data),
130
+ encode_str(signature),
131
+ encode_str(secret),
132
+ max_age,
133
+ )
134
+
135
+ response = json_loads(decode_str(response_json, free=True))
136
+ return {
137
+ "relay_id": uuid.UUID(response["relay_id"]),
138
+ "token": response["token"],
139
+ "public_key": response["public_key"],
140
+ "version": response["version"],
141
+ }
142
+
143
+
144
+ def is_version_supported(version):
145
+ """
146
+ Checks if the provided Relay version is still compatible with this library. The version can be
147
+ ``None``, in which case a legacy Relay is assumed.
148
+ """
149
+ return rustcall(lib.relay_version_supported, encode_str(version or ""))
@@ -0,0 +1,133 @@
1
+ import sys
2
+ from enum import IntEnum
3
+
4
+ from sentry_relay._lowlevel import lib
5
+ from sentry_relay.utils import decode_str, encode_str
6
+
7
+ __all__ = ["DataCategory", "SPAN_STATUS_CODE_TO_NAME", "SPAN_STATUS_NAME_TO_CODE"]
8
+
9
+
10
+ class DataCategory(IntEnum):
11
+ # begin generated
12
+ DEFAULT = 0
13
+ ERROR = 1
14
+ TRANSACTION = 2
15
+ SECURITY = 3
16
+ ATTACHMENT = 4
17
+ SESSION = 5
18
+ PROFILE = 6
19
+ REPLAY = 7
20
+ TRANSACTION_PROCESSED = 8
21
+ TRANSACTION_INDEXED = 9
22
+ MONITOR = 10
23
+ PROFILE_INDEXED = 11
24
+ SPAN = 12
25
+ MONITOR_SEAT = 13
26
+ USER_REPORT_V2 = 14
27
+ METRIC_BUCKET = 15
28
+ SPAN_INDEXED = 16
29
+ PROFILE_DURATION = 17
30
+ PROFILE_CHUNK = 18
31
+ METRIC_SECOND = 19
32
+ DO_NOT_USE_REPLAY_VIDEO = 20
33
+ UPTIME = 21
34
+ ATTACHMENT_ITEM = 22
35
+ LOG_ITEM = 23
36
+ LOG_BYTE = 24
37
+ PROFILE_DURATION_UI = 25
38
+ PROFILE_CHUNK_UI = 26
39
+ SEER_AUTOFIX = 27
40
+ SEER_SCANNER = 28
41
+ UNKNOWN = -1
42
+ # end generated
43
+
44
+ @classmethod
45
+ def parse(cls, name):
46
+ """
47
+ Parses a `DataCategory` from its API name.
48
+ """
49
+ category = DataCategory(lib.relay_data_category_parse(encode_str(name or "")))
50
+ if category == DataCategory.UNKNOWN:
51
+ return None # Unknown is a Rust-only value, replace with None
52
+ return category
53
+
54
+ @classmethod
55
+ def from_event_type(cls, event_type):
56
+ """
57
+ Parses a `DataCategory` from an event type.
58
+ """
59
+ s = encode_str(event_type or "")
60
+ return DataCategory(lib.relay_data_category_from_event_type(s))
61
+
62
+ @classmethod
63
+ def event_categories(cls):
64
+ """
65
+ Returns categories that count as events, including transactions.
66
+ """
67
+ return [
68
+ DataCategory.DEFAULT,
69
+ DataCategory.ERROR,
70
+ DataCategory.TRANSACTION,
71
+ DataCategory.SECURITY,
72
+ DataCategory.USER_REPORT_V2,
73
+ ]
74
+
75
+ @classmethod
76
+ def error_categories(cls):
77
+ """
78
+ Returns categories that count as traditional error tracking events.
79
+ """
80
+ return [DataCategory.DEFAULT, DataCategory.ERROR, DataCategory.SECURITY]
81
+
82
+ def api_name(self):
83
+ """
84
+ Returns the API name of the given `DataCategory`.
85
+ """
86
+ return decode_str(lib.relay_data_category_name(self.value), free=True)
87
+
88
+
89
+ def _check_generated():
90
+ prefix = "RELAY_DATA_CATEGORY_"
91
+
92
+ attrs = {}
93
+ for attr in dir(lib):
94
+ if attr.startswith(prefix):
95
+ category_name = attr[len(prefix) :]
96
+ attrs[category_name] = getattr(lib, attr)
97
+
98
+ if attrs != DataCategory.__members__:
99
+ values = sorted(
100
+ attrs.items(), key=lambda kv: sys.maxsize if kv[1] == -1 else kv[1]
101
+ )
102
+ generated = "".join(f" {k} = {v}\n" for k, v in values)
103
+ raise AssertionError(
104
+ f"DataCategory enum does not match source!\n\n"
105
+ f"Paste this into `class DataCategory` in py/sentry_relay/consts.py:\n\n"
106
+ f"{generated}"
107
+ )
108
+
109
+
110
+ _check_generated()
111
+
112
+ SPAN_STATUS_CODE_TO_NAME = {}
113
+ SPAN_STATUS_NAME_TO_CODE = {}
114
+
115
+
116
+ def _make_span_statuses():
117
+ prefix = "RELAY_SPAN_STATUS_"
118
+
119
+ for attr in dir(lib):
120
+ if not attr.startswith(prefix):
121
+ continue
122
+
123
+ status_name = attr[len(prefix) :].lower()
124
+ status_code = getattr(lib, attr)
125
+
126
+ SPAN_STATUS_CODE_TO_NAME[status_code] = status_name
127
+ SPAN_STATUS_NAME_TO_CODE[status_name] = status_code
128
+
129
+ # Legacy alias
130
+ SPAN_STATUS_NAME_TO_CODE["unknown_error"] = SPAN_STATUS_NAME_TO_CODE["unknown"]
131
+
132
+
133
+ _make_span_statuses()
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+ from typing import TYPE_CHECKING
3
+ from sentry_relay._lowlevel import lib
4
+
5
+
6
+ __all__ = ["RelayError"]
7
+ exceptions_by_code = {}
8
+
9
+
10
+ class RelayError(Exception):
11
+ code = None
12
+
13
+ def __init__(self, msg):
14
+ Exception.__init__(self)
15
+ self.message = msg
16
+ self.rust_info = None
17
+
18
+ def __str__(self):
19
+ rv = self.message
20
+ if self.rust_info is not None:
21
+ return f"{rv}\n\n{self.rust_info}"
22
+ return rv
23
+
24
+
25
+ def _make_error(error_name, base=RelayError, code=None):
26
+ class Exc(base):
27
+ pass
28
+
29
+ Exc.__name__ = error_name
30
+ Exc.__qualname__ = error_name
31
+ if code is not None:
32
+ Exc.code = code
33
+ globals()[Exc.__name__] = Exc
34
+ __all__.append(Exc.__name__)
35
+ return Exc
36
+
37
+
38
+ def _get_error_base(error_name):
39
+ pieces = error_name.split("Error", 1)
40
+ if len(pieces) == 2 and pieces[0] and pieces[1]:
41
+ base_error_name = pieces[0] + "Error"
42
+ base_class = globals().get(base_error_name)
43
+ if base_class is None:
44
+ base_class = _make_error(base_error_name)
45
+ return base_class
46
+ return RelayError
47
+
48
+
49
+ def _make_exceptions():
50
+ prefix = "RELAY_ERROR_CODE_"
51
+ for attr in dir(lib):
52
+ if not attr.startswith(prefix):
53
+ continue
54
+
55
+ error_name = attr[len(prefix) :].title().replace("_", "")
56
+ base = _get_error_base(error_name)
57
+ exc = _make_error(error_name, base=base, code=getattr(lib, attr))
58
+ exceptions_by_code[exc.code] = exc
59
+
60
+
61
+ _make_exceptions()
62
+
63
+ if TYPE_CHECKING:
64
+ # treat unknown attribute names as exception types
65
+ def __getattr__(name: str) -> type[RelayError]: ...
@@ -0,0 +1,373 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Callable, Any
5
+
6
+ from sentry_relay._lowlevel import lib, ffi
7
+ from sentry_relay.utils import (
8
+ encode_str,
9
+ decode_str,
10
+ rustcall,
11
+ RustObject,
12
+ attached_refs,
13
+ make_buf,
14
+ )
15
+
16
+ __all__ = [
17
+ "split_chunks",
18
+ "meta_with_chunks",
19
+ "StoreNormalizer",
20
+ "GeoIpLookup",
21
+ "is_glob_match",
22
+ "is_codeowners_path_match",
23
+ "parse_release",
24
+ "validate_pii_selector",
25
+ "validate_pii_config",
26
+ "convert_datascrubbing_config",
27
+ "pii_strip_event",
28
+ "pii_selector_suggestions_from_event",
29
+ "VALID_PLATFORMS",
30
+ "validate_rule_condition",
31
+ "validate_sampling_condition",
32
+ "validate_sampling_configuration",
33
+ "normalize_project_config",
34
+ "normalize_cardinality_limit_config",
35
+ "normalize_global_config",
36
+ ]
37
+
38
+
39
+ def _init_valid_platforms() -> frozenset[str]:
40
+ size_out = ffi.new("uintptr_t *")
41
+ strings = rustcall(lib.relay_valid_platforms, size_out)
42
+
43
+ valid_platforms = []
44
+ for i in range(int(size_out[0])):
45
+ valid_platforms.append(decode_str(strings[i], free=True))
46
+
47
+ return frozenset(valid_platforms)
48
+
49
+
50
+ VALID_PLATFORMS = _init_valid_platforms()
51
+
52
+
53
+ def split_chunks(
54
+ string,
55
+ remarks,
56
+ json_dumps: Callable[[Any], Any] = json.dumps,
57
+ json_loads: Callable[[str | bytes], Any] = json.loads,
58
+ ):
59
+ json_chunks = rustcall(
60
+ lib.relay_split_chunks,
61
+ encode_str(string),
62
+ encode_str(json_dumps(remarks)),
63
+ )
64
+ return json_loads(decode_str(json_chunks, free=True))
65
+
66
+
67
+ def meta_with_chunks(data, meta):
68
+ if not isinstance(meta, dict):
69
+ return meta
70
+
71
+ result = {}
72
+ for key, item in meta.items():
73
+ if key == "" and isinstance(item, dict):
74
+ result[""] = item.copy()
75
+ if item.get("rem") and isinstance(data, str):
76
+ result[""]["chunks"] = split_chunks(data, item["rem"])
77
+ elif isinstance(data, dict):
78
+ result[key] = meta_with_chunks(data.get(key), item)
79
+ elif isinstance(data, list):
80
+ int_key = int(key)
81
+ val = data[int_key] if int_key < len(data) else None
82
+ result[key] = meta_with_chunks(val, item)
83
+ else:
84
+ result[key] = item
85
+
86
+ return result
87
+
88
+
89
+ class GeoIpLookup(RustObject):
90
+ __dealloc_func__ = lib.relay_geoip_lookup_free
91
+ __slots__ = ("_path",)
92
+
93
+ @classmethod
94
+ def from_path(cls, path):
95
+ if isinstance(path, str):
96
+ path = path.encode("utf-8")
97
+ rv = cls._from_objptr(rustcall(lib.relay_geoip_lookup_new, path))
98
+ rv._path = path
99
+ return rv
100
+
101
+ def __repr__(self):
102
+ return f"<GeoIpLookup {self._path!r}>"
103
+
104
+
105
+ class StoreNormalizer(RustObject):
106
+ __dealloc_func__ = lib.relay_store_normalizer_free
107
+ __init__ = object.__init__
108
+ __slots__ = ("__weakref__",)
109
+
110
+ def __new__(
111
+ cls, geoip_lookup=None, json_dumps: Callable[[Any], Any] = json.dumps, **config
112
+ ):
113
+ config = json_dumps(config)
114
+ geoptr = geoip_lookup._get_objptr() if geoip_lookup is not None else ffi.NULL
115
+ rv = cls._from_objptr(
116
+ rustcall(lib.relay_store_normalizer_new, encode_str(config), geoptr)
117
+ )
118
+ if geoip_lookup is not None:
119
+ attached_refs[rv] = geoip_lookup
120
+ return rv
121
+
122
+ def normalize_event(
123
+ self,
124
+ event=None,
125
+ raw_event=None,
126
+ json_loads: Callable[[str | bytes], Any] = json.loads,
127
+ ):
128
+ if raw_event is None:
129
+ raw_event = _serialize_event(event)
130
+
131
+ event = _encode_raw_event(raw_event)
132
+ rv = self._methodcall(lib.relay_store_normalizer_normalize_event, event)
133
+ return json_loads(decode_str(rv, free=True))
134
+
135
+
136
+ def _serialize_event(event):
137
+ # TODO(@anonrig): Look into ensure_ascii requirement
138
+ raw_event = json.dumps(event, ensure_ascii=False)
139
+ if isinstance(raw_event, str):
140
+ raw_event = raw_event.encode("utf-8", errors="replace")
141
+ return raw_event
142
+
143
+
144
+ def _encode_raw_event(raw_event):
145
+ event = encode_str(raw_event, mutable=True)
146
+ rustcall(lib.relay_translate_legacy_python_json, event)
147
+ return event
148
+
149
+
150
+ def is_glob_match(
151
+ value,
152
+ pat,
153
+ double_star=False,
154
+ case_insensitive=False,
155
+ path_normalize=False,
156
+ allow_newline=False,
157
+ ):
158
+ flags = 0
159
+ if double_star:
160
+ flags |= lib.GLOB_FLAGS_DOUBLE_STAR
161
+ if case_insensitive:
162
+ flags |= lib.GLOB_FLAGS_CASE_INSENSITIVE
163
+ # Since on the C side we're only working with bytes we need to lowercase the pattern
164
+ # and value here. This works with both bytes and unicode strings.
165
+ value = value.lower()
166
+ pat = pat.lower()
167
+ if path_normalize:
168
+ flags |= lib.GLOB_FLAGS_PATH_NORMALIZE
169
+ if allow_newline:
170
+ flags |= lib.GLOB_FLAGS_ALLOW_NEWLINE
171
+
172
+ if isinstance(value, str):
173
+ value = value.encode("utf-8")
174
+ return rustcall(lib.relay_is_glob_match, make_buf(value), encode_str(pat), flags)
175
+
176
+
177
+ def is_codeowners_path_match(value, pattern):
178
+ if isinstance(value, str):
179
+ value = value.encode("utf-8")
180
+ return rustcall(
181
+ lib.relay_is_codeowners_path_match, make_buf(value), encode_str(pattern)
182
+ )
183
+
184
+
185
+ def validate_pii_selector(selector):
186
+ """
187
+ Validate a PII selector spec. Used to validate datascrubbing safe fields.
188
+ """
189
+ assert isinstance(selector, str)
190
+ raw_error = rustcall(lib.relay_validate_pii_selector, encode_str(selector))
191
+ error = decode_str(raw_error, free=True)
192
+ if error:
193
+ raise ValueError(error)
194
+
195
+
196
+ def validate_pii_config(config):
197
+ """
198
+ Validate a PII config against the schema. Used in project options UI.
199
+
200
+ The parameter is a JSON-encoded string. We should pass the config through
201
+ as a string such that line numbers from the error message match with what
202
+ the user typed in.
203
+ """
204
+ assert isinstance(config, str)
205
+ raw_error = rustcall(lib.relay_validate_pii_config, encode_str(config))
206
+ error = decode_str(raw_error, free=True)
207
+ if error:
208
+ raise ValueError(error)
209
+
210
+
211
+ def convert_datascrubbing_config(
212
+ config,
213
+ json_dumps: Callable[[Any], Any] = json.dumps,
214
+ json_loads: Callable[[str | bytes], Any] = json.loads,
215
+ ):
216
+ """
217
+ Convert an old datascrubbing config to the new PII config format.
218
+ """
219
+ raw_config = encode_str(json_dumps(config))
220
+ raw_rv = rustcall(lib.relay_convert_datascrubbing_config, raw_config)
221
+ return json_loads(decode_str(raw_rv, free=True))
222
+
223
+
224
+ def pii_strip_event(
225
+ config,
226
+ event,
227
+ json_dumps: Callable[[Any], Any] = json.dumps,
228
+ json_loads: Callable[[str | bytes], Any] = json.loads,
229
+ ):
230
+ """
231
+ Scrub an event using new PII stripping config.
232
+ """
233
+ raw_config = encode_str(json_dumps(config))
234
+ raw_event = encode_str(json_dumps(event))
235
+ raw_rv = rustcall(lib.relay_pii_strip_event, raw_config, raw_event)
236
+ return json_loads(decode_str(raw_rv, free=True))
237
+
238
+
239
+ def pii_selector_suggestions_from_event(
240
+ event,
241
+ json_dumps: Callable[[Any], Any] = json.dumps,
242
+ json_loads: Callable[[str | bytes], Any] = json.loads,
243
+ ):
244
+ """
245
+ Walk through the event and collect selectors that can be applied to it in a
246
+ PII config. This function is used in the UI to provide auto-completion of
247
+ selectors.
248
+ """
249
+ raw_event = encode_str(json_dumps(event))
250
+ raw_rv = rustcall(lib.relay_pii_selector_suggestions_from_event, raw_event)
251
+ return json_loads(decode_str(raw_rv, free=True))
252
+
253
+
254
+ def parse_release(release, json_loads: Callable[[str | bytes], Any] = json.loads):
255
+ """Parses a release string into a dictionary of its components."""
256
+ return json_loads(
257
+ decode_str(rustcall(lib.relay_parse_release, encode_str(release)), free=True)
258
+ )
259
+
260
+
261
+ def compare_version(a, b):
262
+ """Compares two versions with each other and returns 1/0/-1."""
263
+ return rustcall(lib.relay_compare_versions, encode_str(a), encode_str(b))
264
+
265
+
266
+ def validate_sampling_condition(condition):
267
+ """
268
+ Deprecated legacy alias. Please use ``validate_rule_condition`` instead.
269
+ """
270
+ return validate_rule_condition(condition)
271
+
272
+
273
+ def validate_rule_condition(condition):
274
+ """
275
+ Validate a dynamic rule condition. Used by dynamic sampling, metric extraction, and metric
276
+ tagging.
277
+
278
+ :param condition: A string containing the condition encoded as JSON.
279
+ """
280
+ assert isinstance(condition, str)
281
+ raw_error = rustcall(lib.relay_validate_rule_condition, encode_str(condition))
282
+ error = decode_str(raw_error, free=True)
283
+ if error:
284
+ raise ValueError(error)
285
+
286
+
287
+ def validate_sampling_configuration(condition):
288
+ """
289
+ Validate the whole sampling configuration. Used in dynamic sampling serializer.
290
+ The parameter is a string containing the rules configuration as JSON.
291
+ """
292
+ assert isinstance(condition, str)
293
+ raw_error = rustcall(
294
+ lib.relay_validate_sampling_configuration, encode_str(condition)
295
+ )
296
+ error = decode_str(raw_error, free=True)
297
+ if error:
298
+ raise ValueError(error)
299
+
300
+
301
+ def normalize_project_config(
302
+ config,
303
+ json_dumps: Callable[[Any], Any] = json.dumps,
304
+ json_loads: Callable[[str | bytes], Any] = json.loads,
305
+ ):
306
+ """Normalize a project config.
307
+
308
+ :param config: the project config to validate.
309
+ :param json_dumps: a function that stringifies python objects
310
+ :param json_loads: a function that parses and converts JSON strings
311
+ """
312
+ serialized = json_dumps(config)
313
+ normalized = rustcall(lib.relay_normalize_project_config, encode_str(serialized))
314
+ rv = decode_str(normalized, free=True)
315
+ try:
316
+ return json_loads(rv)
317
+ except Exception:
318
+ # Catch all errors since json.loads implementation can change.
319
+ raise ValueError(rv)
320
+
321
+
322
+ def normalize_cardinality_limit_config(
323
+ config,
324
+ json_dumps: Callable[[Any], Any] = json.dumps,
325
+ json_loads: Callable[[str | bytes], Any] = json.loads,
326
+ ):
327
+ """Normalize the cardinality limit config.
328
+
329
+ Normalization consists of deserializing and serializing back the given
330
+ cardinality limit config. If deserializing fails, throw an exception. Note that even if
331
+ the roundtrip doesn't produce errors, the given config may differ from
332
+ normalized one.
333
+
334
+ :param config: the cardinality limit config to validate.
335
+ :param json_dumps: a function that stringifies python objects
336
+ :param json_loads: a function that parses and converts JSON strings
337
+ """
338
+ serialized = json_dumps(config)
339
+ normalized = rustcall(
340
+ lib.normalize_cardinality_limit_config, encode_str(serialized)
341
+ )
342
+ rv = decode_str(normalized, free=True)
343
+ try:
344
+ return json_loads(rv)
345
+ except Exception:
346
+ # Catch all errors since json.loads implementation can change.
347
+ raise ValueError(rv)
348
+
349
+
350
+ def normalize_global_config(
351
+ config,
352
+ json_dumps: Callable[[Any], Any] = json.dumps,
353
+ json_loads: Callable[[str | bytes], Any] = json.loads,
354
+ ):
355
+ """Normalize the global config.
356
+
357
+ Normalization consists of deserializing and serializing back the given
358
+ global config. If deserializing fails, throw an exception. Note that even if
359
+ the roundtrip doesn't produce errors, the given config may differ from
360
+ normalized one.
361
+
362
+ :param config: the global config to validate.
363
+ :param json_dumps: a function that stringifies python objects
364
+ :param json_loads: a function that parses and converts JSON strings
365
+ """
366
+ serialized = json_dumps(config)
367
+ normalized = rustcall(lib.relay_normalize_global_config, encode_str(serialized))
368
+ rv = decode_str(normalized, free=True)
369
+ try:
370
+ return json_loads(rv)
371
+ except Exception:
372
+ # Catch all errors since json.loads implementation can change.
373
+ raise ValueError(rv)
File without changes
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import uuid
5
+ import weakref
6
+ from sentry_relay._lowlevel import ffi, lib
7
+ from sentry_relay.exceptions import exceptions_by_code, RelayError
8
+
9
+
10
+ attached_refs: weakref.WeakKeyDictionary[object, bytes]
11
+ attached_refs = weakref.WeakKeyDictionary()
12
+
13
+
14
+ lib.relay_init()
15
+ os.environ["RUST_BACKTRACE"] = "1"
16
+
17
+
18
+ class _NoDict(type):
19
+ def __new__(cls, name, bases, d):
20
+ d.setdefault("__slots__", ())
21
+ return type.__new__(cls, name, bases, d)
22
+
23
+
24
+ def rustcall(func, *args):
25
+ """Calls rust method and does some error handling."""
26
+ lib.relay_err_clear()
27
+ rv = func(*args)
28
+ err = lib.relay_err_get_last_code()
29
+ if not err:
30
+ return rv
31
+ msg = lib.relay_err_get_last_message()
32
+ cls = exceptions_by_code.get(err, RelayError)
33
+ exc = cls(decode_str(msg, free=True))
34
+ backtrace = decode_str(lib.relay_err_get_backtrace(), free=True)
35
+ if backtrace:
36
+ exc.rust_info = backtrace
37
+ raise exc
38
+
39
+
40
+ class RustObject(metaclass=_NoDict):
41
+ __slots__ = ["_objptr", "_shared"]
42
+ __dealloc_func__ = None
43
+
44
+ def __init__(self):
45
+ raise TypeError("Cannot instanciate %r objects" % self.__class__.__name__)
46
+
47
+ @classmethod
48
+ def _from_objptr(cls, ptr, shared=False):
49
+ rv = object.__new__(cls)
50
+ rv._objptr = ptr
51
+ rv._shared = shared
52
+ return rv
53
+
54
+ def _methodcall(self, func, *args):
55
+ return rustcall(func, self._get_objptr(), *args)
56
+
57
+ def _get_objptr(self):
58
+ if not self._objptr:
59
+ raise RuntimeError("Object is closed")
60
+ return self._objptr
61
+
62
+ def __del__(self):
63
+ if rustcall is None:
64
+ # Interpreter is shutting down and our memory management utils are
65
+ # gone. Just give up, the process is going away anyway.
66
+ return
67
+
68
+ if self._objptr is None or self._shared:
69
+ return
70
+ f = self.__class__.__dealloc_func__
71
+ if f is not None:
72
+ rustcall(f, self._objptr)
73
+ self._objptr = None
74
+
75
+
76
+ def decode_str(s, free=False):
77
+ """Decodes a RelayStr"""
78
+ try:
79
+ if s.len == 0:
80
+ return ""
81
+ return ffi.unpack(s.data, s.len).decode("utf-8", "replace")
82
+ finally:
83
+ if free and s.owned:
84
+ lib.relay_str_free(ffi.addressof(s))
85
+
86
+
87
+ def encode_str(s, mutable=False):
88
+ """Encodes a RelayStr"""
89
+ rv = ffi.new("RelayStr *")
90
+ if isinstance(s, str):
91
+ s = s.encode("utf-8")
92
+ if mutable:
93
+ s = bytearray(s)
94
+ rv.data = ffi.from_buffer(s)
95
+ rv.len = len(s)
96
+ # we have to hold a weak reference here to ensure our string does not
97
+ # get collected before the string is used.
98
+ attached_refs[rv] = s
99
+ return rv
100
+
101
+
102
+ def make_buf(value):
103
+ buf = memoryview(bytes(value))
104
+ rv = ffi.new("RelayBuf *")
105
+ rv.data = ffi.from_buffer(buf)
106
+ rv.len = len(buf)
107
+ attached_refs[rv] = buf
108
+ return rv
109
+
110
+
111
+ def decode_uuid(value):
112
+ """Decodes the given uuid value."""
113
+ return uuid.UUID(bytes=bytes(bytearray(ffi.unpack(value.data, 16))))
@@ -0,0 +1,121 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ import atexit
5
+ import shutil
6
+ import zipfile
7
+ import tempfile
8
+ import subprocess
9
+ from setuptools import setup, find_packages
10
+ from distutils.command.sdist import sdist
11
+
12
+
13
+ _version_re = re.compile(r'(?m)^version\s*=\s*"(.*?)"\s*$')
14
+
15
+
16
+ DEBUG_BUILD = os.environ.get("RELAY_DEBUG") == "1"
17
+
18
+ with open("README", encoding="UTF-8") as f:
19
+ readme = f.read()
20
+
21
+
22
+ if os.path.isfile("../relay-cabi/Cargo.toml"):
23
+ with open("../relay-cabi/Cargo.toml") as f:
24
+ match = _version_re.search(f.read())
25
+ assert match is not None
26
+ version = match[1]
27
+ else:
28
+ with open("version.txt") as f:
29
+ version = f.readline().strip()
30
+
31
+
32
+ def vendor_rust_deps():
33
+ subprocess.Popen(["scripts/git-archive-all", "py/rustsrc.zip"], cwd="..").wait()
34
+
35
+
36
+ def write_version():
37
+ with open("version.txt", "wb") as f:
38
+ f.write(("%s\n" % version).encode())
39
+
40
+
41
+ class CustomSDist(sdist):
42
+ def run(self):
43
+ vendor_rust_deps()
44
+ write_version()
45
+ sdist.run(self)
46
+
47
+
48
+ def build_native(spec):
49
+ cmd = ["cargo", "build", "-p", "relay-cabi"]
50
+ if not DEBUG_BUILD:
51
+ cmd.extend(("--profile", "release-cabi"))
52
+ target = "release-cabi"
53
+ else:
54
+ target = "debug"
55
+
56
+ # Step 0: find rust sources
57
+ if not os.path.isfile("../relay-cabi/Cargo.toml"):
58
+ scratchpad = tempfile.mkdtemp()
59
+
60
+ @atexit.register
61
+ def delete_scratchpad():
62
+ try:
63
+ shutil.rmtree(scratchpad)
64
+ except OSError:
65
+ pass
66
+
67
+ zf = zipfile.ZipFile("rustsrc.zip")
68
+ zf.extractall(scratchpad)
69
+ rust_path = scratchpad + "/rustsrc"
70
+ else:
71
+ rust_path = ".."
72
+ scratchpad = None
73
+
74
+ # if the lib already built we replace the command
75
+ if os.environ.get("SKIP_RELAY_LIB_BUILD") is not None:
76
+ cmd = ["echo", "'Use pre-built library.'"]
77
+
78
+ # Step 1: build the rust library
79
+ build = spec.add_external_build(cmd=cmd, path=rust_path)
80
+
81
+ def find_dylib():
82
+ cargo_target = os.environ.get("CARGO_BUILD_TARGET")
83
+ if cargo_target:
84
+ in_path = f"target/{cargo_target}/{target}"
85
+ else:
86
+ in_path = "target/%s" % target
87
+ return build.find_dylib("relay_cabi", in_path=in_path)
88
+
89
+ rtld_flags = ["NOW"]
90
+ if sys.platform == "darwin":
91
+ rtld_flags.append("NODELETE")
92
+ spec.add_cffi_module(
93
+ module_path="sentry_relay._lowlevel",
94
+ dylib=find_dylib,
95
+ header_filename=lambda: build.find_header(
96
+ "relay.h", in_path="relay-cabi/include"
97
+ ),
98
+ rtld_flags=rtld_flags,
99
+ )
100
+
101
+
102
+ setup(
103
+ name="sentry-relay",
104
+ version=version,
105
+ packages=find_packages(),
106
+ author="Sentry",
107
+ license="FSL-1.0-Apache-2.0",
108
+ author_email="hello@sentry.io",
109
+ description="A python library to access sentry relay functionality.",
110
+ long_description=readme,
111
+ long_description_content_type="text/markdown",
112
+ include_package_data=True,
113
+ package_data={"sentry_relay": ["py.typed", "_lowlevel.pyi"]},
114
+ zip_safe=False,
115
+ platforms="any",
116
+ python_requires=">=3.10",
117
+ install_requires=["milksnake>=0.1.6"],
118
+ setup_requires=["milksnake>=0.1.6"],
119
+ milksnake_tasks=[build_native],
120
+ cmdclass={"sdist": CustomSDist}, # type: ignore
121
+ )
@@ -0,0 +1 @@
1
+ 0.9.9