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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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