atomicshop 2.16.16__py3-none-any.whl → 2.16.18__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.

Potentially problematic release.


This version of atomicshop might be problematic. Click here for more details.

atomicshop/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  """Atomic Basic functions and classes to make developer life easier"""
2
2
 
3
3
  __author__ = "Den Kras"
4
- __version__ = '2.16.16'
4
+ __version__ = '2.16.18'
@@ -5,7 +5,7 @@ from typing import Literal
5
5
  from ...print_api import print_api
6
6
  from ...wrappers.loggingw import reading, consts
7
7
  from ...file_io import csvs
8
- from ... import urls
8
+ from ... import urls, filesystem
9
9
 
10
10
 
11
11
  def calculate_moving_average(
@@ -43,7 +43,7 @@ def calculate_moving_average(
43
43
  date_format: str = consts.DEFAULT_ROTATING_SUFFIXES_FROM_WHEN['midnight']
44
44
 
45
45
  # Get all the file paths and their midnight rotations.
46
- logs_paths: list = reading.get_logs_paths(
46
+ logs_paths: list[filesystem.AtomicPath] = reading.get_logs_paths(
47
47
  log_file_path=file_path,
48
48
  date_format=date_format
49
49
  )
@@ -54,14 +54,14 @@ def calculate_moving_average(
54
54
 
55
55
  statistics_content: dict = {}
56
56
  # Read each file to its day.
57
- for log_path_dict in logs_paths:
58
- date_string = log_path_dict['date_string']
57
+ for log_atomic_path in logs_paths:
58
+ date_string = log_atomic_path.datetime_string
59
59
  statistics_content[date_string] = {}
60
60
 
61
- statistics_content[date_string]['file'] = log_path_dict
61
+ statistics_content[date_string]['file'] = log_atomic_path
62
62
 
63
63
  log_file_content, log_file_header = (
64
- csvs.read_csv_to_list_of_dicts_by_header(log_path_dict['file_path'], **(print_kwargs or {})))
64
+ csvs.read_csv_to_list_of_dicts_by_header(log_atomic_path.path, **(print_kwargs or {})))
65
65
  statistics_content[date_string]['content'] = log_file_content
66
66
  statistics_content[date_string]['header'] = log_file_header
67
67
 
@@ -14,6 +14,17 @@ import logging
14
14
  """
15
15
 
16
16
 
17
+ def is_logger_exists(
18
+ logger_name: str
19
+ ) -> bool:
20
+ """
21
+ Function to check if the logger exists.
22
+ :param logger_name: str, Name of the logger.
23
+ :return: bool, True if the logger exists, False otherwise.
24
+ """
25
+ return logger_name in logging.Logger.manager.loggerDict
26
+
27
+
17
28
  def get_logger(logger_name: str) -> logging.Logger:
18
29
  """
19
30
  Function to get a logger.
@@ -8,6 +8,7 @@ from . import loggers, handlers
8
8
  # noinspection PyPep8Naming
9
9
  def create_logger(
10
10
  logger_name: str,
11
+ get_existing_if_exists: bool = True,
11
12
  file_path: str = None,
12
13
  directory_path: str = None,
13
14
  add_stream: bool = False,
@@ -42,6 +43,8 @@ def create_logger(
42
43
  Function to get a logger and add StreamHandler and TimedRotatingFileHandler to it.
43
44
 
44
45
  :param logger_name: Name of the logger.
46
+ :param get_existing_if_exists: bool, If set to True, the logger will be returned if it already exists.
47
+ If set to False, the new stream/file handler will be added to existing logger again.
45
48
  :param file_path: full path to the log file. If you don't want to use the file, set it to None.
46
49
  You can set the directory_path only and then the 'logger_name' will be used as the file name with the
47
50
  'file_type' as the file extension.
@@ -173,8 +176,15 @@ def create_logger(
173
176
 
174
177
  file_path = f"{directory_path}{os.sep}{logger_name}.{file_type}"
175
178
 
179
+ # Check if the logger exists before creating it/getting the existing.
180
+ is_logger_exists = loggers.is_logger_exists(logger_name)
181
+
176
182
  logger = get_logger_with_level(logger_name, logging_level)
177
183
 
184
+ # If the logger already exists, and we don't want to add the handlers again, return the logger.
185
+ if get_existing_if_exists and is_logger_exists:
186
+ return logger
187
+
178
188
  if add_stream:
179
189
  handlers.add_stream_handler(
180
190
  logger=logger, logging_level=logging_level, formatter=formatter_streamhandler,
@@ -14,7 +14,7 @@ def get_logs_paths(
14
14
  latest_only: bool = False,
15
15
  previous_day_only: bool = False,
16
16
  specific_date: str = None
17
- ):
17
+ ) -> list[filesystem.AtomicPath]:
18
18
  """
19
19
  This function gets the logs file paths from the directory. Supports rotating files to get the logs by time.
20
20
 
@@ -61,7 +61,7 @@ def get_logs_paths(
61
61
  log_files_directory_path: str = str(Path(log_file_path).parent)
62
62
 
63
63
  # Get all the log file paths by the file_name_pattern and the date_format string.
64
- logs_files: list = filesystem.get_paths_from_directory(
64
+ logs_files: list[filesystem.AtomicPath] = filesystem.get_paths_from_directory(
65
65
  log_files_directory_path,
66
66
  get_file=True,
67
67
  file_name_check_pattern=file_name_pattern,
@@ -23,6 +23,14 @@ def test_connection(
23
23
  uri: str = MONGODB_DEFAULT_URI,
24
24
  raise_exception: bool = False
25
25
  ) -> bool:
26
+ """
27
+ Test the connection to the MongoDB server.
28
+
29
+ :param uri: str, URI to the MongoDB server.
30
+ :param raise_exception: bool, True to raise an exception if the connection fails, False otherwise (just return
31
+ False).
32
+ :return: bool, True if the connection is successful, False otherwise.
33
+ """
26
34
  try:
27
35
  client = MongoClient(uri)
28
36
  client.admin.command('ping')
@@ -1,7 +1,11 @@
1
- from typing import Union
1
+ from typing import Union, Literal
2
2
  import datetime
3
+ import json
3
4
 
5
+ # noinspection PyPackageRequirements
4
6
  import pymongo
7
+ # noinspection PyPackageRequirements
8
+ import pymongo.database
5
9
 
6
10
  from ...basics import dicts
7
11
 
@@ -14,6 +18,253 @@ These are good examples to get you started, but can't really cover all the use c
14
18
  """
15
19
 
16
20
 
21
+ class MongoDBWrapper:
22
+ def __init__(
23
+ self,
24
+ db_name: str,
25
+ uri: str = mongo_infra.MONGODB_DEFAULT_URI
26
+ ):
27
+ self.db_name: str = db_name
28
+ self.uri: str = uri
29
+
30
+ # noinspection PyTypeChecker
31
+ self.client: pymongo.MongoClient = None
32
+ # noinspection PyTypeChecker
33
+ self.db: pymongo.database.Database = None
34
+
35
+ def connect(self):
36
+ """
37
+ Connect to a MongoDB database.
38
+ :return: pymongo.MongoClient, the client object.
39
+ """
40
+
41
+ if not self.client:
42
+ self.client = connect(uri=self.uri)
43
+ self.db = get_db(database=self.db_name, mongo_client=self.client)
44
+
45
+ def disconnect(self):
46
+ """
47
+ Disconnect from a MongoDB database.
48
+ :return: None
49
+ """
50
+
51
+ if self.client:
52
+ self.client.close()
53
+ self.client = None
54
+
55
+ def index(
56
+ self,
57
+ object_instance: Union[list[dict], dict],
58
+ collection_name: str,
59
+ add_timestamp: bool = False,
60
+ convert_mixed_lists_to_strings: bool = False
61
+ ):
62
+ """
63
+ Add a dictionary or list dictionaries to a MongoDB collection.
64
+ :param object_instance: list of dictionaries or dictionary to add to the collection.
65
+ :param collection_name: str, the name of the collection.
66
+ :param add_timestamp: bool, if True, a current time timestamp will be added to the object.
67
+ :param convert_mixed_lists_to_strings: bool, if True, mixed lists or tuples when entries are
68
+ strings and integers, the integers will be converted to strings.
69
+
70
+ :return: None
71
+ """
72
+
73
+ self.connect()
74
+
75
+ index(
76
+ object_instance=object_instance,
77
+ database=self.db, collection_name=collection_name,
78
+ add_timestamp=add_timestamp, convert_mixed_lists_to_strings=convert_mixed_lists_to_strings,
79
+ mongo_client=self.client, close_client=False)
80
+
81
+ def delete(
82
+ self,
83
+ object_instance: Union[list[dict], dict],
84
+ collection_name: str
85
+ ):
86
+ """
87
+ Remove a list of dictionaries or a dictionary from a MongoDB collection.
88
+
89
+ :param object_instance: list of dictionaries, the list of dictionaries to remove from the collection.
90
+ :param collection_name: str, the name of the collection.
91
+
92
+ :return: None
93
+ """
94
+
95
+ self.connect()
96
+
97
+ delete(
98
+ object_instance=object_instance,
99
+ database=self.db, collection_name=collection_name,
100
+ mongo_client=self.client, close_client=False)
101
+
102
+ def find(
103
+ self,
104
+ collection_name: str,
105
+ query: dict = None,
106
+ page: int = None,
107
+ items: int = None,
108
+ sorting: dict[str, Literal[
109
+ 'asc', 'desc',
110
+ 1, -1]] = None,
111
+ convert_object_id_to_str: bool = False,
112
+ keys_convert_to_dict: list[str] = None
113
+ ) -> list[dict]:
114
+ """
115
+ Find entries in a MongoDB collection by query.
116
+ :param collection_name: str, the name of the collection.
117
+ :param query: dict, the query to search for.
118
+ Example, search for all entries with column name 'name' equal to 'John':
119
+ query = {'name': 'John'}
120
+ Example, return all entries from collection:
121
+ query = None
122
+
123
+ CHECK MORE EXAMPLES IN THE DOCSTRING OF THE FUNCTION 'find' BELOW which is not in this class.
124
+ :param page: int, the page number (Optional).
125
+ The results are filtered after results are fetched from db.
126
+ :param items: int, the number of results per page (Optional).
127
+ The results are filtered after results are fetched from db.
128
+ :param sorting: dict, the name of the field and the order to sort the containers by.
129
+ You can use several fields to sort the containers by several fields.
130
+ In this case the containers will be sorted by the first field, then by the second field, etc.
131
+ You can also use only singular field to sort the containers by only one field.
132
+ Usage:
133
+ {
134
+ field_name: order
135
+ }
136
+ Example:
137
+ {
138
+ 'vendor': 'asc',
139
+ 'model': 'desc'
140
+ }
141
+
142
+ Or example using integers:
143
+ {
144
+ 'vendor': 1,
145
+ 'model': -1
146
+ }
147
+
148
+ :param convert_object_id_to_str: bool, if True, the '_id' field will be converted to a string.
149
+ The '_id' field is an ObjectId type, which is a complex object, it can be converted to a string for simpler
150
+ processing.
151
+ :param keys_convert_to_dict: list, the keys of the documents that should be converted from string to dict.
152
+ Recursively searches for keys in specified list in a nested dictionary in result entries list,
153
+ and converts their values using 'json.loads' if found.
154
+ :return: list of dictionaries, the list of entries that match the query.
155
+ """
156
+
157
+ self.connect()
158
+
159
+ entries: list[dict] = find(
160
+ database=self.db, collection_name=collection_name,
161
+ query=query, page=page, items=items, sorting=sorting,
162
+ convert_object_id_to_str=convert_object_id_to_str, key_convert_to_dict=keys_convert_to_dict,
163
+ mongo_client=self.client, close_client=False)
164
+
165
+ return entries
166
+
167
+ def distinct(
168
+ self,
169
+ collection_name: str,
170
+ field_name: str,
171
+ query: dict = None
172
+ ) -> list:
173
+ """
174
+ Get distinct values of a field from a MongoDB collection.
175
+ Example:
176
+ Example database:
177
+ {
178
+ 'users': [
179
+ {'name': 'John', 'age': 25},
180
+ {'name': 'John', 'age': 30},
181
+ {'name': 'Alice', 'age': 25}
182
+ ]
183
+ }
184
+
185
+ Get distinct values of the field 'name' from the collection 'users':
186
+ distinct('users', 'name')
187
+
188
+ Output:
189
+ ['John', 'Alice']
190
+
191
+ :param collection_name: str, the name of the collection.
192
+ :param field_name: str, the name of the field.
193
+ :param query: dict, the query to search for. If None, the query will not be executed.
194
+
195
+ :return: list, the list of distinct values.
196
+ """
197
+
198
+ self.connect()
199
+
200
+ distinct_values = distinct(
201
+ database=self.db, collection_name=collection_name,
202
+ field_name=field_name, query=query, mongo_client=self.client, close_client=False)
203
+
204
+ return distinct_values
205
+
206
+ def count_entries_in_collection(
207
+ self,
208
+ collection_name: str,
209
+ query: dict = None
210
+ ) -> int:
211
+ """
212
+ Count entries in a MongoDB collection by query.
213
+
214
+ :param collection_name: str, the name of the collection.
215
+ :param query: dict, the query to search for.
216
+ Example, search for all entries with column name 'name' equal to 'John':
217
+ query = {'name': 'John'}
218
+ Example, return all entries from collection:
219
+ query = None
220
+
221
+ :return: int, the number of entries that match the query.
222
+ """
223
+
224
+ self.connect()
225
+
226
+ count = count_entries_in_collection(
227
+ database=self.db, collection_name=collection_name,
228
+ query=query, mongo_client=self.client, close_client=False)
229
+
230
+ return count
231
+
232
+ def get_client(self):
233
+ return self.client
234
+
235
+ def get_stats_db(
236
+ self
237
+ ):
238
+ """
239
+ Get the stats of a MongoDB database.
240
+
241
+ :return: dict, the stats of the collection.
242
+ """
243
+
244
+ self.connect()
245
+
246
+ stats = get_stats_db(
247
+ database=self.db, mongo_client=self.client, close_client=False)
248
+
249
+ return stats
250
+
251
+ def get_stats_db_size(
252
+ self
253
+ ):
254
+ """
255
+ Get the size of a MongoDB database in bytes.
256
+
257
+ :return: int, the size of the database in bytes.
258
+ """
259
+
260
+ self.connect()
261
+
262
+ size = get_stats_db_size(
263
+ database=self.db, mongo_client=self.client, close_client=False)
264
+
265
+ return size
266
+
267
+
17
268
  def connect(uri: str = mongo_infra.MONGODB_DEFAULT_URI):
18
269
  """
19
270
  Connect to a MongoDB database.
@@ -23,9 +274,27 @@ def connect(uri: str = mongo_infra.MONGODB_DEFAULT_URI):
23
274
  return pymongo.MongoClient(uri)
24
275
 
25
276
 
277
+ def get_db(
278
+ database: str,
279
+ mongo_client: pymongo.MongoClient = None
280
+ ) -> pymongo.database.Database:
281
+ """
282
+ Get a MongoDB database object.
283
+ :param database: String, the name of the database.
284
+ :param mongo_client: pymongo.MongoClient, the connection object.
285
+ If None, a new connection will be created to default URI.
286
+ :return: pymongo.database.Database, the database object.
287
+ """
288
+
289
+ if not mongo_client:
290
+ mongo_client = connect()
291
+
292
+ return mongo_client[database]
293
+
294
+
26
295
  def index(
27
296
  object_instance: Union[list[dict], dict],
28
- database_name: str,
297
+ database: Union[str, pymongo.database.Database],
29
298
  collection_name: str,
30
299
  add_timestamp: bool = False,
31
300
  convert_mixed_lists_to_strings: bool = False,
@@ -35,7 +304,9 @@ def index(
35
304
  """
36
305
  Add a dictionary or list dictionaries to a MongoDB collection.
37
306
  :param object_instance: list of dictionaries or dictionary to add to the collection.
38
- :param database_name: str, the name of the database.
307
+ :param database: String or the database object.
308
+ str - the name of the database. In this case the database object will be created.
309
+ pymongo.database.Database - the database object that will be used instead of creating a new one.
39
310
  :param collection_name: str, the name of the collection.
40
311
  :param add_timestamp: bool, if True, a current time timestamp will be added to the object.
41
312
  :param convert_mixed_lists_to_strings: bool, if True, mixed lists or tuples when entries are strings and integers,
@@ -53,7 +324,7 @@ def index(
53
324
  mongo_client = connect()
54
325
  close_client = True
55
326
 
56
- db = mongo_client[database_name]
327
+ db = _get_pymongo_db_from_string_or_pymongo_db(database, mongo_client)
57
328
  collection = db[collection_name]
58
329
 
59
330
  if convert_mixed_lists_to_strings:
@@ -78,7 +349,7 @@ def index(
78
349
 
79
350
  def delete(
80
351
  object_instance: Union[list[dict], dict],
81
- database_name: str,
352
+ database: Union[str, pymongo.database.Database],
82
353
  collection_name: str,
83
354
  mongo_client: pymongo.MongoClient = None,
84
355
  close_client: bool = False
@@ -87,7 +358,9 @@ def delete(
87
358
  Remove a list of dictionaries or a dictionary from a MongoDB collection.
88
359
 
89
360
  :param object_instance: list of dictionaries, the list of dictionaries to remove from the collection.
90
- :param database_name: str, the name of the database.
361
+ :param database: String or the database object.
362
+ str - the name of the database. In this case the database object will be created.
363
+ pymongo.database.Database - the database object that will be used instead of creating a new one.
91
364
  :param collection_name: str, the name of the collection.
92
365
  :param mongo_client: pymongo.MongoClient, the connection object.
93
366
  If None, a new connection will be created to default URI.
@@ -102,7 +375,7 @@ def delete(
102
375
  mongo_client = connect()
103
376
  close_client = True
104
377
 
105
- db = mongo_client[database_name]
378
+ db = _get_pymongo_db_from_string_or_pymongo_db(database, mongo_client)
106
379
  collection = db[collection_name]
107
380
 
108
381
  if isinstance(object_instance, dict):
@@ -116,21 +389,91 @@ def delete(
116
389
 
117
390
 
118
391
  def find(
119
- database_name: str,
392
+ database: Union[str, pymongo.database.Database],
120
393
  collection_name: str,
121
394
  query: dict = None,
395
+ page: int = None,
396
+ items: int = None,
397
+ sorting: dict[str, Literal[
398
+ 'asc', 'desc',
399
+ 1, -1]] = None,
400
+ convert_object_id_to_str: bool = False,
401
+ key_convert_to_dict: list[str] = None,
122
402
  mongo_client: pymongo.MongoClient = None,
123
403
  close_client: bool = False
124
- ):
404
+ ) -> list[dict]:
125
405
  """
126
406
  Find entries in a MongoDB collection by query.
127
- :param database_name: str, the name of the database.
407
+ :param database: String or the database object.
408
+ str - the name of the database. In this case the database object will be created.
409
+ pymongo.database.Database - the database object that will be used instead of creating a new one.
128
410
  :param collection_name: str, the name of the collection.
129
411
  :param query: dict, the query to search for.
130
- Example, search for all entries with column name 'name' equal to 'John':
131
- query = {'name': 'John'}
132
412
  Example, return all entries from collection:
133
413
  query = None
414
+ Example, search for all entries with column name 'name' equal to 'John':
415
+ query = {'name': 'John'}
416
+
417
+ Additional parameters to use in the value of the query:
418
+ $regex: Will search for a regex pattern in the field.
419
+ Example for searching for a value that contains 'test':
420
+ query = {'field_name': {'$regex': 'test'}}
421
+ This will return all entries where the field 'field_name' contains the word 'test':
422
+ 'test', 'test1', '2test', etc.
423
+
424
+ Example for searching for a value that starts with 'test':
425
+ query = {'field_name': {'$regex': '^test'}}
426
+ $options: The options for the regex search.
427
+ 'i': case-insensitive search.
428
+ Example for case-insensitive search:
429
+ query = {'field_name': {'$regex': 'test', '$options': 'i'}}
430
+ $and: Will search for entries that match all the conditions.
431
+ Example for searching for entries that match all the conditions:
432
+ query = {'$and': [
433
+ {'field_name1': 'value1'},
434
+ {'field_name2': 'value2'}
435
+ ]}
436
+ $or: Will search for entries that match at least one of the conditions.
437
+ Example for searching for entries that match at least one of the conditions:
438
+ query = {'$or': [
439
+ {'field_name1': 'value1'},
440
+ {'field_name2': 'value2'}
441
+ ]}
442
+ $in: Will search for a value in a list of values.
443
+ Example for searching for a value that is in a list of values:
444
+ query = {'field_name': {'$in': ['value1', 'value2', 'value3']}}
445
+ $nin: Will search for a value not in a list of values.
446
+ Example for searching for a value that is not in a list of values:
447
+ query = {'field_name': {'$nin': ['value1', 'value2', 'value3']}}
448
+
449
+
450
+ :param page: int, the page number (Optional).
451
+ :param items: int, the number of results per page (Optional).
452
+ :param sorting: dict, the name of the field and the order to sort the containers by.
453
+ You can use several fields to sort the containers by several fields.
454
+ In this case the containers will be sorted by the first field, then by the second field, etc.
455
+ You can also use only singular field to sort the containers by only one field.
456
+ Usage:
457
+ {
458
+ field_name: order
459
+ }
460
+ Example:
461
+ {
462
+ 'vendor': 'asc',
463
+ 'model': 'desc'
464
+ }
465
+
466
+ Or example using integers:
467
+ {
468
+ 'vendor': 1,
469
+ 'model': -1
470
+ }
471
+ :param convert_object_id_to_str: bool, if True, the '_id' field will be converted to a string.
472
+ The '_id' field is an ObjectId type, which is a complex object, it can be converted to a string for simpler
473
+ processing.
474
+ :param key_convert_to_dict: list, the keys of the documents that should be converted from string to dict.
475
+ Recursively searches for keys in specified list in a nested dictionary in result entries list,
476
+ and converts their values using 'json.loads' if found.
134
477
  :param mongo_client: pymongo.MongoClient, the connection object.
135
478
  If None, a new connection will be created to default URI.
136
479
  :param close_client: bool, if True, the connection will be closed after the operation.
@@ -138,17 +481,58 @@ def find(
138
481
  :return: list of dictionaries, the list of entries that match the query.
139
482
  """
140
483
 
484
+ if page and not items:
485
+ raise ValueError("If 'page' is provided, 'items' must be provided as well.")
486
+ elif items and not page:
487
+ page = 1
488
+
489
+ if sorting:
490
+ for key_to_sort_by, order in sorting.items():
491
+ if order not in ['asc', 'desc', 1, -1]:
492
+ raise ValueError("The order must be 'asc', 'desc', 1 or -1.")
493
+
141
494
  if not mongo_client:
142
495
  mongo_client = connect()
143
496
  close_client = True
144
497
 
145
- db = mongo_client[database_name]
498
+ db = _get_pymongo_db_from_string_or_pymongo_db(database, mongo_client)
146
499
  collection = db[collection_name]
147
500
 
148
501
  if query is None:
149
- entries: list = list(collection.find())
150
- else:
151
- entries: list = list(collection.find(query))
502
+ query = {}
503
+
504
+ # Calculate the number of documents to skip
505
+ skip_items = 0
506
+ if page and items:
507
+ skip_items = (page - 1) * items
508
+
509
+ collection_items = collection.find(query)
510
+
511
+ if sorting:
512
+ sorting_list_of_tuples: list[tuple[str, int]] = []
513
+ for key_to_sort_by, order in sorting.items():
514
+ if order == 'asc':
515
+ order = pymongo.ASCENDING
516
+ elif order == 'desc':
517
+ order = pymongo.DESCENDING
518
+
519
+ sorting_list_of_tuples.append((key_to_sort_by, order))
520
+
521
+ collection_items = collection_items.sort(sorting_list_of_tuples)
522
+
523
+ # 'skip_items' can be 0, if we ask for the first page, so we still need to cut the number of items.
524
+ # In this case checking if 'items' is not None is enough.
525
+ if items:
526
+ collection_items = collection_items.skip(skip_items).limit(items)
527
+
528
+ entries: list[dict] = list(collection_items)
529
+
530
+ if convert_object_id_to_str:
531
+ for entry_index, entry in enumerate(entries):
532
+ entries[entry_index]['_id'] = str(entry['_id'])
533
+
534
+ if key_convert_to_dict and entries:
535
+ entries = _convert_key_values_to_objects(keys_convert_to_dict=key_convert_to_dict, returned_data=entries)
152
536
 
153
537
  if close_client:
154
538
  mongo_client.close()
@@ -156,15 +540,116 @@ def find(
156
540
  return entries
157
541
 
158
542
 
543
+ def distinct(
544
+ database: Union[str, pymongo.database.Database],
545
+ collection_name: str,
546
+ field_name: str,
547
+ query: dict = None,
548
+ mongo_client: pymongo.MongoClient = None,
549
+ close_client: bool = False
550
+ ) -> list:
551
+ """
552
+ Get distinct values of a field from a MongoDB collection.
553
+ Example:
554
+ Example database:
555
+ {
556
+ 'users': [
557
+ {'name': 'John', 'age': 25},
558
+ {'name': 'John', 'age': 30},
559
+ {'name': 'Alice', 'age': 25}
560
+ ]
561
+ }
562
+
563
+ Get distinct values of the field 'name' from the collection 'users':
564
+ distinct('my_db', 'users', 'name')
565
+
566
+ Output:
567
+ ['John', 'Alice']
568
+
569
+ :param database: String or the database object.
570
+ str - the name of the database. In this case the database object will be created.
571
+ pymongo.database.Database - the database object that will be used instead of creating a new one.
572
+ :param collection_name: str, the name of the collection.
573
+ :param field_name: str, the name of the field.
574
+ :param query: dict, the query to search for.
575
+ If None, the query will not be executed.
576
+ :param mongo_client: pymongo.MongoClient, the connection object.
577
+ If None, a new connection will be created to default URI.
578
+ :param close_client: bool, if True, the connection will be closed after the operation.
579
+
580
+ :return: list, the list of distinct values.
581
+ """
582
+
583
+ if not mongo_client:
584
+ mongo_client = connect()
585
+ close_client = True
586
+
587
+ db = _get_pymongo_db_from_string_or_pymongo_db(database, mongo_client)
588
+ collection = db[collection_name]
589
+
590
+ distinct_values = collection.distinct(field_name, query)
591
+
592
+ if close_client:
593
+ mongo_client.close()
594
+
595
+ return distinct_values
596
+
597
+
598
+ def count_entries_in_collection(
599
+ database: Union[str, pymongo.database.Database],
600
+ collection_name: str,
601
+ query: dict = None,
602
+ mongo_client: pymongo.MongoClient = None,
603
+ close_client: bool = False
604
+ ) -> int:
605
+ """
606
+ Count entries in a MongoDB collection by query.
607
+
608
+ :param database: String or the database object.
609
+ str - the name of the database. In this case the database object will be created.
610
+ pymongo.database.Database - the database object that will be used instead of creating a new one.
611
+ :param collection_name: str, the name of the collection.
612
+ :param query: dict, the query to search for.
613
+ Example, search for all entries with column name 'name' equal to 'John':
614
+ query = {'name': 'John'}
615
+ Example, return all entries from collection:
616
+ query = None
617
+ :param mongo_client: pymongo.MongoClient, the connection object.
618
+ If None, a new connection will be created to default URI.
619
+ :param close_client: bool, if True, the connection will be closed after the operation.
620
+
621
+ :return: int, the number of entries that match the query.
622
+ """
623
+
624
+ if not mongo_client:
625
+ mongo_client = connect()
626
+ close_client = True
627
+
628
+ db = _get_pymongo_db_from_string_or_pymongo_db(database, mongo_client)
629
+ collection = db[collection_name]
630
+
631
+ if query is None:
632
+ query = {}
633
+
634
+ count = collection.count_documents(query)
635
+
636
+ if close_client:
637
+ mongo_client.close()
638
+
639
+ return count
640
+
641
+
159
642
  def delete_all_entries_from_collection(
160
- database_name: str,
643
+ database: Union[str, pymongo.database.Database],
161
644
  collection_name: str,
162
645
  mongo_client: pymongo.MongoClient = None,
163
646
  close_client: bool = False
164
647
  ):
165
648
  """
166
649
  Remove all entries from a MongoDB collection.
167
- :param database_name: str, the name of the database.
650
+ :param database: String or the database object.
651
+ str - the name of the database. In this case the database object will be created.
652
+ pymongo.database.Database - the database object that will be used instead of creating a new one.
168
653
  :param collection_name: str, the name of the collection.
169
654
  :param mongo_client: pymongo.MongoClient, the connection object.
170
655
  If None, a new connection will be created to default URI.
@@ -177,7 +662,7 @@ def delete_all_entries_from_collection(
177
662
  mongo_client = connect()
178
663
  close_client = True
179
664
 
180
- db = mongo_client[database_name]
665
+ db = _get_pymongo_db_from_string_or_pymongo_db(database, mongo_client)
181
666
  collection = db[collection_name]
182
667
 
183
668
  collection.delete_many({})
@@ -188,7 +673,7 @@ def delete_all_entries_from_collection(
188
673
 
189
674
  def overwrite_collection(
190
675
  object_instance: list,
191
- database_name: str,
676
+ database: Union[str, pymongo.database.Database],
192
677
  collection_name: str,
193
678
  add_timestamp: bool = False,
194
679
  convert_mixed_lists_to_strings: bool = False,
@@ -198,7 +683,9 @@ def overwrite_collection(
198
683
  """
199
684
  Overwrite a MongoDB collection with list of dicts or a dict.
200
685
  :param object_instance: list of dictionaries, the list of dictionaries to overwrite in the collection.
201
- :param database_name: str, the name of the database.
686
+ :param database: String or the database object.
687
+ str - the name of the database. In this case the database object will be created.
688
+ pymongo.database.Database - the database object that will be used instead of creating a new one.
202
689
  :param collection_name: str, the name of the collection.
203
690
  :param add_timestamp: bool, if True, a current time timestamp will be added to the object.
204
691
  :param convert_mixed_lists_to_strings: bool, if True, mixed lists or tuples when entries are strings and integers,
@@ -217,13 +704,13 @@ def overwrite_collection(
217
704
  close_client = True
218
705
 
219
706
  delete_all_entries_from_collection(
220
- database_name=database_name, collection_name=collection_name,
707
+ database=database, collection_name=collection_name,
221
708
  mongo_client=mongo_client
222
709
  )
223
710
 
224
711
  index(
225
712
  object_instance=object_instance,
226
- database_name=database_name, collection_name=collection_name,
713
+ database=database, collection_name=collection_name,
227
714
  add_timestamp=add_timestamp, convert_mixed_lists_to_strings=convert_mixed_lists_to_strings,
228
715
  mongo_client=mongo_client, close_client=close_client)
229
716
 
@@ -242,15 +729,39 @@ def _is_object_list_of_dicts_or_dict(
242
729
  raise ValueError("Object must be a dictionary or a list of dictionaries.")
243
730
 
244
731
 
732
+ def _get_pymongo_db_from_string_or_pymongo_db(
733
+ database: Union[str, pymongo.database.Database],
734
+ mongo_client: pymongo.MongoClient
735
+ ) -> pymongo.database.Database:
736
+ """
737
+ Get a pymongo.database.Database object from a string or a pymongo.database.Database object.
738
+
739
+ :param database: Union[str, pymongo.database.Database], the database object or the name of the database.
740
+ If the database is a string, the database object will be created.
741
+ If the database is a pymongo.database.Database object, it will be returned as is.
742
+ :param mongo_client: mongodb.MongoClient, the connection object.
743
+ :return: pymongo.database.Database, the database object.
744
+ """
745
+
746
+ if isinstance(database, str):
747
+ return mongo_client[database]
748
+ elif isinstance(database, pymongo.database.Database):
749
+ return database
750
+ else:
751
+ raise ValueError("Database must be a string (database name) or a pymongo.database.Database object.")
752
+
753
+
245
754
  def get_stats_db(
246
- database_name: str,
755
+ database: Union[str, pymongo.database.Database],
247
756
  mongo_client: pymongo.MongoClient = None,
248
757
  close_client: bool = False
249
758
  ):
250
759
  """
251
760
  Get the stats of a MongoDB database.
252
761
 
253
- :param database_name: str, the name of the database.
762
+ :param database: String or the database object.
763
+ str - the name of the database. In this case the database object will be created.
764
+ pymongo.database.Database - the database object that will be used instead of creating a new one.
254
765
  :param mongo_client: pymongo.MongoClient, the connection object.
255
766
  If None, a new connection will be created to default URI.
256
767
  :param close_client: bool, if True, the connection will be closed after the operation.
@@ -262,7 +773,7 @@ def get_stats_db(
262
773
  mongo_client = connect()
263
774
  close_client = True
264
775
 
265
- db = mongo_client[database_name]
776
+ db = _get_pymongo_db_from_string_or_pymongo_db(database, mongo_client)
266
777
 
267
778
  stats = db.command("dbStats")
268
779
 
@@ -273,14 +784,16 @@ def get_stats_db(
273
784
 
274
785
 
275
786
  def get_stats_db_size(
276
- database_name: str,
787
+ database: Union[str, pymongo.database.Database],
277
788
  mongo_client: pymongo.MongoClient = None,
278
789
  close_client: bool = False
279
790
  ):
280
791
  """
281
792
  Get the size of a MongoDB database in bytes.
282
793
 
283
- :param database_name: str, the name of the database.
794
+ :param database: String or the database object.
795
+ str - the name of the database. In this case the database object will be created.
796
+ pymongo.database.Database - the database object that will be used instead of creating a new one.
284
797
  :param mongo_client: pymongo.MongoClient, the connection object.
285
798
  If None, a new connection will be created to default URI.
286
799
  :param close_client: bool, if True, the connection will be closed after the operation.
@@ -289,6 +802,40 @@ def get_stats_db_size(
289
802
  """
290
803
 
291
804
  stats = get_stats_db(
292
- database_name=database_name, mongo_client=mongo_client, close_client=close_client)
805
+ database=database, mongo_client=mongo_client, close_client=close_client)
293
806
 
294
807
  return stats['dataSize']
808
+
809
+
810
+ def _convert_key_values_to_objects(
811
+ keys_convert_to_dict: list[str],
812
+ returned_data: Union[dict, list]
813
+ ) -> Union[dict, list]:
814
+ """
815
+ Recursively searches for provided keys from the 'keys_convert_to_dict' list like 'test1' and 'test2'
816
+ in a nested dictionary 'returned_data' and converts their values using "json.loads" if found.
817
+
818
+ :param keys_convert_to_dict: list, the keys of the documents that should be converted from string to dict.
819
+ :param returned_data: The nested dictionary to search through.
820
+ :type returned_data: dict
821
+ """
822
+
823
+ if isinstance(returned_data, dict):
824
+ for key, value in returned_data.items():
825
+ if key in keys_convert_to_dict:
826
+ # Can be that the value is None, so we don't need to convert it.
827
+ if value is None:
828
+ continue
829
+
830
+ try:
831
+ returned_data[key] = json.loads(value)
832
+ except (ValueError, TypeError):
833
+ # This is needed only to know the possible exception types.
834
+ raise
835
+ else:
836
+ _convert_key_values_to_objects(keys_convert_to_dict, value)
837
+ elif isinstance(returned_data, list):
838
+ for i, item in enumerate(returned_data):
839
+ returned_data[i] = _convert_key_values_to_objects(keys_convert_to_dict, item)
840
+
841
+ return returned_data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: atomicshop
3
- Version: 2.16.16
3
+ Version: 2.16.18
4
4
  Summary: Atomic functions and classes to make developer life easier
5
5
  Author: Denis Kras
6
6
  License: MIT License
@@ -1,4 +1,4 @@
1
- atomicshop/__init__.py,sha256=43IIwVKubkBxqcW9co_kEzLRQCXqxLEBrGIx94aWzjg,124
1
+ atomicshop/__init__.py,sha256=oNmVsW_-ZdNWYFu1cVLreluzXWdxRiR3VEX5qDGDz5A,124
2
2
  atomicshop/_basics_temp.py,sha256=6cu2dd6r2dLrd1BRNcVDKTHlsHs_26Gpw8QS6v32lQ0,3699
3
3
  atomicshop/_create_pdf_demo.py,sha256=Yi-PGZuMg0RKvQmLqVeLIZYadqEZwUm-4A9JxBl_vYA,3713
4
4
  atomicshop/_patch_import.py,sha256=ENp55sKVJ0e6-4lBvZnpz9PQCt3Otbur7F6aXDlyje4,6334
@@ -145,7 +145,7 @@ atomicshop/mitm/engines/__reference_general/recorder___reference_general.py,sha2
145
145
  atomicshop/mitm/engines/__reference_general/responder___reference_general.py,sha256=1AM49UaFTKA0AHw-k3SV3uH3QbG-o6ux0c-GoWkKNU0,6993
146
146
  atomicshop/mitm/statistic_analyzer_helper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
147
147
  atomicshop/mitm/statistic_analyzer_helper/analyzer_helper.py,sha256=pk6L1t1ea1kvlBoR9QEJptOmaX-mumhwLsP2GCKukbk,5920
148
- atomicshop/mitm/statistic_analyzer_helper/moving_average_helper.py,sha256=lsG_eSWf6M_3AfWqIYEBNwYAyVHJfBiRVKHhmWIOZ9A,16615
148
+ atomicshop/mitm/statistic_analyzer_helper/moving_average_helper.py,sha256=Wge-OfbbClfjeEWwOyksd-x9C5QZdRD3KRSjtP9VL9Q,16651
149
149
  atomicshop/monitor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
150
150
  atomicshop/monitor/change_monitor.py,sha256=K5NlVp99XIDDPnQQMdru4BDmua_DtcDIhVAzkTOvD5s,7673
151
151
  atomicshop/monitor/checks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -248,13 +248,13 @@ atomicshop/wrappers/loggingw/consts.py,sha256=JWiUJEydjhwatBxtIJsGTmDUSTLbmIRidt
248
248
  atomicshop/wrappers/loggingw/filters.py,sha256=CMs5PAMb68zxJgBcQobaOFDG5kLJBOVYnoBHjDgksO8,2859
249
249
  atomicshop/wrappers/loggingw/formatters.py,sha256=7XUJvlB0CK4DCkEp8NTL0S0dkyrZD0UTADgEwkStKOY,5483
250
250
  atomicshop/wrappers/loggingw/handlers.py,sha256=hAPFJQ-wFoNO8QzGrJRSvyuP09Q1F0Dl9_w7zzlgcW0,18155
251
- atomicshop/wrappers/loggingw/loggers.py,sha256=DHOOTAtqkwn1xgvLHSkOiBm6yFGNuQy1kvbhG-TDog8,2374
252
- atomicshop/wrappers/loggingw/loggingw.py,sha256=_VT5aqYgUVI6uurrnfm0_tAYO41Aavs1SSnlU6gfOiQ,11808
253
- atomicshop/wrappers/loggingw/reading.py,sha256=tplnJlQ7RMxWv2s782tWGOo1C7WNemk2SRTZCCgD9J0,16474
251
+ atomicshop/wrappers/loggingw/loggers.py,sha256=QH5QainlGLyrDpsDu4T1C8-WQQau3JW2OS5RgC-kXpM,2677
252
+ atomicshop/wrappers/loggingw/loggingw.py,sha256=6HUn2z4ZW8PgakPscosKx23qYwHlBQcLiZGw-VZHi-k,12374
253
+ atomicshop/wrappers/loggingw/reading.py,sha256=SZFE4d6IFoC1GveFf28oAuDQzHG8UnBHx3K3-dE5Mes,16528
254
254
  atomicshop/wrappers/mongodbw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
255
255
  atomicshop/wrappers/mongodbw/install_mongodb.py,sha256=3ZPqrXxj3lC-PnAKGXclylLuOqsbyXYeUpb5iGjdeUU,6626
256
- atomicshop/wrappers/mongodbw/mongo_infra.py,sha256=JO63ShbHYRBmUdFv-GeJxkPQdhRhq59ly6bxnn64fUM,1713
257
- atomicshop/wrappers/mongodbw/mongodbw.py,sha256=_K1alcxAAIiMhwJYNB82OD-FjQVQijxp2UYnq-g1z98,9860
256
+ atomicshop/wrappers/mongodbw/mongo_infra.py,sha256=IjEF0jPzQz866MpTm7rnksnyyWQeUT_B2h2DA9ryAio,2034
257
+ atomicshop/wrappers/mongodbw/mongodbw.py,sha256=v_5YYgbgT0F-k2I-hDHQCMPhpry8OonsPclubJk2NG0,31509
258
258
  atomicshop/wrappers/nodejsw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
259
259
  atomicshop/wrappers/nodejsw/install_nodejs.py,sha256=QZg-R2iTQt7kFb8wNtnTmwraSGwvUs34JIasdbNa7ZU,5154
260
260
  atomicshop/wrappers/playwrightw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -309,8 +309,8 @@ atomicshop/wrappers/socketw/ssl_base.py,sha256=kmiif84kMhBr5yjQW17p935sfjR5JKG0L
309
309
  atomicshop/wrappers/socketw/statistics_csv.py,sha256=V_m1D0KpizQox3IEWp2AUcncwWy5kG25hbFrc-mBSJE,3029
310
310
  atomicshop/wrappers/winregw/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
311
311
  atomicshop/wrappers/winregw/winreg_network.py,sha256=bQ8Jql8bVGBJ0dt3VQ56lga_1LBOMLI3Km_otvvbU6c,7138
312
- atomicshop-2.16.16.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
313
- atomicshop-2.16.16.dist-info/METADATA,sha256=JpxLeyrTWZ_qfqn0SMMQtTi9nPcAOj3YLYkPeh-YAJw,10473
314
- atomicshop-2.16.16.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
315
- atomicshop-2.16.16.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
316
- atomicshop-2.16.16.dist-info/RECORD,,
312
+ atomicshop-2.16.18.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
313
+ atomicshop-2.16.18.dist-info/METADATA,sha256=b68lhNlBqxZwdV4zttZaQcCUAF18IPOHwKq4PaFIAkA,10473
314
+ atomicshop-2.16.18.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
315
+ atomicshop-2.16.18.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
316
+ atomicshop-2.16.18.dist-info/RECORD,,