sentry-nodestore-elastic 1.0.2__py3-none-any.whl → 1.1.0__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.
@@ -1,37 +1,78 @@
1
1
  import base64
2
2
  from datetime import datetime, timezone
3
3
  import logging
4
+ import re
4
5
  import zlib
6
+ from typing import Optional, List, Any
5
7
  import elasticsearch
8
+ from elasticsearch import Elasticsearch
6
9
  from sentry.nodestore.base import NodeStorage
7
10
 
8
- class ElasticNodeStorage(NodeStorage):
9
- logger = logging.getLogger("sentry.nodestore.elastic")
11
+ logger = logging.getLogger("sentry.nodestore.elastic")
12
+
10
13
 
14
+ class ElasticNodeStorage(NodeStorage):
15
+ """
16
+ Elasticsearch backend for Sentry nodestore.
17
+
18
+ This backend stores Sentry node objects in Elasticsearch instead of PostgreSQL,
19
+ providing better scalability and performance for high-load environments.
20
+ """
21
+
22
+ logger = logger
11
23
  encoding = 'utf-8'
24
+
25
+ # Index name pattern for date-based indices
26
+ INDEX_DATE_PATTERN = re.compile(r'^sentry-(\d{4}-\d{2}-\d{2})')
12
27
 
13
28
  def __init__(
14
29
  self,
15
- es,
16
- index='sentry-{date}',
17
- refresh=False,
18
- template_name='sentry',
19
- alias_name='sentry',
20
- validate_es=False,
21
- ):
30
+ es: Elasticsearch,
31
+ index: str = 'sentry-{date}',
32
+ refresh: bool = False,
33
+ template_name: str = 'sentry',
34
+ alias_name: str = 'sentry',
35
+ validate_es: bool = False,
36
+ ) -> None:
37
+ """
38
+ Initialize Elasticsearch nodestore backend.
39
+
40
+ Args:
41
+ es: Elasticsearch client instance
42
+ index: Index name pattern with {date} placeholder (default: 'sentry-{date}')
43
+ refresh: Whether to refresh index after writes (default: False for better performance)
44
+ template_name: Name of the index template (default: 'sentry')
45
+ alias_name: Name of the index alias (default: 'sentry')
46
+ validate_es: Whether to validate Elasticsearch connection on init (default: False)
47
+ """
48
+ if not isinstance(es, Elasticsearch):
49
+ raise TypeError("es parameter must be an Elasticsearch client instance")
50
+
22
51
  self.es = es
23
52
  self.index = index
24
53
  self.refresh = refresh
25
54
  self.template_name = template_name
26
55
  self.alias_name = alias_name
27
56
  self.validate_es = validate_es
57
+
58
+ if self.validate_es:
59
+ try:
60
+ self.es.info()
61
+ except Exception as e:
62
+ raise ConnectionError(f"Failed to connect to Elasticsearch: {e}") from e
28
63
 
29
64
  super(ElasticNodeStorage, self).__init__()
30
65
 
31
- def bootstrap(self):
66
+ def bootstrap(self) -> None:
67
+ """
68
+ Bootstrap Elasticsearch index template.
69
+
70
+ Creates an index template if it doesn't exist. Does not overwrite
71
+ existing templates to allow manual customization.
72
+ """
32
73
  try:
33
- # do not owerwrite existing template with same name
34
- # it may have been changed in elastic manually after creation
74
+ # Do not overwrite existing template with same name
75
+ # It may have been changed in elastic manually after creation
35
76
  # or created manually before sentry initialization
36
77
  self.es.indices.get_index_template(name=self.template_name)
37
78
  self.logger.info(
@@ -49,144 +90,298 @@ class ElasticNodeStorage(NodeStorage):
49
90
  "status": "not found"
50
91
  }
51
92
  )
