python-databases 0.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.
- python_databases/__init__.py +12 -0
- python_databases/elastic_search_infrastructure/__init__.py +12 -0
- python_databases/elastic_search_infrastructure/elastic_search.py +318 -0
- python_databases-0.1.0.dist-info/METADATA +31 -0
- python_databases-0.1.0.dist-info/RECORD +8 -0
- python_databases-0.1.0.dist-info/WHEEL +5 -0
- python_databases-0.1.0.dist-info/licenses/LICENSE +21 -0
- python_databases-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dotenv import load_dotenv
|
|
2
|
+
|
|
3
|
+
from python_databases.elastic_search_infrastructure.elastic_search import (
|
|
4
|
+
UrlProtocol,
|
|
5
|
+
ElasticSearch,
|
|
6
|
+
ElasticSearchOnPrem,
|
|
7
|
+
ElasticSearchCloud
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
__all__ = ['UrlProtocol', 'ElasticSearch', 'ElasticSearchOnPrem', 'ElasticSearchCloud']
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from dotenv import load_dotenv
|
|
2
|
+
|
|
3
|
+
from python_databases.elastic_search_infrastructure.elastic_search import (
|
|
4
|
+
UrlProtocol,
|
|
5
|
+
ElasticSearch,
|
|
6
|
+
ElasticSearchOnPrem,
|
|
7
|
+
ElasticSearchCloud
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
__all__ = ['UrlProtocol', 'ElasticSearch', 'ElasticSearchOnPrem', 'ElasticSearchCloud']
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Optional, Any
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from elasticsearch import Elasticsearch, helpers
|
|
10
|
+
from retrying import retry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UrlProtocol(Enum):
|
|
14
|
+
HTTP = 'http'
|
|
15
|
+
HTTPS = 'https'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ElasticSearch(ABC):
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
elk_hostname: str,
|
|
22
|
+
elasticsearch_port: Optional[int],
|
|
23
|
+
kibana_port: Optional[int],
|
|
24
|
+
protocol: UrlProtocol,
|
|
25
|
+
username: Optional[str],
|
|
26
|
+
password: Optional[str]
|
|
27
|
+
):
|
|
28
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
29
|
+
self._change_elasticsearch_logger()
|
|
30
|
+
|
|
31
|
+
self.protocol = protocol
|
|
32
|
+
self.elk_hostname = elk_hostname
|
|
33
|
+
self.username = username
|
|
34
|
+
self.password = password
|
|
35
|
+
|
|
36
|
+
self.elasticsearch_port = elasticsearch_port
|
|
37
|
+
if not self.elasticsearch_port:
|
|
38
|
+
self.elasticsearch_url = f'{self.protocol.value}://{self.elk_hostname}'
|
|
39
|
+
else:
|
|
40
|
+
self.elasticsearch_url = f'{self.protocol.value}://{self.elk_hostname}:{self.elasticsearch_port}'
|
|
41
|
+
self.kibana_port = kibana_port
|
|
42
|
+
|
|
43
|
+
self.elk_client = None
|
|
44
|
+
|
|
45
|
+
def _change_elasticsearch_logger(self):
|
|
46
|
+
tracer = logging.getLogger('elasticsearch')
|
|
47
|
+
tracer.setLevel(logging.CRITICAL) # or desired level
|
|
48
|
+
# tracer.addHandler(logging.FileHandler('indexer.log'))
|
|
49
|
+
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def connect_to_elasticsearch(self):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
def set_list_of_docs(self, list_of_docs: list[dict], request_timeout: int = 600, quick: bool = True) -> None:
|
|
55
|
+
self.logger.info('Start to report the documents to ELK')
|
|
56
|
+
|
|
57
|
+
if quick:
|
|
58
|
+
if failed_response := helpers.streaming_bulk(
|
|
59
|
+
self.elk_client,
|
|
60
|
+
list_of_docs,
|
|
61
|
+
raise_on_error=False,
|
|
62
|
+
request_timeout=request_timeout,
|
|
63
|
+
chunk_size=1000
|
|
64
|
+
):
|
|
65
|
+
for item in failed_response:
|
|
66
|
+
if item[1]['index']['status'] != 201 or item[1]['index']['_shards']['failed'] > 0:
|
|
67
|
+
self.logger.debug(f'Failed document: \n{item}')
|
|
68
|
+
else:
|
|
69
|
+
success_response, failed_response = helpers.bulk(self.elk_client, list_of_docs)
|
|
70
|
+
if success_response == len(list_of_docs):
|
|
71
|
+
self.logger.info('All documents were reported successfully')
|
|
72
|
+
else:
|
|
73
|
+
self.logger.error(
|
|
74
|
+
f'Not all documents were reported successfully.\n'
|
|
75
|
+
f'The documents that were not reported successfully: {failed_response}'
|
|
76
|
+
)
|
|
77
|
+
self.logger.info('Finish to report the documents to ELK')
|
|
78
|
+
|
|
79
|
+
def fill_list_of_docs( # noqa
|
|
80
|
+
self,
|
|
81
|
+
list_of_docs: list[dict],
|
|
82
|
+
index_name: str,
|
|
83
|
+
doc: dict[str, Any]
|
|
84
|
+
) -> None:
|
|
85
|
+
list_of_docs.append(
|
|
86
|
+
{
|
|
87
|
+
"_index": index_name,
|
|
88
|
+
"_source": deepcopy(doc)
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _add_basic_info(
|
|
93
|
+
self,
|
|
94
|
+
doc: dict,
|
|
95
|
+
doc_id: str,
|
|
96
|
+
timestamp: datetime,
|
|
97
|
+
date_and_time: str,
|
|
98
|
+
username: str = None
|
|
99
|
+
) -> dict:
|
|
100
|
+
doc.update(
|
|
101
|
+
{
|
|
102
|
+
'doc_id': doc_id,
|
|
103
|
+
'timestamp': timestamp,
|
|
104
|
+
'date_str': f'{date_and_time}'.replace('-', '_')
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
if username:
|
|
108
|
+
doc['username'] = username
|
|
109
|
+
return doc
|
|
110
|
+
|
|
111
|
+
def _add_list_values_as_str(self, doc: dict, value: Any, column: str) -> None:
|
|
112
|
+
"""Add list values as both list and string representation to the provided doc"""
|
|
113
|
+
if type(value) is list and all(type(item) is str for item in value):
|
|
114
|
+
filtered_value = list(filter(lambda item: item is not None, value))
|
|
115
|
+
doc[f'{column}'] = filtered_value
|
|
116
|
+
doc[f'{column}_str'] = ', '.join(filtered_value)
|
|
117
|
+
|
|
118
|
+
def _build_document(
|
|
119
|
+
self,
|
|
120
|
+
row: dict,
|
|
121
|
+
doc_id: str,
|
|
122
|
+
timestamp: datetime,
|
|
123
|
+
date_and_time: str,
|
|
124
|
+
username: str = None
|
|
125
|
+
) -> dict:
|
|
126
|
+
doc = self._add_basic_info(
|
|
127
|
+
doc={},
|
|
128
|
+
doc_id=doc_id,
|
|
129
|
+
timestamp=timestamp,
|
|
130
|
+
date_and_time=date_and_time,
|
|
131
|
+
username=username
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Add custom fields
|
|
135
|
+
for custom_field, value in row.items():
|
|
136
|
+
doc[custom_field] = value
|
|
137
|
+
self._add_list_values_as_str(
|
|
138
|
+
doc=doc,
|
|
139
|
+
value=value,
|
|
140
|
+
column=custom_field
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return doc
|
|
144
|
+
|
|
145
|
+
def _prepare_documents_for_bulk(
|
|
146
|
+
self,
|
|
147
|
+
data: list[dict],
|
|
148
|
+
doc_id: str,
|
|
149
|
+
doc_index_name: str,
|
|
150
|
+
timestamp: datetime,
|
|
151
|
+
date_and_time: str,
|
|
152
|
+
username: str = None
|
|
153
|
+
) -> list[dict]:
|
|
154
|
+
list_of_docs = []
|
|
155
|
+
for index, row in enumerate(data):
|
|
156
|
+
self.logger.debug(f'index: {index + 1}/{len(data)}')
|
|
157
|
+
|
|
158
|
+
doc = self._build_document(
|
|
159
|
+
row=row,
|
|
160
|
+
doc_id=doc_id,
|
|
161
|
+
timestamp=timestamp,
|
|
162
|
+
date_and_time=date_and_time,
|
|
163
|
+
username=username
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self.fill_list_of_docs(
|
|
167
|
+
list_of_docs=list_of_docs,
|
|
168
|
+
index_name=doc_index_name,
|
|
169
|
+
doc=doc
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
return list_of_docs
|
|
173
|
+
|
|
174
|
+
def fill_elk_index_as_bulk_chunk(self, list_of_docs: list[dict], chunk_size=1000, time_sleep: int = 60) -> None:
|
|
175
|
+
for i in range(0, len(list_of_docs), chunk_size):
|
|
176
|
+
self.logger.info(f'index: {i} - {i + chunk_size} / {len(list_of_docs)}')
|
|
177
|
+
chunk = list_of_docs[i:i + chunk_size]
|
|
178
|
+
self.set_list_of_docs(list_of_docs=chunk)
|
|
179
|
+
time.sleep(time_sleep)
|
|
180
|
+
|
|
181
|
+
def fill_elk_index_as_bulk(
|
|
182
|
+
self,
|
|
183
|
+
data: list[dict],
|
|
184
|
+
doc_id: str,
|
|
185
|
+
doc_index_name: str,
|
|
186
|
+
timestamp: datetime,
|
|
187
|
+
date_and_time: str,
|
|
188
|
+
username: str = None,
|
|
189
|
+
chunk_size=1000,
|
|
190
|
+
time_sleep: int = 1
|
|
191
|
+
) -> None: # use as the main function to send data to the elastic search
|
|
192
|
+
list_of_docs = self._prepare_documents_for_bulk(
|
|
193
|
+
data=data,
|
|
194
|
+
doc_id=doc_id,
|
|
195
|
+
doc_index_name=doc_index_name,
|
|
196
|
+
timestamp=timestamp,
|
|
197
|
+
date_and_time=date_and_time,
|
|
198
|
+
username=username
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
self.fill_elk_index_as_bulk_chunk(
|
|
202
|
+
list_of_docs=list_of_docs,
|
|
203
|
+
chunk_size=chunk_size,
|
|
204
|
+
time_sleep=time_sleep
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def convert_dataframes_to_list_of_docs(self, dataframe: pd.DataFrame) -> list:
|
|
208
|
+
data = []
|
|
209
|
+
dataframe_values = dataframe.values.tolist()
|
|
210
|
+
for index, value in enumerate(dataframe_values):
|
|
211
|
+
self.logger.debug(f'index: {index}/{len(dataframe_values)}')
|
|
212
|
+
data.append(value)
|
|
213
|
+
return data
|
|
214
|
+
|
|
215
|
+
def check_if_index_exists(self, index: str) -> bool:
|
|
216
|
+
if self.elk_client.indices.exists(index=index):
|
|
217
|
+
self.logger.info(f"The index '{index}' exists.")
|
|
218
|
+
return True
|
|
219
|
+
else:
|
|
220
|
+
self.logger.info(f"The index '{index}' does not exist.")
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def delete_index(self, index: str) -> None:
|
|
224
|
+
self.elk_client.indices.delete(index=index)
|
|
225
|
+
self.logger.info(f"The index '{index}' was deleted.")
|
|
226
|
+
|
|
227
|
+
def check_if_index_exists_and_delete_if_exists(self, index: str) -> bool:
|
|
228
|
+
if self.check_if_index_exists(index=index):
|
|
229
|
+
self.delete_index(index=index)
|
|
230
|
+
return True
|
|
231
|
+
else:
|
|
232
|
+
self.logger.info(f"The index '{index}' does not exist.")
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
def get_documents(self, index: str, query: dict) -> list[dict]:
|
|
236
|
+
try:
|
|
237
|
+
res = self.elk_client.search(index=index, body=query)["hits"]["hits"]
|
|
238
|
+
return [doc["_source"] for doc in res]
|
|
239
|
+
except Exception as e:
|
|
240
|
+
raise Exception(f"Failed to get documents from index '{index}': {str(e)}")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class ElasticSearchOnPrem(ElasticSearch):
|
|
244
|
+
def __init__(
|
|
245
|
+
self,
|
|
246
|
+
elk_hostname: str,
|
|
247
|
+
elasticsearch_port: Optional[int] = 9200,
|
|
248
|
+
kibana_port: Optional[int] = 5602,
|
|
249
|
+
protocol: UrlProtocol = UrlProtocol.HTTPS,
|
|
250
|
+
username: Optional[str] = None,
|
|
251
|
+
password: Optional[str] = None
|
|
252
|
+
):
|
|
253
|
+
super().__init__(
|
|
254
|
+
elk_hostname=elk_hostname,
|
|
255
|
+
elasticsearch_port=elasticsearch_port,
|
|
256
|
+
kibana_port=kibana_port,
|
|
257
|
+
protocol=protocol,
|
|
258
|
+
username=username,
|
|
259
|
+
password=password
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
263
|
+
|
|
264
|
+
self.connect_to_elasticsearch()
|
|
265
|
+
|
|
266
|
+
@retry(stop_max_attempt_number=3, wait_fixed=180000)
|
|
267
|
+
def connect_to_elasticsearch(self):
|
|
268
|
+
if self.username and self.password:
|
|
269
|
+
self.elk_client = Elasticsearch(
|
|
270
|
+
hosts=[self.elasticsearch_url],
|
|
271
|
+
http_auth=(self.username, self.password),
|
|
272
|
+
# ca_certs="/etc/elasticsearch/certs/http_ca.crt",
|
|
273
|
+
verify_certs=False, timeout=60
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
self.elk_client = Elasticsearch(hosts=[f'{self.elasticsearch_url}'], timeout=20)
|
|
277
|
+
|
|
278
|
+
if self.elk_client.ping():
|
|
279
|
+
self.logger.info("Elasticsearch on-prem Connection Successful")
|
|
280
|
+
else:
|
|
281
|
+
self.logger.error("Elasticsearch on-prem Connection Failed")
|
|
282
|
+
raise Exception(
|
|
283
|
+
f'Failed to connect to Elasticsearch on-prem on {self.elasticsearch_url}')
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class ElasticSearchCloud(ElasticSearch):
|
|
287
|
+
def __init__(
|
|
288
|
+
self,
|
|
289
|
+
elk_hostname: str,
|
|
290
|
+
elasticsearch_port: Optional[int] = 9200,
|
|
291
|
+
kibana_port: Optional[int] = 5602,
|
|
292
|
+
protocol: UrlProtocol = UrlProtocol.HTTPS,
|
|
293
|
+
username: Optional[str] = None,
|
|
294
|
+
password: Optional[str] = None
|
|
295
|
+
):
|
|
296
|
+
super().__init__(
|
|
297
|
+
elk_hostname=elk_hostname,
|
|
298
|
+
elasticsearch_port=elasticsearch_port,
|
|
299
|
+
kibana_port=kibana_port,
|
|
300
|
+
protocol=protocol,
|
|
301
|
+
username=username,
|
|
302
|
+
password=password
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
self.logger = logging.getLogger(self.__class__.__name__)
|
|
306
|
+
|
|
307
|
+
self.connect_to_elasticsearch()
|
|
308
|
+
|
|
309
|
+
@retry(stop_max_attempt_number=3, wait_fixed=180000)
|
|
310
|
+
def connect_to_elasticsearch(self):
|
|
311
|
+
self.elk_client = Elasticsearch(cloud_id=self.elk_hostname, http_auth=(self.username, self.password))
|
|
312
|
+
|
|
313
|
+
if self.elk_client.ping():
|
|
314
|
+
self.logger.info("Elasticsearch cloud Connection Successful")
|
|
315
|
+
else:
|
|
316
|
+
self.logger.error("Elasticsearch cloud Connection Failed")
|
|
317
|
+
raise Exception(
|
|
318
|
+
f'Failed to connect to Elasticsearch cloud on {self.elk_hostname}')
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-databases
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library for managing and interacting with multiple databases seamlessly.
|
|
5
|
+
Home-page: https://github.com/aviz92/python-databases
|
|
6
|
+
Author: Avi Zaguri
|
|
7
|
+
Author-email:
|
|
8
|
+
Project-URL: Repository, https://github.com/aviz92/python-databases
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: setuptools
|
|
13
|
+
Requires-Dist: wheel
|
|
14
|
+
Requires-Dist: dotenv
|
|
15
|
+
Requires-Dist: custom-python-logger
|
|
16
|
+
Requires-Dist: retrying
|
|
17
|
+
Requires-Dist: elasticsearch<9
|
|
18
|
+
Requires-Dist: pandas
|
|
19
|
+
Requires-Dist: psycopg2-binary
|
|
20
|
+
Requires-Dist: pytest
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: description
|
|
23
|
+
Dynamic: description-content-type
|
|
24
|
+
Dynamic: home-page
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: project-url
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# Python Databases
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
python_databases/__init__.py,sha256=NjWdKySjMPaJk8eCFiw54p1ddv42zGj3aVFTVvfkT3E,298
|
|
2
|
+
python_databases/elastic_search_infrastructure/__init__.py,sha256=NjWdKySjMPaJk8eCFiw54p1ddv42zGj3aVFTVvfkT3E,298
|
|
3
|
+
python_databases/elastic_search_infrastructure/elastic_search.py,sha256=1W1jRByLljUDHLE8MqdIOHI_ak6CRKq7YLf09wKwTqQ,10951
|
|
4
|
+
python_databases-0.1.0.dist-info/licenses/LICENSE,sha256=cSikHY6SZFsPZSBizCDAJ0-Bjjzxt-JtX6TVbKxwimo,1067
|
|
5
|
+
python_databases-0.1.0.dist-info/METADATA,sha256=eiuP5xi7JuN_9qYiTWU_0f0huTQ9gfTXj9bX-ZqKwHU,846
|
|
6
|
+
python_databases-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
python_databases-0.1.0.dist-info/top_level.txt,sha256=fXcMUntaYJfKeFS7FjQ6DY4WVXYBsgdrDmhOudYs-vg,17
|
|
8
|
+
python_databases-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Avi Zaguri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
python_databases
|