database-mongodb-local 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. database_mongodb_local-0.0.1/PKG-INFO +24 -0
  2. database_mongodb_local-0.0.1/README.md +23 -0
  3. database_mongodb_local-0.0.1/database_mongodb_local/__init__.py +0 -0
  4. database_mongodb_local-0.0.1/database_mongodb_local/src/__init__.py +0 -0
  5. database_mongodb_local-0.0.1/database_mongodb_local/src/connector.py +51 -0
  6. database_mongodb_local-0.0.1/database_mongodb_local/src/constants_src.py +24 -0
  7. database_mongodb_local-0.0.1/database_mongodb_local/src/decorators.py +27 -0
  8. database_mongodb_local-0.0.1/database_mongodb_local/src/exceptions.py +14 -0
  9. database_mongodb_local-0.0.1/database_mongodb_local/src/generic_crud.py +341 -0
  10. database_mongodb_local-0.0.1/database_mongodb_local/src/generic_crud_ml.py +317 -0
  11. database_mongodb_local-0.0.1/database_mongodb_local/src/generic_mapping.py +135 -0
  12. database_mongodb_local-0.0.1/database_mongodb_local/src/version.py +5 -0
  13. database_mongodb_local-0.0.1/database_mongodb_local/tests/__init__.py +0 -0
  14. database_mongodb_local-0.0.1/database_mongodb_local/tests/generic_crud_ml_system_test.py +99 -0
  15. database_mongodb_local-0.0.1/database_mongodb_local/tests/generic_crud_ml_test.py +264 -0
  16. database_mongodb_local-0.0.1/database_mongodb_local/tests/generic_crud_system_test.py +76 -0
  17. database_mongodb_local-0.0.1/database_mongodb_local/tests/generic_crud_test.py +232 -0
  18. database_mongodb_local-0.0.1/database_mongodb_local/tests/generic_mapping_system_test.py +69 -0
  19. database_mongodb_local-0.0.1/database_mongodb_local/tests/generic_mapping_test.py +78 -0
  20. database_mongodb_local-0.0.1/database_mongodb_local.egg-info/PKG-INFO +24 -0
  21. database_mongodb_local-0.0.1/database_mongodb_local.egg-info/SOURCES.txt +25 -0
  22. database_mongodb_local-0.0.1/database_mongodb_local.egg-info/dependency_links.txt +1 -0
  23. database_mongodb_local-0.0.1/database_mongodb_local.egg-info/requires.txt +4 -0
  24. database_mongodb_local-0.0.1/database_mongodb_local.egg-info/top_level.txt +1 -0
  25. database_mongodb_local-0.0.1/pyproject.toml +27 -0
  26. database_mongodb_local-0.0.1/setup.cfg +4 -0
  27. database_mongodb_local-0.0.1/setup.py +28 -0
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: database-mongodb-local
3
+ Version: 0.0.1
4
+ Summary: PyPI database-mongodb-local Python Package owned by Circlez.ai
5
+ Home-page: https://github.com/circles-zone/database-mongodb-local-python-package
6
+ Author: Circles
7
+ Author-email: info@circlez.ai
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Operating System :: OS Independent
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: pymongo>=4.0
12
+ Requires-Dist: python-dotenv
13
+ Requires-Dist: database-infrastructure-local>=0.1.8
14
+ Requires-Dist: logger-local==0.0.186b2614
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: classifier
18
+ Dynamic: description
19
+ Dynamic: description-content-type
20
+ Dynamic: home-page
21
+ Dynamic: requires-dist
22
+ Dynamic: summary
23
+
24
+ PyPI database-mongodb-local Python Package owned by Circlez.ai
@@ -0,0 +1,23 @@
1
+ # General
2
+
3
+ Unlike the README.md in the root directory, this is a README.md of this specific repo/package<br>
4
+
5
+ TODO Please update this README.md of this specific repo/package and not the README.md in the root directory based on the python-package-template repo<br>
6
+
7
+ ## TODOs
8
+
9
+ TODO example of a TODO, please it with a real TODO<br>
10
+
11
+ ## Versions
12
+
13
+ [pub] 0.0.1 Initial version (change the directories, files, setup.py, .github/workflows/*.yml)<br>
14
+
15
+ ## Installing instructions
16
+
17
+ It's advised to use venv<br>
18
+ If you have Windows, it's also advised to use Windows Subsystem for Linux<br>
19
+
20
+ ## Install dependencies
21
+
22
+ Run ./download-beta-sdk.sh from the bash terminal<br>
23
+ Run pip install -r requirements.txt from the bash terminal<br>
@@ -0,0 +1,51 @@
1
+ import os
2
+ from functools import lru_cache
3
+
4
+ from dotenv import load_dotenv
5
+ from pymongo import MongoClient
6
+ from pymongo.database import Database
7
+
8
+ load_dotenv()
9
+
10
+ DEFAULT_HOST = "localhost"
11
+ DEFAULT_PORT = 27017
12
+
13
+
14
+ def _is_mock() -> bool:
15
+ return os.getenv("MONGODB_MOCK", "false").lower() in ("1", "true", "yes")
16
+
17
+
18
+ @lru_cache(maxsize=None)
19
+ def _get_real_client() -> MongoClient:
20
+ host = os.getenv("MONGODB_HOST", DEFAULT_HOST)
21
+ port = int(os.getenv("MONGODB_PORT", DEFAULT_PORT))
22
+ username = os.getenv("MONGODB_USERNAME")
23
+ password = os.getenv("MONGODB_PASSWORD")
24
+
25
+ if username and password:
26
+ return MongoClient(host=host, port=port, username=username, password=password)
27
+ return MongoClient(host=host, port=port)
28
+
29
+
30
+ class Connector:
31
+ """Single owner of MongoDB connection creation (information hiding).
32
+
33
+ Callers never build a client themselves — they ask Connector for a
34
+ database. When ``MONGODB_MOCK`` is set, a fresh in-memory mongomock
35
+ database is returned so tests run without a real MongoDB.
36
+ """
37
+
38
+ @staticmethod
39
+ def get_database(database_name: str) -> Database:
40
+ if _is_mock():
41
+ import mongomock
42
+ return mongomock.MongoClient()[database_name]
43
+ return _get_real_client()[database_name]
44
+
45
+ @staticmethod
46
+ def is_mongo_available() -> bool:
47
+ try:
48
+ _get_real_client().admin.command("ping")
49
+ return True
50
+ except Exception:
51
+ return False
@@ -0,0 +1,24 @@
1
+ from logger_local.LoggerComponentEnum import LoggerComponentEnum
2
+
3
+ DEVELOPER_EMAIL_ADDRESS = "dor3382@gmail.com"
4
+
5
+ CRUD_MONGODB_CODE_LOGGER_OBJECT = {
6
+ "component_id": 1,
7
+ "component_name": "database-mongodb-local/GenericCrudMongodb",
8
+ "component_category": LoggerComponentEnum.ComponentCategory.Code.value,
9
+ "developer_email_address": DEVELOPER_EMAIL_ADDRESS,
10
+ }
11
+
12
+ CRUD_ML_CODE_LOGGER_OBJECT = {
13
+ "component_id": 2,
14
+ "component_name": "database-mongodb-local/GenericCrudMlMongoDB",
15
+ "component_category": LoggerComponentEnum.ComponentCategory.Code.value,
16
+ "developer_email_address": DEVELOPER_EMAIL_ADDRESS,
17
+ }
18
+
19
+ MAPPING_CODE_LOGGER_OBJECT = {
20
+ "component_id": 3,
21
+ "component_name": "database-mongodb-local/GenericMappingMongodb",
22
+ "component_category": LoggerComponentEnum.ComponentCategory.Code.value,
23
+ "developer_email_address": DEVELOPER_EMAIL_ADDRESS,
24
+ }
@@ -0,0 +1,27 @@
1
+ import functools
2
+
3
+ from pymongo.errors import (
4
+ ConnectionFailure,
5
+ DuplicateKeyError,
6
+ PyMongoError,
7
+ ServerSelectionTimeoutError,
8
+ )
9
+
10
+ from .exceptions import DatabaseConnectionError, DatabaseDuplicateError, DatabaseOperationError
11
+
12
+
13
+ def handle_db_errors(func):
14
+ @functools.wraps(func)
15
+ def wrapper(*args, **kwargs):
16
+ try:
17
+ return func(*args, **kwargs)
18
+ except DuplicateKeyError as e:
19
+ raise DatabaseDuplicateError(
20
+ f"Duplicate key error in {func.__name__}: {e}") from e
21
+ except (ConnectionFailure, ServerSelectionTimeoutError) as e:
22
+ raise DatabaseConnectionError(
23
+ f"Connection error in {func.__name__}: {e}") from e
24
+ except PyMongoError as e:
25
+ raise DatabaseOperationError(
26
+ f"Database error in {func.__name__}: {e}") from e
27
+ return wrapper
@@ -0,0 +1,14 @@
1
+ class DatabaseError(RuntimeError):
2
+ pass
3
+
4
+
5
+ class DatabaseDuplicateError(DatabaseError):
6
+ pass
7
+
8
+
9
+ class DatabaseConnectionError(DatabaseError):
10
+ pass
11
+
12
+
13
+ class DatabaseOperationError(DatabaseError):
14
+ pass
@@ -0,0 +1,341 @@
1
+ from datetime import datetime, timezone
2
+ from typing import Any, Optional
3
+
4
+ from bson import ObjectId
5
+ from pymongo.collection import Collection
6
+ from pymongo.errors import DuplicateKeyError
7
+
8
+ from logger_local.MetaLogger import ABCMetaLogger
9
+
10
+ from database_infrastructure_local.generic_crud_abstract import GenericCrudAbstract
11
+
12
+ from .connector import Connector
13
+ from .constants_src import CRUD_MONGODB_CODE_LOGGER_OBJECT
14
+ from .decorators import handle_db_errors
15
+
16
+
17
+ class GenericCrudMongodb(GenericCrudAbstract, metaclass=ABCMetaLogger,
18
+ object=CRUD_MONGODB_CODE_LOGGER_OBJECT):
19
+ """MongoDB implementation of the shared GenericCrud interface.
20
+
21
+ To keep the interface consistent across MySQL, PostgreSQL and MongoDB,
22
+ this class uses the same method names and parameters as the MySQL DAL:
23
+ ``schema_name`` maps to a MongoDB database and ``table_name`` maps to a
24
+ MongoDB collection.
25
+ """
26
+
27
+ def __init__(self, *,
28
+ default_schema_name: str,
29
+ default_table_name: str,
30
+ is_test_data: bool = False) -> None:
31
+ self.default_schema_name = default_schema_name
32
+ self.default_table_name = default_table_name
33
+ self.is_test_data = is_test_data
34
+ self._db = Connector.get_database(default_schema_name)
35
+
36
+ def _collection(self, table_name: str = None) -> Collection:
37
+ return self._db[table_name or self.default_table_name]
38
+
39
+ def _live_filter(self, extra: dict = None) -> dict:
40
+ """Returns a filter that excludes soft-deleted documents.
41
+
42
+ All select/update/delete operations automatically apply this filter —
43
+ documents with a non-null end_timestamp are invisible to callers.
44
+ Use undelete_by_id to restore a soft-deleted document.
45
+ """
46
+ f = dict(extra or {})
47
+ f["end_timestamp"] = None
48
+ return f
49
+
50
+ # ------------------------------------------------------------------ insert
51
+
52
+ @handle_db_errors
53
+ def insert(self, *, schema_name: str = None, table_name: str = None,
54
+ data_dict: dict = None,
55
+ ignore_duplicate: bool = False,
56
+ commit_changes: bool = True) -> Optional[ObjectId]:
57
+ table_name = table_name or self.default_table_name
58
+ data_dict = dict(data_dict or {})
59
+ data_dict.setdefault("end_timestamp", None)
60
+ data_dict.setdefault("is_test_data", self.is_test_data)
61
+ data_dict["created_timestamp"] = datetime.now(timezone.utc)
62
+ data_dict["updated_timestamp"] = datetime.now(timezone.utc)
63
+ try:
64
+ result = self._collection(table_name).insert_one(data_dict)
65
+ return result.inserted_id
66
+ except DuplicateKeyError:
67
+ if ignore_duplicate:
68
+ return None
69
+ raise
70
+
71
+ @handle_db_errors
72
+ def insert_if_not_exists_by_field(self, *, table_name: str = None,
73
+ field: str,
74
+ data_dict: dict) -> Optional[ObjectId]:
75
+ table_name = table_name or self.default_table_name
76
+ existing = self._collection(table_name).find_one(
77
+ self._live_filter({field: data_dict[field]}), {"_id": 1})
78
+ if existing:
79
+ return None
80
+ return self.insert(table_name=table_name, data_dict=data_dict)
81
+
82
+ @handle_db_errors
83
+ def insert_if_not_exists(self, *, table_name: str = None,
84
+ data_dict: dict = None,
85
+ data_dict_compare: dict = None) -> Optional[ObjectId]:
86
+ table_name = table_name or self.default_table_name
87
+ data_dict_compare = data_dict_compare or data_dict
88
+ existing = self._collection(table_name).find_one(
89
+ self._live_filter(data_dict_compare), {"_id": 1})
90
+ if existing:
91
+ return existing["_id"]
92
+ return self.insert(table_name=table_name, data_dict=data_dict)
93
+
94
+ @handle_db_errors
95
+ def insert_many(self, *, table_name: str = None,
96
+ data_dicts: list[dict]) -> int:
97
+ if not data_dicts:
98
+ return 0
99
+ table_name = table_name or self.default_table_name
100
+ now = datetime.now(timezone.utc)
101
+ docs = [dict(d) for d in data_dicts]
102
+ for doc in docs:
103
+ doc.setdefault("end_timestamp", None)
104
+ doc.setdefault("is_test_data", self.is_test_data)
105
+ doc.setdefault("created_timestamp", now)
106
+ doc.setdefault("updated_timestamp", now)
107
+ result = self._collection(table_name).insert_many(docs)
108
+ return len(result.inserted_ids)
109
+
110
+ # ------------------------------------------------------------------ upsert
111
+
112
+ @handle_db_errors
113
+ def upsert(self, *, table_name: str = None,
114
+ data_dict: dict = None,
115
+ data_dict_compare: dict = None) -> Optional[ObjectId]:
116
+ table_name = table_name or self.default_table_name
117
+ data_dict = dict(data_dict or {})
118
+ data_dict["updated_timestamp"] = datetime.now(timezone.utc)
119
+
120
+ if not data_dict_compare:
121
+ return self.insert(table_name=table_name, data_dict=data_dict)
122
+
123
+ existing = self._collection(table_name).find_one(
124
+ self._live_filter(data_dict_compare), {"_id": 1})
125
+ if existing:
126
+ self._collection(table_name).update_one(
127
+ {"_id": existing["_id"]}, {"$set": data_dict})
128
+ return existing["_id"]
129
+ return self.insert(table_name=table_name, data_dict=data_dict)
130
+
131
+ # ------------------------------------------------------------------ update
132
+
133
+ @handle_db_errors
134
+ def update_by_id(self, *, table_name: str = None,
135
+ document_id: ObjectId,
136
+ data_dict: dict) -> int:
137
+ return self.update_by_where(
138
+ table_name=table_name,
139
+ where={"_id": document_id},
140
+ data_dict=data_dict)
141
+
142
+ @handle_db_errors
143
+ def update_by_column_and_value(self, *, table_name: str = None,
144
+ column_name: str,
145
+ column_value: Any,
146
+ data_dict: dict) -> int:
147
+ return self.update_by_where(
148
+ table_name=table_name,
149
+ where={column_name: column_value},
150
+ data_dict=data_dict)
151
+
152
+ @handle_db_errors
153
+ def update_by_where(self, *, table_name: str = None,
154
+ where: dict,
155
+ data_dict: dict) -> int:
156
+ """``where`` is a MongoDB filter dict (the MongoDB equivalent of a SQL WHERE clause)."""
157
+ table_name = table_name or self.default_table_name
158
+ data_dict = dict(data_dict)
159
+ data_dict["updated_timestamp"] = datetime.now(timezone.utc)
160
+ result = self._collection(table_name).update_many(
161
+ self._live_filter(where), {"$set": data_dict})
162
+ return result.modified_count
163
+
164
+ # ------------------------------------------------------------------ delete
165
+
166
+ @handle_db_errors
167
+ def delete_by_id(self, *, table_name: str = None,
168
+ document_id: ObjectId) -> int:
169
+ return self.delete_by_where(
170
+ table_name=table_name,
171
+ where={"_id": document_id})
172
+
173
+ @handle_db_errors
174
+ def delete_by_column_and_value(self, *, table_name: str = None,
175
+ column_name: str,
176
+ column_value: Any) -> int:
177
+ return self.delete_by_where(
178
+ table_name=table_name,
179
+ where={column_name: column_value})
180
+
181
+ @handle_db_errors
182
+ def delete_by_where(self, *, table_name: str = None,
183
+ where: dict) -> int:
184
+ """``where`` is a MongoDB filter dict (the MongoDB equivalent of a SQL WHERE clause)."""
185
+ table_name = table_name or self.default_table_name
186
+ result = self._collection(table_name).update_many(
187
+ self._live_filter(where),
188
+ {"$set": {"end_timestamp": datetime.now(timezone.utc)}})
189
+ return result.modified_count
190
+
191
+ @handle_db_errors
192
+ def undelete_by_id(self, *, table_name: str = None,
193
+ document_id: ObjectId) -> int:
194
+ table_name = table_name or self.default_table_name
195
+ result = self._collection(table_name).update_one(
196
+ {"_id": document_id}, {"$set": {"end_timestamp": None}})
197
+ return result.modified_count
198
+
199
+ # ------------------------------------------------------------------ select
200
+
201
+ @handle_db_errors
202
+ def select_one_dict_by_id(self, *,
203
+ table_name: str = None,
204
+ document_id: ObjectId) -> Optional[dict]:
205
+ return self.select_one_dict_by_where(
206
+ table_name=table_name,
207
+ where={"_id": document_id})
208
+
209
+ @handle_db_errors
210
+ def select_one_dict_by_column_and_value(self, *,
211
+ table_name: str = None,
212
+ column_name: str,
213
+ column_value: Any,
214
+ fields: dict = None) -> Optional[dict]:
215
+ return self.select_one_dict_by_where(
216
+ table_name=table_name,
217
+ where={column_name: column_value},
218
+ fields=fields)
219
+
220
+ @handle_db_errors
221
+ def select_one_dict_by_where(self, *,
222
+ table_name: str = None,
223
+ where: dict = None,
224
+ fields: dict = None) -> Optional[dict]:
225
+ """``where`` is a MongoDB filter dict (the MongoDB equivalent of a SQL WHERE clause)."""
226
+ table_name = table_name or self.default_table_name
227
+ return self._collection(table_name).find_one(
228
+ self._live_filter(where or {}), fields)
229
+
230
+ @handle_db_errors
231
+ def select_multi_dict_by_column_and_value(self, *,
232
+ table_name: str = None,
233
+ column_name: str,
234
+ column_value: Any,
235
+ fields: dict = None,
236
+ limit: int = 0,
237
+ order_by: list = None) -> list[dict]:
238
+ return self.select_multi_dict_by_where(
239
+ table_name=table_name,
240
+ where={column_name: column_value},
241
+ fields=fields, limit=limit, order_by=order_by)
242
+
243
+ @handle_db_errors
244
+ def select_multi_dict_by_where(self, *,
245
+ table_name: str = None,
246
+ where: dict = None,
247
+ fields: dict = None,
248
+ limit: int = 0,
249
+ order_by: list = None) -> list[dict]:
250
+ """``where`` is a MongoDB filter dict (the MongoDB equivalent of a SQL WHERE clause)."""
251
+ table_name = table_name or self.default_table_name
252
+ cursor = self._collection(table_name).find(
253
+ self._live_filter(where or {}), fields)
254
+ if order_by:
255
+ cursor = cursor.sort(order_by)
256
+ if limit:
257
+ cursor = cursor.limit(limit)
258
+ return list(cursor)
259
+
260
+ @handle_db_errors
261
+ def select_one_value_by_column_and_value(self, *,
262
+ table_name: str = None,
263
+ column_name: str,
264
+ column_value: Any,
265
+ field: str) -> Any:
266
+ return self.select_one_value_by_where(
267
+ table_name=table_name,
268
+ where={column_name: column_value},
269
+ field=field)
270
+
271
+ @handle_db_errors
272
+ def select_one_value_by_where(self, *,
273
+ table_name: str = None,
274
+ where: dict = None,
275
+ field: str) -> Any:
276
+ doc = self.select_one_dict_by_where(
277
+ table_name=table_name,
278
+ where=where,
279
+ fields={field: 1})
280
+ return doc.get(field) if doc else None
281
+
282
+ @handle_db_errors
283
+ def select_multi_value_by_column_and_value(self, *,
284
+ table_name: str = None,
285
+ column_name: str,
286
+ column_value: Any,
287
+ field: str,
288
+ limit: int = 0) -> list:
289
+ return self.select_multi_value_by_where(
290
+ table_name=table_name,
291
+ where={column_name: column_value},
292
+ field=field, limit=limit)
293
+
294
+ @handle_db_errors
295
+ def select_multi_value_by_where(self, *,
296
+ table_name: str = None,
297
+ where: dict = None,
298
+ field: str,
299
+ limit: int = 0) -> list:
300
+ docs = self.select_multi_dict_by_where(
301
+ table_name=table_name,
302
+ where=where,
303
+ fields={field: 1},
304
+ limit=limit)
305
+ return [doc.get(field) for doc in docs if field in doc]
306
+
307
+ @handle_db_errors
308
+ def get_num_of_rows(self, *, table_name: str = None,
309
+ where: dict = None) -> int:
310
+ table_name = table_name or self.default_table_name
311
+ return self._collection(table_name).count_documents(
312
+ self._live_filter(where or {}))
313
+
314
+ @handle_db_errors
315
+ def foreach(self, *,
316
+ function: callable,
317
+ table_name: str = None,
318
+ where: dict = None,
319
+ batch_size: int = 100) -> None:
320
+ table_name = table_name or self.default_table_name
321
+ skip = 0
322
+ while True:
323
+ batch = list(self._collection(table_name).find(
324
+ self._live_filter(where or {}),
325
+ skip=skip,
326
+ limit=batch_size))
327
+ if not batch:
328
+ break
329
+ for doc in batch:
330
+ function(doc)
331
+ skip += batch_size
332
+
333
+ @handle_db_errors
334
+ def merge_entities(self, *,
335
+ entity_id1: ObjectId,
336
+ entity_id2: ObjectId,
337
+ table_name: str = None) -> None:
338
+ self.delete_by_id(document_id=entity_id1, table_name=table_name)
339
+
340
+ def close(self) -> None:
341
+ pass