brynq-sdk-elastic 3.0.2__tar.gz → 3.0.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.0
2
2
  Name: brynq_sdk_elastic
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: elastic wrapper from BrynQ
5
5
  Home-page: UNKNOWN
6
6
  Author: BrynQ
@@ -0,0 +1 @@
1
+ from .elastic import Elastic
@@ -0,0 +1,431 @@
1
+ import warnings
2
+ import requests
3
+ import json
4
+ import datetime
5
+ import string
6
+ import random
7
+ import pandas as pd
8
+ import os
9
+
10
+
11
+ class Elastic:
12
+ def __init__(self, api_key: str = None, customer_name: str = None, space_name: str = None, disabled: bool = False):
13
+ """
14
+ A package to create indexes, users, roles, getting data, etc.
15
+ :param api_key: The api key to connect to elasticsearch if not provided in the .env file
16
+ """
17
+ try:
18
+ self.verify = False
19
+ self.disabled = disabled
20
+ self.timeout = 60 # seconds before request to elastic fails
21
+ elasticsearch_host = os.getenv("ELASTIC_HOST")
22
+ elasticsearch_port = os.getenv("ELASTIC_PORT")
23
+ kibana_port = os.getenv("KIBANA_PORT")
24
+ elastic_token = os.getenv('ELASTIC_API_KEY', api_key)
25
+
26
+ if not self.disabled:
27
+ # Check for missing environment variables and show warnings
28
+ if elasticsearch_host is None:
29
+ raise KeyError("Environment variable ELASTIC_HOST is not set. Please set it and try again")
30
+ if elasticsearch_port is None:
31
+ elasticsearch_port = 9200
32
+ warnings.warn("Environment variable ELASTIC_PORT is not set. Using default port 9200")
33
+ if kibana_port is None:
34
+ kibana_port = 5601
35
+ warnings.warn("Environment variable KIBANA_PORT is not set. Using default port 5601")
36
+ if elastic_token is None:
37
+ raise KeyError("Environment variable ELASTIC_API_KEY is not set and no api_key is provided. Please specify either one and try again")
38
+ if os.getenv("ELASTIC_SPACE") is None:
39
+ warnings.warn("Environment variable ELASTIC_SPACE is not set. Using 'default'")
40
+
41
+ # Build the host URLs
42
+ self.elasticsearch_host = f'https://{elasticsearch_host}:{elasticsearch_port}'
43
+ self.kibana_host = f'http://{elasticsearch_host}:{kibana_port}'
44
+ self.elastic_token = elastic_token if elastic_token is not None else api_key
45
+ self.space_name = os.getenv('ELASTIC_SPACE', 'default') if space_name is None else space_name
46
+ self.client_user = os.getenv('BRYNQ_SUBDOMAIN', 'default').lower().replace(' ', '_') if customer_name is None else customer_name.lower().replace(' ', '_')
47
+
48
+ if self.client_user == 'default':
49
+ warnings.warn("Environment variable BRYNQ_SUBDOMAIN is not set and customer_name is not specified. Using 'default'")
50
+
51
+ print(f"Elasticsearch running on: {elasticsearch_host}")
52
+
53
+ self.timestamp = int(datetime.datetime.now().timestamp())
54
+ self.elastic_headers = {
55
+ 'Content-Type': 'application/json',
56
+ 'Authorization': f'ApiKey {self.elastic_token}'
57
+ }
58
+ self.kibana_headers = {
59
+ 'Content-Type': 'application/json',
60
+ 'Authorization': f'ApiKey {self.elastic_token}',
61
+ 'kbn-xsrf': 'true'
62
+ }
63
+ if not self.disabled:
64
+ self.get_health()
65
+ self.create_space(space_name=self.space_name)
66
+ except Exception as e:
67
+ raise ConnectionError('Could not establish a connection: {}'.format(str(e)))
68
+
69
+ def get_health(self) -> str:
70
+ """
71
+ Check if a there is a connection with elasticsearch
72
+ :return: if the connection is established or not
73
+ """
74
+ # Get the health of the database connection
75
+ if self.disabled:
76
+ return 'Healthy connection established with elasticsearch!'
77
+
78
+ try:
79
+ health = requests.get(url=f'{self.elasticsearch_host}/_cat/health?', headers=self.elastic_headers, verify=self.verify, timeout=self.timeout).status_code
80
+ if health != 200:
81
+ raise ConnectionError(f"Elasticsearch cluster health check failed with status code: {health}")
82
+ else:
83
+ return 'Healthy connection established with elasticsearch!'
84
+ except Exception as e:
85
+ raise ConnectionError(f'Elasticsearch was not reachable, error is: {e}')
86
+
87
+ def initialize_customer(self):
88
+ # Creates the index for the user if it does not exist yet
89
+ self.create_index(index_name=f'task_execution_log_{self.client_user}')
90
+
91
+ # creates the data view for the space and index if it does not exist yet
92
+ self.create_data_view(space_name=self.space_name, view_name=f'task_execution_log_{self.client_user}', name=f'Task execution log {self.client_user}', time_field='started_at')
93
+
94
+ def create_space(self, space_name: str) -> str:
95
+ """
96
+ This function creates a space in elasticsearch for the current customer
97
+ :param space_name: The name of the space
98
+ :return: The status of the creation of the space
99
+ """
100
+ try:
101
+ if self.disabled:
102
+ return 'Space creation disabled'
103
+
104
+ url = f'{self.kibana_host}/api/spaces/space'
105
+ data = {
106
+ "id": space_name,
107
+ "name": space_name,
108
+ "description": f"This is the space for {space_name}",
109
+ "color": "#aabbcc",
110
+ "initials": space_name[0:2].upper(),
111
+ "disabledFeatures": [],
112
+ }
113
+
114
+ response = requests.head(url=url + fr'/{space_name}', headers=self.kibana_headers, verify=self.verify, timeout=self.timeout)
115
+
116
+ if response.status_code == 200:
117
+ return f'Index \'{space_name}\' already exists'
118
+ else:
119
+ response = requests.post(url=url, headers=self.kibana_headers, data=json.dumps(data), verify=self.verify, timeout=self.timeout)
120
+ if response.status_code == 200:
121
+ return f'space {space_name} created'
122
+ else:
123
+ raise ConnectionError(f'Could not create space {space_name} with status code: {response.status_code}. Response: {response.text}')
124
+ except:
125
+ message = "Could not create space, since this is not strictly necessary to write logs, continue without it"
126
+ print(message)
127
+ return message
128
+
129
+ def create_data_view(self, space_name: str, view_name: str, name: str, time_field: str) -> str:
130
+ """
131
+ This function creates a data view in elasticsearch for the current customer
132
+ :param space_name: The name of the space
133
+ :param view_name: The name of the data view
134
+ :param time_field: The name of the time field
135
+ :return: The status of the creation of the data view
136
+ """
137
+ try:
138
+ if self.disabled:
139
+ return 'Data view creation disabled'
140
+
141
+ url = f'{self.kibana_host}/s/{space_name}/api/data_views/data_view'
142
+ data = {
143
+ "data_view": {
144
+ "title": f'{view_name}*',
145
+ "id": f'{view_name}',
146
+ "name": f'{name}',
147
+ "timeFieldName": time_field
148
+ }
149
+ }
150
+
151
+ response = requests.head(url=url + fr'/{view_name}', headers=self.kibana_headers, verify=self.verify, timeout=self.timeout)
152
+
153
+ if response.status_code == 200:
154
+ return f'Data view \'{view_name}\' already exists'
155
+ else:
156
+ response = requests.post(url=url, headers=self.kibana_headers, data=json.dumps(data), verify=self.verify, timeout=self.timeout)
157
+ if response.status_code == 200:
158
+ return f'data view {view_name} created'
159
+ else:
160
+ raise ConnectionError(f'Could not create data view {view_name} with status code: {response.status_code}. Response: {response.text}')
161
+ except:
162
+ message = "Could not create data view, since this is not strictly necessary to write logs, continue without it"
163
+ print(message)
164
+ return message
165
+
166
+ def get_all_docs_from_index(self, index: str) -> pd.DataFrame:
167
+ """
168
+ Get all the documents from a certain index
169
+ :param index: the name of the index
170
+ :return: The response of the request to elasticsearch
171
+ """
172
+ if self.disabled:
173
+ return pd.DataFrame()
174
+
175
+ size = 10000
176
+
177
+ # Get all indices with the given index from the function parameter. For each day a new index.
178
+ indices = requests.get(url=self.elasticsearch_host + '/' + index + '*/_settings', headers=self.elastic_headers, verify=self.verify, timeout=self.timeout).json()
179
+ index_list = {}
180
+
181
+ for index in indices:
182
+ index_date = datetime.date(2023, 4, 3)
183
+ index_list[str(index_date)] = index
184
+
185
+ url = f'{self.elasticsearch_host}/{index}/_search'
186
+
187
+ # initial request
188
+ params = {"size": size, "scroll": "10m"}
189
+ response = requests.get(url=url, headers=self.elastic_headers, params=params, verify=self.verify, timeout=self.timeout).json()
190
+
191
+ # next requests until finished
192
+ scroll_id = response['_scroll_id']
193
+ total = response['hits']['total']['value']
194
+ response = pd.json_normalize(response['hits']['hits'])
195
+ response.drop(['_id', '_index', '_score'], axis=1, inplace=True)
196
+
197
+ # start all the request to elastic based on the scroll_id and add to the initial response
198
+ loop_boolean = True
199
+ body = json.dumps({"scroll": "10m", "scroll_id": scroll_id})
200
+ url = f'{self.elasticsearch_host}/_search/scroll'
201
+
202
+ while loop_boolean and total > size:
203
+ next_response = pd.json_normalize(requests.post(url=url, data=body, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout).json()["hits"]["hits"])
204
+ next_response.drop(['_id', '_index', '_score'], axis=1, inplace=True)
205
+ response = pd.concat([response, next_response], ignore_index=True)
206
+ print(f'Received {len(next_response)} documents from index {index}')
207
+ if len(next_response) != size:
208
+ loop_boolean = False
209
+ return response
210
+
211
+ def delete_index(self, index_name) -> str:
212
+ """
213
+ Deletes an existing index if it exists. Documentation: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-delete-index.html
214
+ :param index_name: The index you want to delete
215
+ :return: The response of the request to elasticsearch
216
+ """
217
+ if self.disabled:
218
+ return 'Index deletion disabled'
219
+
220
+ # Check if index exists
221
+ url = f'{self.elasticsearch_host}/{index_name}'
222
+ response = requests.head(url=url, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout)
223
+
224
+ # Delete index if it exists
225
+ if response.status_code == 404:
226
+ return f'Index \'{index_name}\' does not exist'
227
+ else:
228
+ response = requests.delete(url=url, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout)
229
+ if response.status_code == 200:
230
+ return f'Index \'{index_name}\' deleted'
231
+ else:
232
+ raise ConnectionError(f'Could not delete index {index_name} with status code: {response.status_code}. Response: {response.text}')
233
+
234
+ def create_index(self, index_name: str) -> str:
235
+ """
236
+ Creates a new index in the elasticsearch instance. Documentation: https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
237
+ :param index_name: The name of the desired index
238
+ :return: The response of the request to elasticsearch
239
+ """
240
+ if self.disabled:
241
+ return 'Index creation disabled'
242
+
243
+ url = f'{self.elasticsearch_host}/{index_name}-000001'
244
+ response = requests.head(url=url, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout)
245
+ if response.status_code == 200:
246
+ body = {
247
+ "settings": {
248
+ "index.lifecycle.name": "task_execution_log",
249
+ "index.lifecycle.rollover_alias": f"{index_name}_rollover"
250
+ }
251
+ }
252
+ response = requests.put(url=f"{url}/_settings", headers=self.elastic_headers, json=body, verify=self.verify, timeout=self.timeout)
253
+ if response.status_code == 200:
254
+ return f'Index {index_name} settings updated'
255
+ else:
256
+ raise ConnectionError(f'Could not update index {index_name} with status code: {response.status_code}. Response: {response.text}')
257
+ else:
258
+ body = {
259
+ "aliases":
260
+ {
261
+ f"{index_name}_rollover": {
262
+ "is_write_index": True
263
+ }
264
+ },
265
+ "settings": {
266
+ "index.lifecycle.name": "task_execution_log",
267
+ "index.lifecycle.rollover_alias": f"{index_name}_rollover"
268
+ }
269
+ }
270
+ response = requests.put(url=url, headers=self.elastic_headers, json=body, verify=self.verify, timeout=self.timeout)
271
+ if response.status_code == 200:
272
+ return f'Index {index_name} created'
273
+ else:
274
+ raise ConnectionError(f'Could not create index {index_name} with status code: {response.status_code}. Response: {response.text}')
275
+
276
+ def create_or_update_role(self, role_name: str, index: str) -> str:
277
+ """
278
+ Creates or updates a role. All the indexes which start with the same constraint as the role_name, are added to the role
279
+ :param role_name: The name of the desired role. Most often the username which also is used for the mysql database user (sc_customer)
280
+ :param index: one or more index names in a list.
281
+ :return: The response of the request to elasticsearch
282
+ """
283
+ try:
284
+ if self.disabled:
285
+ return 'Role creation disabled'
286
+
287
+ url = f'{self.kibana_host}/api/security/role/{role_name}'
288
+ # Set the body
289
+ body = {
290
+ 'elasticsearch': {
291
+ 'cluster': ['transport_client'],
292
+ 'indices': [
293
+ {
294
+ 'names': [index],
295
+ 'privileges': ['read', 'write', 'read_cross_cluster', 'view_index_metadata', 'index']
296
+ }
297
+ ]
298
+ },
299
+ 'kibana': [{
300
+ 'feature': {
301
+ 'dashboard': ['read'],
302
+ 'discover': ['read']
303
+ },
304
+ 'spaces': [role_name],
305
+ }],
306
+ 'metadata': {
307
+ 'version': 1
308
+ }
309
+ }
310
+ body = json.dumps(body)
311
+
312
+ response = requests.head(url=url, headers=self.kibana_headers, verify=self.verify, timeout=self.timeout)
313
+
314
+ if response.status_code == 200:
315
+ return f'Role \'{role_name}\' already exists'
316
+ else:
317
+ response = requests.put(url=url, data=body, headers=self.kibana_headers, verify=self.verify, timeout=self.timeout)
318
+ if response.status_code == 204:
319
+ return f'Role {role_name} created'
320
+ else:
321
+ raise ConnectionError(f'Could not create role {role_name} with status code: {response.status_code}. Response: {response.text}')
322
+ except:
323
+ message = "Could not create role, since this is not strictly necessary to write logs, continue without it"
324
+ print(message)
325
+ return message
326
+
327
+ def get_indices(self) -> dict:
328
+ """
329
+ Get all the indices in the elasticsearch instance
330
+ :return: A dictionary with all the indices
331
+ """
332
+ if self.disabled:
333
+ return {}
334
+
335
+ indices = requests.get(url=f'{self.elasticsearch_host}/_cat/indices?format=json', headers=self.elastic_headers, verify=self.verify, timeout=self.timeout).json()
336
+ return indices
337
+
338
+ def create_user(self, user_name: str, password: str, user_description: str, roles: list) -> str:
339
+ """
340
+ Creates a user if it doesn't exist.
341
+ :param user_name: The username. Most often the username which also is used for the mysql database user (sc_customer)
342
+ :param password: Choose a safe password. At least 8 characters long
343
+ :param user_description: A readable description. Often the customer name
344
+ :param roles: Give the roles to which the user belongs in a list. Most often the same role_name as the user_name
345
+ :return: The response of the request to elasticsearch
346
+ """
347
+ if self.disabled:
348
+ return 'User creation disabled'
349
+
350
+ url = f'{self.elasticsearch_host}/_security/user/{user_name}'
351
+ body = {
352
+ 'password': f'{password}',
353
+ 'roles': roles,
354
+ 'full_name': f'{user_description}'
355
+ }
356
+ body = json.dumps(body)
357
+
358
+ response = requests.head(url=url, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout)
359
+
360
+ if response.status_code == 200:
361
+ return f'user {user_name} already exists'
362
+ else:
363
+ response = requests.put(url=url, data=body, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout)
364
+ if response.status_code == 200:
365
+ return f'user {user_name}, with password: {password} has been created'
366
+ else:
367
+ raise ConnectionError(f'Could not create user {user_name} with status code: {response.status_code}. Response: {response.text}')
368
+
369
+ def post_document(self, index_name: str, document: dict) -> requests.Response:
370
+ """
371
+ Posts a document to the specified index. Documentation: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html
372
+ :param index_name: The name of the index to which the document should be posted
373
+ :param document: The document to be posted
374
+ :return: The response of the request to elasticsearch
375
+ """
376
+ if self.disabled:
377
+ return None
378
+
379
+ url = f'{self.elasticsearch_host}/{index_name}_rollover/_doc/'
380
+ body = json.dumps(document)
381
+ response = requests.post(url=url, data=body, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout)
382
+ return response
383
+
384
+ def get_document(self, index_name: str, document_id: str) -> requests.Response:
385
+ """
386
+ Gets a document from the specified index. Documentation: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html
387
+ :param index_name: The name of the index from which the document should be retrieved
388
+ :param document_id: The id of the document to be retrieved
389
+ :return: The response of the request to elasticsearch
390
+ """
391
+ if self.disabled:
392
+ return None
393
+
394
+ url = f'{self.elasticsearch_host}/{index_name}/_doc/{document_id}'
395
+ response = requests.get(url=url, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout)
396
+ return response
397
+
398
+ def delete_document(self, index_name: str, document_id: str) -> requests.Response:
399
+ """
400
+ Deletes a document from the specified index. Documentation: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html
401
+ :param index_name: The name of the index from which the document should be deleted
402
+ :param document_id: The id of the document to be deleted
403
+ :return: The response of the request to elasticsearch
404
+ """
405
+ if self.disabled:
406
+ return None
407
+
408
+ url = f'{self.elasticsearch_host}/{index_name}/_doc/{document_id}'
409
+ response = requests.delete(url=url, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout)
410
+ return response
411
+
412
+ def task_execution_log(self, information: dict) -> requests.Response:
413
+ """
414
+ Write a document to the elasticsearch database
415
+ :param information: the information to be inserted into the database.
416
+ :return: the response of the post request
417
+ """
418
+ if self.disabled:
419
+ return None
420
+
421
+ # Add new document
422
+ url = f'{self.elasticsearch_host}/task_execution_log_{self.client_user}_rollover/_doc/'
423
+ body = json.dumps(information)
424
+ response = requests.post(url=url, data=body, headers=self.elastic_headers, verify=self.verify, timeout=self.timeout)
425
+ return response
426
+
427
+ @staticmethod
428
+ def generate_password(length=20):
429
+ characters = string.ascii_letters + string.digits + string.punctuation
430
+ password = ''.join(random.choice(characters) for _ in range(length))
431
+ return password
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 1.0
2
2
  Name: brynq-sdk-elastic
3
- Version: 3.0.2
3
+ Version: 3.0.4
4
4
  Summary: elastic wrapper from BrynQ
5
5
  Home-page: UNKNOWN
6
6
  Author: BrynQ
@@ -1,4 +1,6 @@
1
1
  setup.py
2
+ brynq_sdk_elastic/__init__.py
3
+ brynq_sdk_elastic/elastic.py
2
4
  brynq_sdk_elastic.egg-info/PKG-INFO
3
5
  brynq_sdk_elastic.egg-info/SOURCES.txt
4
6
  brynq_sdk_elastic.egg-info/dependency_links.txt
@@ -0,0 +1 @@
1
+ brynq_sdk_elastic
@@ -2,7 +2,7 @@ from setuptools import setup, find_namespace_packages
2
2
 
3
3
  setup(
4
4
  name='brynq_sdk_elastic',
5
- version='3.0.2',
5
+ version='3.0.4',
6
6
  description='elastic wrapper from BrynQ',
7
7
  long_description='elastic wrapper from BrynQ',
8
8
  author='BrynQ',