52
- self.es.indices.put_index_template(
53
- create = True,
54
- name = self.template_name,
55
- index_patterns = [
56
- "sentry-*"
57
- ],
58
- template = {
59
- "settings": {
60
- "index": {
61
- "number_of_shards": 3,
62
- "number_of_replicas": 0
63
- }
64
- },
65
- "mappings": {
66
- "_source": {
67
- "enabled": False
93
+ try:
94
+ self.es.indices.put_index_template(
95
+ create=True,
96
+ name=self.template_name,
97
+ index_patterns=["sentry-*"],
98
+ template={
99
+ "settings": {
100
+ "index": {
101
+ "number_of_shards": 3,
102
+ "number_of_replicas": 0
103
+ }
68
104
  },
69
- "dynamic": "false",
70
- "dynamic_templates": [],
71
- "properties": {
72
- "data": {
73
- "type": "text",
74
- "index": False,
75
- "store": True
105
+ "mappings": {
106
+ "_source": {
107
+ "enabled": False
76
108
  },
77
- "timestamp": {
78
- "type": "date",
79
- "store": True
109
+ "dynamic": "false",
110
+ "dynamic_templates": [],
111
+ "properties": {
112
+ "data": {
113
+ "type": "text",
114
+ "index": False,
115
+ "store": True
116
+ },
117
+ "timestamp": {
118
+ "type": "date",
119
+ "store": True
120
+ }
80
121
  }
122
+ },
123
+ "aliases": {
124
+ self.alias_name: {}
81
125
  }
82
- },
83
- "aliases": {
84
- self.alias_name: {}
85
126
  }
86
- }
87
- )
88
- self.logger.info(
89
- "bootstrap.template.create",
90
- extra={
91
- "template": self.template_name,
92
- "alias": self.alias_name
93
- }
94
- )
127
+ )
128
+ self.logger.info(
129
+ "bootstrap.template.create",
130
+ extra={
131
+ "template": self.template_name,
132
+ "alias": self.alias_name
133
+ }
134
+ )
135
+ except elasticsearch.exceptions.RequestError as e:
136
+ self.logger.error(
137
+ "bootstrap.template.create.error",
138
+ extra={
139
+ "template": self.template_name,
140
+ "error": str(e)
141
+ },
142
+ exc_info=True
143
+ )
144
+ raise
95
145
 
96
- def _get_write_index(self):
97
- return self.index.format(date=datetime.today().strftime('%Y-%m-%d'))
146
+ def _get_write_index(self) -> str:
147
+ """Get the index name for writing based on current date."""
148
+ return self.index.format(date=datetime.now(timezone.utc).strftime('%Y-%m-%d'))
98
149
 
99
- def _get_read_index(self, id):
100
- search = self.es.search(
101
- index=self.alias_name,
102
- body={
103
- "query": {
104
- "term": {
105
- "_id": id
106
- },
107
- },
108
- }
109
- )
110
- if search["hits"]["total"]["value"] == 1:
111
- return search["hits"]["hits"][0]["_index"]
112
- else:
150
+ def _get_read_index(self, id: str) -> Optional[str]:
151
+ """
152
+ Get the index name containing the document with given ID.
153
+
154
+ Optimized to use direct get through alias instead of search query.
155
+ Falls back to search if direct get fails (for backward compatibility).
156
+
157
+ Args:
158
+ id: Document ID to find
159
+
160
+ Returns:
161
+ Index name containing the document, or None if not found
162
+ """
163
+ # Try direct get through alias first (more efficient)
164
+ try:
165
+ # Use _source: false and stored_fields to avoid loading document data
166
+ response = self.es.get(
167
+ id=id,
168
+ index=self.alias_name,
169
+ _source=False,
170
+ stored_fields="_none_"
171
+ )
172
+ return response.get('_index')
173
+ except elasticsearch.exceptions.NotFoundError:
174
+ return None
175
+ except elasticsearch.exceptions.RequestError:
176
+ # Fallback to search if direct get fails (e.g., alias routing issues)
177
+ try:
178
+ search = self.es.search(
179
+ index=self.alias_name,
180
+ body={
181
+ "query": {
182
+ "term": {
183
+ "_id": id
184
+ }
185
+ },
186
+ "size": 1,
187
+ "_source": False
188
+ }
189
+ )
190
+ if search["hits"]["total"]["value"] == 1:
191
+ return search["hits"]["hits"][0]["_index"]
192
+ except Exception as e:
193
+ self.logger.warning(
194
+ "document.get_index.error",
195
+ extra={
196
+ "doc_id": id,
197
+ "error": str(e)
198
+ }
199
+ )
113
200
  return None
114
201
 
115
- def _compress(self, data):
202
+ def _compress(self, data: bytes) -> str:
203
+ """
204
+ Compress and encode data for storage.
205
+
206
+ Args:
207
+ data: Raw bytes to compress
208
+
209
+ Returns:
210
+ Base64-encoded compressed string
211
+ """
212
+ if not isinstance(data, bytes):
213
+ raise TypeError(f"data must be bytes, got {type(data)}")
116
214
  return base64.b64encode(zlib.compress(data)).decode(self.encoding)
117
215
 
118
- def _decompress(self, data):
119
- return zlib.decompress(base64.b64decode(data))
120
-
121
- def delete(self, id):
216
+ def _decompress(self, data: str) -> bytes:
122
217
  """
123
- >>> nodestore.delete('key1')
218
+ Decompress and decode data from storage.
219
+
220
+ Args:
221
+ data: Base64-encoded compressed string
222
+
223
+ Returns:
224
+ Decompressed bytes
124
225
  """
226
+ if not isinstance(data, str):
227
+ raise TypeError(f"data must be str, got {type(data)}")
228
+ try:
229
+ return zlib.decompress(base64.b64decode(data))
230
+ except (ValueError, zlib.error) as e:
231
+ raise ValueError(f"Failed to decompress data: {e}") from e
125
232
 
233
+ def delete(self, id: str) -> None:
234
+ """
235
+ Delete a node by ID.
236
+
237
+ Args:
238
+ id: Document ID to delete
239
+
240
+ Example:
241
+ >>> nodestore.delete('key1')
242
+ """
243
+ if not id:
244
+ raise ValueError("id cannot be empty")
245
+
126
246
  try:
247
+ # Use direct delete instead of delete_by_query for better performance
248
+ index = self._get_read_index(id)
249
+ if index:
250
+ self.es.delete(id=id, index=index, refresh=self.refresh)
251
+ else:
252
+ # Fallback to delete_by_query if index not found
253
+ self.es.delete_by_query(
254
+ index=self.alias_name,
255
+ query={
256
+ "term": {
257
+ "_id": id
258
+ }
259
+ }
260
+ )
127
261
  self.logger.info(
128
262
  "document.delete.executed",
129
263
  extra={
130
264
  "doc_id": id
131
265
  }
132
266
  )
133
- self.es.delete_by_query(
134
- index=self.alias_name,
135
- query = {
136
- "term": {
137
- "_id": id
138
- }
139
- }
140
- )
141
267
  except elasticsearch.exceptions.NotFoundError:
268
+ # Document doesn't exist, which is fine
142
269
  pass
143
270
  except elasticsearch.exceptions.ConflictError:
271
+ # Concurrent deletion, which is fine
144
272
  pass
273
+ except Exception as e:
274
+ self.logger.error(
275
+ "document.delete.error",
276
+ extra={
277
+ "doc_id": id,
278
+ "error": str(e)
279
+ },
280
+ exc_info=True
281
+ )
282
+ raise
145
283
 
146
- def delete_multi(self, id_list):
284
+ def delete_multi(self, id_list: List[str]) -> None:
147
285
  """
148
286
  Delete multiple nodes.
287
+
149
288
  Note: This is not guaranteed to be atomic and may result in a partial
150
289
  delete.
151
- >>> delete_multi(['key1', 'key2'])
290
+
291
+ Args:
292
+ id_list: List of document IDs to delete
293
+
294
+ Example:
295
+ >>> delete_multi(['key1', 'key2'])
152
296
  """
153
-
297
+ if not id_list:
298
+ return
299
+
300
+ if not isinstance(id_list, list):
301
+ raise TypeError(f"id_list must be a list, got {type(id_list)}")
302
+
154
303
  try:
155
304
  response = self.es.delete_by_query(
156
305
  index=self.alias_name,
157
- query = {
306
+ query={
158
307
  "ids": {
159
308
  "values": id_list
160
309
  }
161
- }
310
+ },
311
+ refresh=self.refresh
162
312
  )
163
313
  self.logger.info(
164
314
  "document.delete_multi.executed",
165
315
  extra={
166
316
  "docs_to_delete": len(id_list),
167
- "docs_deleted": response["deleted"]
317
+ "docs_deleted": response.get("deleted", 0)
168
318
  }
169
319
  )
170
320
  except elasticsearch.exceptions.NotFoundError:
321
+ # Indices don't exist, which is fine
171
322
  pass
172
323
  except elasticsearch.exceptions.ConflictError:
324
+ # Concurrent deletion, which is fine
173
325
  pass
326
+ except Exception as e:
327
+ self.logger.error(
328
+ "document.delete_multi.error",
329
+ extra={
330
+ "docs_to_delete": len(id_list),
331
+ "error": str(e)
332
+ },
333
+ exc_info=True
334
+ )
335
+ raise
174
336
 
175
337
 
176
- def _get_bytes(self, id):
338
+ def _get_bytes(self, id: str) -> Optional[bytes]:
177
339
  """
178
- >>> nodestore._get_bytes('key1')
179
- b'{"message": "hello world"}'
340
+ Get raw bytes for a node by ID.
341
+
342
+ Args:
343
+ id: Document ID to retrieve
344
+
345
+ Returns:
346
+ Decompressed bytes, or None if not found
347
+
348
+ Example:
349
+ >>> nodestore._get_bytes('key1')
350
+ b'{"message": "hello world"}'
180
351
  """
352
+ if not id:
353
+ return None
354
+
181
355
  index = self._get_read_index(id)
182
356
 
183
357
  if index is not None:
184
358
  try:
185
359
  response = self.es.get(id=id, index=index, stored_fields=["data"])
360
+ if 'fields' in response and 'data' in response['fields']:
361
+ return self._decompress(response['fields']['data'][0])
362
+ else:
363
+ self.logger.warning(
364
+ "document.get.warning",
365
+ extra={
366
+ "doc_id": id,
367
+ "index": index,
368
+ "error": "data field not found in response"
369
+ }
370
+ )
371
+ return None
186
372
  except elasticsearch.exceptions.NotFoundError:
187
373
  return None
188
- else:
189
- return self._decompress(response['fields']['data'][0])
374
+ except Exception as e:
375
+ self.logger.error(
376
+ "document.get.error",
377
+ extra={
378
+ "doc_id": id,
379
+ "index": index,
380
+ "error": str(e)
381
+ },
382
+ exc_info=True
383
+ )
384
+ return None
190
385
  else:
191
386
  self.logger.warning(
192
387
  "document.get.warning",
@@ -198,44 +393,142 @@ class ElasticNodeStorage(NodeStorage):
198
393
  return None
199
394
 
200
395
 
201
- def _set_bytes(self, id, data, ttl=None):
396
+ def _set_bytes(self, id: str, data: bytes, ttl: Optional[int] = None) -> None:
202
397
  """
203
- >>> nodestore.set('key1', b"{'foo': 'bar'}")
398
+ Set raw bytes for a node by ID.
399
+
400
+ Args:
401
+ id: Document ID
402
+ data: Raw bytes to store
403
+ ttl: Time to live in seconds (not currently used, reserved for future use)
404
+
405
+ Example:
406
+ >>> nodestore._set_bytes('key1', b"{'foo': 'bar'}")
204
407
  """
408
+ if not id:
409
+ raise ValueError("id cannot be empty")
410
+
411
+ if not isinstance(data, bytes):
412
+ raise TypeError(f"data must be bytes, got {type(data)}")
413
+
205
414
  index = self._get_write_index()
206
- self.es.index(
207
- id=id,
208
- index=index,
209
- document={'data': self._compress(data), 'timestamp': datetime.utcnow().isoformat()},
210
- refresh=self.refresh,
211
- )
415
+ try:
416
+ self.es.index(
417
+ id=id,
418
+ index=index,
419
+ document={
420
+ 'data': self._compress(data),
421
+ 'timestamp': datetime.now(timezone.utc).isoformat()
422
+ },
423
+ refresh=self.refresh,
424
+ )
425
+ except Exception as e:
426
+ self.logger.error(
427
+ "document.set.error",
428
+ extra={
429
+ "doc_id": id,
430
+ "index": index,
431
+ "error": str(e)
432
+ },
433
+ exc_info=True
434
+ )
435
+ raise
212
436
 
213
- def cleanup(self, cutoff: datetime):
214
- for index in self.es.indices.get_alias(index=self.alias_name):
215
- # parse date from manually changed indices after reindex
216
- # (they may have postfixes like '-fixed' or '-reindex')
217
- index_date = '-'.join(index.split('-')[1:4])
218
- index_ts = datetime.strptime(index_date, "%Y-%m-%d").replace(
219
- tzinfo=timezone.utc
220
- )
221
- if index_ts < cutoff:
222
- try:
223
- self.es.indices.delete(index=index)
224
- except elasticsearch.exceptions.NotFoundError:
225
- self.logger.info(
226
- "index.delete.error",
227
- extra={
228
- "index": index,
229
- "error": "not found"
230
- }
231
- )
232
- else:
233
- self.logger.info(
234
- "index.delete.executed",
235
- extra={
236
- "index": index,
237
- "index_ts": index_ts.timestamp(),
238
- "cutoff_ts": cutoff.timestamp(),
239
- "status": "deleted"
240
- }
241
- )
437
+ def cleanup(self, cutoff: datetime) -> None:
438
+ """
439
+ Clean up indices older than the cutoff date.
440
+
441
+ Args:
442
+ cutoff: Datetime threshold - indices older than this will be deleted
443
+ """
444
+ if not isinstance(cutoff, datetime):
445
+ raise TypeError(f"cutoff must be a datetime object, got {type(cutoff)}")
446
+
447
+ # Ensure cutoff is timezone-aware
448
+ if cutoff.tzinfo is None:
449
+ cutoff = cutoff.replace(tzinfo=timezone.utc)
450
+
451
+ try:
452
+ alias_indices = self.es.indices.get_alias(index=self.alias_name)
453
+ except elasticsearch.exceptions.NotFoundError:
454
+ self.logger.warning(
455
+ "cleanup.alias.not_found",
456
+ extra={
457
+ "alias": self.alias_name
458
+ }
459
+ )
460
+ return
461
+
462
+ deleted_count = 0
463
+ skipped_count = 0
464
+
465
+ for index in alias_indices:
466
+ # Parse date from index name using regex for more robust parsing
467
+ # Handles indices with postfixes like '-fixed' or '-reindex'
468
+ match = self.INDEX_DATE_PATTERN.match(index)
469
+ if not match:
470
+ self.logger.warning(
471
+ "cleanup.index.skip",
472
+ extra={
473
+ "index": index,
474
+ "reason": "index name does not match expected pattern"
475
+ }
476
+ )
477
+ skipped_count += 1
478
+ continue
479
+
480
+ try:
481
+ index_date_str = match.group(1)
482
+ index_ts = datetime.strptime(index_date_str, "%Y-%m-%d").replace(
483
+ tzinfo=timezone.utc
484
+ )
485
+
486
+ if index_ts < cutoff:
487
+ try:
488
+ self.es.indices.delete(index=index)
489
+ deleted_count += 1
490
+ self.logger.info(
491
+ "index.delete.executed",
492
+ extra={
493
+ "index": index,
494
+ "index_ts": index_ts.timestamp(),
495
+ "cutoff_ts": cutoff.timestamp(),
496
+ "status": "deleted"
497
+ }
498
+ )
499
+ except elasticsearch.exceptions.NotFoundError:
500
+ self.logger.info(
501
+ "index.delete.error",
502
+ extra={
503
+ "index": index,
504
+ "error": "not found"
505
+ }
506
+ )
507
+ except Exception as e:
508
+ self.logger.error(
509
+ "index.delete.error",
510
+ extra={
511
+ "index": index,
512
+ "error": str(e)
513
+ },
514
+ exc_info=True
515
+ )
516
+ except ValueError as e:
517
+ self.logger.warning(
518
+ "cleanup.index.skip",
519
+ extra={
520
+ "index": index,
521
+ "reason": f"failed to parse date: {e}"
522
+ }
523
+ )
524
+ skipped_count += 1
525
+
526
+ self.logger.info(
527
+ "cleanup.completed",
528
+ extra={
529
+ "cutoff_ts": cutoff.timestamp(),
530
+ "deleted_count": deleted_count,
531
+ "skipped_count": skipped_count,
532
+ "total_checked": len(alias_indices)
533
+ }
534
+ )
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: sentry-nodestore-elastic
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: Sentry nodestore Elasticsearch backend
5
5
  Home-page: https://github.com/andrsp/sentry-nodestore-elastic
6
6
  Author: andrsp@gmail.com
@@ -19,8 +19,8 @@ Classifier: Programming Language :: Python
19
19
  Classifier: Operating System :: OS Independent
20
20
  Description-Content-Type: text/markdown
21
21
  License-File: LICENSE
22
- Requires-Dist: sentry==25.*
23
- Requires-Dist: elasticsearch==8.*
22
+ Requires-Dist: sentry<27.0.0,>=26.1.0
23
+ Requires-Dist: elasticsearch<9.0.0,>=8.0.0
24
24
  Dynamic: author
25
25
  Dynamic: author-email
26
26
  Dynamic: classifier
@@ -29,6 +29,7 @@ Dynamic: description-content-type
29
29
  Dynamic: home-page
30
30
  Dynamic: keywords
31
31
  Dynamic: license
32
+ Dynamic: license-file
32
33
  Dynamic: project-url
33
34
  Dynamic: requires-dist
34
35
  Dynamic: summary
@@ -39,7 +40,7 @@ Sentry nodestore Elasticsearch backend
39
40
 
40
41
  [![image](https://img.shields.io/pypi/v/sentry-nodestore-elastic.svg)](https://pypi.python.org/pypi/sentry-nodestore-elastic)
41
42
 
42
- Supported Sentry 25.x & elasticsearch 8.x versions
43
+ Supported Sentry 26.1.0+ & elasticsearch 8.x versions
43
44
 
44
45
  Use Elasticsearch cluster for store node objects from Sentry
45
46
 
@@ -56,7 +57,7 @@ Switching nodestore to dedicated Elasticsearch cluster provides more scalability
56
57
  Rebuild sentry docker image with nodestore package installation
57
58
 
58
59
  ``` shell
59
- FROM getsentry/sentry:25.1.0
60
+ FROM getsentry/sentry:26.1.0
60
61
  RUN pip install sentry-nodestore-elastic
61
62
  ```
62
63
 
@@ -205,7 +206,7 @@ while True:
205
206
 
206
207
  bulk(es, bulk_data)
207
208
  count = count - 2000
208
- print(f"Remainig rows: {count}")
209
+ print(f"Remaining rows: {count}")
209
210
 
210
211
  cursor.close()
211
212
  conn.close()
@@ -0,0 +1,7 @@
1
+ sentry_nodestore_elastic/__init__.py,sha256=vU-X62MDmPtTKab1xRiCrZl2MwOsbPX0kSXpGV7hAHk,64
2
+ sentry_nodestore_elastic/backend.py,sha256=GJDrmf2wILJJGLXtU1UJpOcuiuStmchYYOCOPulRJIc,18294
3
+ sentry_nodestore_elastic-1.1.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
4
+ sentry_nodestore_elastic-1.1.0.dist-info/METADATA,sha256=zoJd9BNYcraXaVfiO7PVm3kUe8Kg_feRxl96lccwLck,6230
5
+ sentry_nodestore_elastic-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
6
+ sentry_nodestore_elastic-1.1.0.dist-info/top_level.txt,sha256=PFv5ZH9Um8naXLk3uknqoowcfN-K8jOpI98smdVpSWQ,25
7
+ sentry_nodestore_elastic-1.1.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,7 +0,0 @@
1
- sentry_nodestore_elastic/__init__.py,sha256=vU-X62MDmPtTKab1xRiCrZl2MwOsbPX0kSXpGV7hAHk,64
2
- sentry_nodestore_elastic/backend.py,sha256=e48_3CQdBs46YqJ3oHBsRwRe3JBjLA5tiNY75XrweQs,7676
3
- sentry_nodestore_elastic-1.0.2.dist-info/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
4
- sentry_nodestore_elastic-1.0.2.dist-info/METADATA,sha256=nj8bYkhykM-28n9fqLpGx7Ib79YO9CUumGG4AhKrwRU,6185
5
- sentry_nodestore_elastic-1.0.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
6
- sentry_nodestore_elastic-1.0.2.dist-info/top_level.txt,sha256=PFv5ZH9Um8naXLk3uknqoowcfN-K8jOpI98smdVpSWQ,25
7
- sentry_nodestore_elastic-1.0.2.dist-info/RECORD,,