atomicshop 2.16.17__py3-none-any.whl → 2.16.19__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.17'
4
+ __version__ = '2.16.19'
atomicshop/filesystem.py CHANGED
@@ -422,17 +422,33 @@ def temporary_change_working_directory(new_working_directory: str) -> None:
422
422
  os.chdir(original_working_directory)
423
423
 
424
424
 
425
- def move_file(source_file_path: str, target_file_path: str, overwrite: bool = True) -> None:
425
+ def move_file(source_file_path: str, target_directory: str, overwrite: bool = True) -> None:
426
426
  """
427
427
  The function moves file from source to target.
428
428
 
429
429
  :param source_file_path: string, full path to source file.
430
- :param target_file_path: string, full path to target file.
430
+ :param target_directory: string, full path to target directory.
431
431
  :param overwrite: boolean, if 'False', then the function will not overwrite the file if it exists.
432
432
 
433
+ Example:
434
+ Before move:
435
+ Source file = 'C:/Users/user1/Downloads/file-to-move.txt'
436
+ Move to directory = 'C:/Users/user1/Documents'
437
+
438
+ Move:
439
+ source_file_path = 'C:/Users/user1/Downloads/file-to-move.txt'
440
+ target_directory = 'C:/Users/user1/Documents'
441
+ move_file(source_file_path, target_file_path)
442
+
443
+ After move:
444
+ 'C:/Users/user1/Downloads'
445
+ 'C:/Users/user1/Documents/file-to-move.txt'
446
+
433
447
  :return: None
434
448
  """
435
449
 
450
+ target_file_path = target_directory + os.sep + Path(source_file_path).name
451
+
436
452
  # Check if 'no_overwrite' is set to 'True' and if the file exists.
437
453
  if not overwrite:
438
454
  if is_file_exists(target_file_path):
@@ -455,12 +471,17 @@ def move_folder(source_directory: str, target_directory: str, overwrite: bool =
455
471
  ------------------------------
456
472
 
457
473
  Example:
458
- source_directory = 'C:/Users/user1/Downloads/folder-to-move'
459
- target_directory = 'C:/Users/user1/Documents'
460
- move_folder(source_directory, target_directory)
474
+ Before move:
475
+ Source folder = 'C:/Users/user1/Downloads/folder-to-move'
476
+ Move to directory = 'C:/Users/user1/Documents'
477
+
478
+ Move:
479
+ source_directory = 'C:/Users/user1/Downloads/folder-to-move'
480
+ target_directory = 'C:/Users/user1/Documents'
481
+ move_folder(source_directory, target_directory)
461
482
 
462
- Result path of the 'folder-to-move' will be:
463
- 'C:/Users/user1/Documents/folder-to-move'
483
+ Result path of the 'folder-to-move' will be:
484
+ 'C:/Users/user1/Documents/folder-to-move'
464
485
 
465
486
  """
466
487
 
@@ -473,14 +494,14 @@ def move_folder(source_directory: str, target_directory: str, overwrite: bool =
473
494
  shutil.move(source_directory, target_directory)
474
495
 
475
496
 
476
- def move_files_from_folder_to_folder(
497
+ def move_top_level_files_from_folder_to_folder(
477
498
  source_directory: str,
478
499
  target_directory: str,
479
500
  overwrite: bool = True
480
501
  ):
481
502
  """
482
- The function is currently non-recursive and not tested with directories inside the source directories.
483
- The function will move all the files from source directory to target directory overwriting existing files.
503
+ The function is non-recursive to move only top level files from source directory to target directory
504
+ overwriting existing files.
484
505
 
485
506
  :param source_directory: string, full path to source directory.
486
507
  :param target_directory: string, full path to target directory.
@@ -488,26 +509,50 @@ def move_files_from_folder_to_folder(
488
509
  """
489
510
 
490
511
  # Iterate over each item in the source directory
491
- for item in os.listdir(source_directory):
492
- # Construct full file path
493
- source_item = os.path.join(source_directory, item)
494
- destination_item = os.path.join(target_directory, item)
512
+ top_level_files: list[str] = get_paths_from_directory(
513
+ directory_path=source_directory, get_file=True, recursive=False, simple_list=True)
495
514
 
515
+ for source_item in top_level_files:
496
516
  # Move each item to the destination directory
497
- move_file(source_file_path=source_item, target_file_path=destination_item, overwrite=overwrite)
517
+ move_file(source_file_path=source_item, target_directory=target_directory, overwrite=overwrite)
498
518
 
499
- # # Get all file names without full paths in source folder.
500
- # file_list_in_source: list = get_paths_from_directory(source_directory, get_file=True)
501
- #
502
- # # Iterate through all the files.
503
- # for file_path in file_list_in_source:
504
- # # Move the file from source to target.
505
- # if file_path.relative_dir:
506
- # create_directory(target_directory + os.sep + file_path.relative_dir)
507
- # relative_file_path: str = file_path.relative_dir + os.sep + Path(file_path.path).name
508
- # else:
509
- # relative_file_path: str = Path(file_path.path).name
510
- # move_file(file_path.path, target_directory + os.sep + relative_file_path)
519
+
520
+ def move_folder_contents_to_folder(
521
+ source_directory: str,
522
+ target_directory: str,
523
+ overwrite: bool = True
524
+ ):
525
+ """
526
+ The function moves all the contents of the source directory to the target directory.
527
+ If target directory is inside the source directory, this folder will be skipped.
528
+
529
+ :param source_directory: string, full path to source directory.
530
+ :param target_directory: string, full path to target directory.
531
+ :param overwrite: boolean, if 'True', then the function will overwrite the files if they exist.
532
+ """
533
+
534
+ # Make sure the destination directory exists, if not create it
535
+ os.makedirs(target_directory, exist_ok=True)
536
+
537
+ # Move contents of the source directory to the destination directory
538
+ for item in os.listdir(source_directory):
539
+ s = os.path.join(source_directory, item)
540
+ d = os.path.join(target_directory, item)
541
+
542
+ # if the target directory is inside the source directory, skip it
543
+ if os.path.abspath(target_directory).startswith(os.path.abspath(s)):
544
+ continue
545
+
546
+ if os.path.isdir(s):
547
+ if os.path.exists(d) and not overwrite:
548
+ print(f"Directory {d} already exists. Skipping due to overwrite=False.")
549
+ else:
550
+ shutil.move(s, d)
551
+ else:
552
+ if os.path.exists(d) and not overwrite:
553
+ print(f"File {d} already exists. Skipping due to overwrite=False.")
554
+ else:
555
+ shutil.move(s, d)
511
556
 
512
557
 
513
558
  def copy_file(
@@ -681,7 +726,7 @@ def get_paths_from_directory(
681
726
  sort_by_last_modified_time: bool = False,
682
727
  add_file_binary: bool = False,
683
728
  add_file_hash: bool = False,
684
- ) -> list[AtomicPath]:
729
+ ) -> Union[list[AtomicPath], list[str]]:
685
730
  """
686
731
  Recursive, by option.
687
732
  The function receives a filesystem directory as string, scans it recursively for files and returns list of
@@ -1524,7 +1569,7 @@ def backup_file(
1524
1569
  else:
1525
1570
  file_name: str = f"{file_name_no_extension}_{timestamp}{file_extension}"
1526
1571
  backup_file_path: str = str(Path(backup_directory) / file_name)
1527
- move_file(file_path, backup_file_path)
1572
+ move_file(file_path, backup_directory)
1528
1573
 
1529
1574
  return backup_file_path
1530
1575
  else:
@@ -51,7 +51,7 @@ def recs_archiver(recs_directory: str) -> list:
51
51
  target_directory_path: str = f"{directory_path.path}{os.sep}{recs_atomic_path.datetime_string}"
52
52
  filesystem.create_directory(target_directory_path)
53
53
  filesystem.move_file(
54
- recs_atomic_path.path, f'{target_directory_path}{os.sep}{recs_atomic_path.name}')
54
+ recs_atomic_path.path, target_directory_path)
55
55
 
56
56
  # Archive directories.
57
57
  archive_directories: list = filesystem.get_paths_from_directory(
@@ -207,8 +207,7 @@ def get_all_log_files_into_list(
207
207
  filesystem.create_directory(move_to_path_with_timestamp)
208
208
  # Move the statistics files.
209
209
  for single_file in logs_files:
210
- move_to_path_with_file = f'{move_to_path_with_timestamp}{os.sep}{single_file.name}'
211
- filesystem.move_file(single_file.path, move_to_path_with_file)
210
+ filesystem.move_file(single_file.path, move_to_path_with_timestamp)
212
211
 
213
212
  return logs_content
214
213
 
@@ -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.17
3
+ Version: 2.16.19
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=8j7ZCpRQjA2znKZa29KoZQlCV-GjDBzWXCHWB8Kft3c,124
1
+ atomicshop/__init__.py,sha256=ZX5xejuYVvRch1tJs02jeNOwNajL4QyvDzPyQpk4C7s,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
@@ -14,7 +14,7 @@ atomicshop/dns.py,sha256=J4yX6vCaRdL0McYWnlJ9arCDKW-yRui7Y5WyL5BoD6M,6391
14
14
  atomicshop/domains.py,sha256=Rxu6JhhMqFZRcoFs69IoEd1PtYca0lMCG6F1AomP7z4,3197
15
15
  atomicshop/emails.py,sha256=I0KyODQpIMEsNRi9YWSOL8EUPBiWyon3HRdIuSj3AEU,1410
16
16
  atomicshop/file_types.py,sha256=-0jzQMRlmU1AP9DARjk-HJm1tVE22E6ngP2mRblyEjY,763
17
- atomicshop/filesystem.py,sha256=5xkZFIKYoTTmRBkJo5p4R1G48Xbdjlyc7R514QqNXQs,58711
17
+ atomicshop/filesystem.py,sha256=XPrOjqxQRP3fa01QztDvuDUpiFjCYtpPUC0KhaGHxqs,60222
18
18
  atomicshop/functions.py,sha256=pK8hoCE9z61PtWCxQJsda7YAphrLH1wxU5x-1QJP-sY,499
19
19
  atomicshop/get_process_list.py,sha256=8cxb7gKe9sl4R6H2yMi8J6oe-RkonTvCdKjRFqi-Fs4,6075
20
20
  atomicshop/get_process_name_cmd_dll.py,sha256=CtaSp3mgxxJKCCVW8BLx6BJNx4giCklU_T7USiCEwfc,5162
@@ -129,7 +129,7 @@ atomicshop/mitm/import_config.py,sha256=_nu8mgA-M4s6dZ8_QWx3x0aVb75upvsCuX_PIUg4
129
129
  atomicshop/mitm/initialize_engines.py,sha256=kBG8TBnyFuwlJ1uKaWDzc5AiZNpwdvouq2pr-PYrdEA,8349
130
130
  atomicshop/mitm/message.py,sha256=d_sm3O_aoZf87dDQP44xOMNEG-uZBN1ZecQgMCacbZs,1814
131
131
  atomicshop/mitm/mitm_main.py,sha256=CdCv4nYt_jwd23AI14v6lC2H8SZeIZqsXjFhwq61UtM,21285
132
- atomicshop/mitm/recs_files.py,sha256=4k6vCemGRmZ605yZ0-yRpF1ny3eAns7f3j61UXTJ99M,2966
132
+ atomicshop/mitm/recs_files.py,sha256=B8fSuvYXlh50LWfwLRw_bYswreTjmdZLuHJzbDC5Gss,2930
133
133
  atomicshop/mitm/shared_functions.py,sha256=hplm98tz8pgJ4WHUVI9sf_oVqUM2KJ1Y2pD6EFSb8P0,1879
134
134
  atomicshop/mitm/statistic_analyzer.py,sha256=AzL9rQhg0tLJj33gZfxdwWghmbXGLh_HyMBDpzuBmsQ,24709
135
135
  atomicshop/mitm/engines/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -250,11 +250,11 @@ atomicshop/wrappers/loggingw/formatters.py,sha256=7XUJvlB0CK4DCkEp8NTL0S0dkyrZD0
250
250
  atomicshop/wrappers/loggingw/handlers.py,sha256=hAPFJQ-wFoNO8QzGrJRSvyuP09Q1F0Dl9_w7zzlgcW0,18155
251
251
  atomicshop/wrappers/loggingw/loggers.py,sha256=QH5QainlGLyrDpsDu4T1C8-WQQau3JW2OS5RgC-kXpM,2677
252
252
  atomicshop/wrappers/loggingw/loggingw.py,sha256=6HUn2z4ZW8PgakPscosKx23qYwHlBQcLiZGw-VZHi-k,12374
253
- atomicshop/wrappers/loggingw/reading.py,sha256=SZFE4d6IFoC1GveFf28oAuDQzHG8UnBHx3K3-dE5Mes,16528
253
+ atomicshop/wrappers/loggingw/reading.py,sha256=ERBSiQbEksySKpXpu2E_6k9dZ6MPH95ZIsmdjWW9MUE,16436
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.17.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
313
- atomicshop-2.16.17.dist-info/METADATA,sha256=hh2ElTNYbFQfLq_0PnD9wptNCmnlx06CLdEMgs-DWDo,10473
314
- atomicshop-2.16.17.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
315
- atomicshop-2.16.17.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
316
- atomicshop-2.16.17.dist-info/RECORD,,
312
+ atomicshop-2.16.19.dist-info/LICENSE.txt,sha256=lLU7EYycfYcK2NR_1gfnhnRC8b8ccOTElACYplgZN88,1094
313
+ atomicshop-2.16.19.dist-info/METADATA,sha256=rjqYESm2qP69gn_SoSyX0Ki4mxg4z4cvCJfuxC2xEcY,10473
314
+ atomicshop-2.16.19.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
315
+ atomicshop-2.16.19.dist-info/top_level.txt,sha256=EgKJB-7xcrAPeqTRF2laD_Np2gNGYkJkd4OyXqpJphA,11
316
+ atomicshop-2.16.19.dist-info/RECORD,,