test_mongo_data_node.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. # Copyright 2021-2025 Avaiga Private Limited
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
  4. # the License. You may obtain a copy of the License at
  5. #
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. #
  8. # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
  9. # an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
  10. # specific language governing permissions and limitations under the License.
  11. from dataclasses import dataclass
  12. from datetime import datetime
  13. from unittest.mock import patch
  14. import mongomock
  15. import pymongo
  16. import pytest
  17. from bson import ObjectId
  18. from bson.errors import InvalidDocument
  19. from taipy import Scope
  20. from taipy.common.config import Config
  21. from taipy.core import MongoDefaultDocument
  22. from taipy.core.common._mongo_connector import _connect_mongodb
  23. from taipy.core.data._data_manager_factory import _DataManagerFactory
  24. from taipy.core.data.data_node_id import DataNodeId
  25. from taipy.core.data.mongo import MongoCollectionDataNode
  26. from taipy.core.data.operator import JoinOperator, Operator
  27. from taipy.core.exceptions.exceptions import InvalidCustomDocument, MissingRequiredProperty
  28. @pytest.fixture(scope="function", autouse=True)
  29. def clear_mongo_connection_cache():
  30. _connect_mongodb.cache_clear()
  31. @dataclass
  32. class CustomObjectWithoutArgs:
  33. def __init__(self, foo=None, bar=None):
  34. self.foo = foo
  35. self.bar = bar
  36. class CustomObjectWithCustomEncoder:
  37. def __init__(self, _id=None, integer=None, text=None, time=None):
  38. self.id = _id
  39. self.integer = integer
  40. self.text = text
  41. self.time = time
  42. def encode(self):
  43. return {"_id": self.id, "integer": self.integer, "text": self.text, "time": self.time.isoformat()}
  44. class CustomObjectWithCustomEncoderDecoder(CustomObjectWithCustomEncoder):
  45. @classmethod
  46. def decode(cls, data):
  47. return cls(data["_id"], data["integer"], data["text"], datetime.fromisoformat(data["time"]))
  48. class TestMongoCollectionDataNode:
  49. __properties = [
  50. {
  51. "db_username": "",
  52. "db_password": "",
  53. "db_name": "taipy",
  54. "collection_name": "foo",
  55. "custom_document": MongoDefaultDocument,
  56. "db_extra_args": {
  57. "ssl": "true",
  58. "retrywrites": "false",
  59. "maxIdleTimeMS": "120000",
  60. },
  61. }
  62. ]
  63. @pytest.mark.parametrize("properties", __properties)
  64. def test_create(self, properties):
  65. mongo_dn_config = Config.configure_mongo_collection_data_node("foo_bar", **properties)
  66. mongo_dn = _DataManagerFactory._build_manager()._create_and_set(mongo_dn_config, None, None)
  67. assert isinstance(mongo_dn, MongoCollectionDataNode)
  68. assert mongo_dn.storage_type() == "mongo_collection"
  69. assert mongo_dn.config_id == "foo_bar"
  70. assert mongo_dn.scope == Scope.SCENARIO
  71. assert mongo_dn.id is not None
  72. assert mongo_dn.owner_id is None
  73. assert mongo_dn.job_ids == []
  74. assert mongo_dn.is_ready_for_reading
  75. assert mongo_dn.custom_document == MongoDefaultDocument
  76. @pytest.mark.parametrize("properties", __properties)
  77. def test_get_user_properties(self, properties):
  78. custom_properties = properties.copy()
  79. custom_properties["foo"] = "bar"
  80. mongo_dn = MongoCollectionDataNode(
  81. "foo_bar",
  82. Scope.SCENARIO,
  83. properties=custom_properties,
  84. )
  85. assert mongo_dn._get_user_properties() == {"foo": "bar"}
  86. @pytest.mark.parametrize(
  87. "properties",
  88. [
  89. {},
  90. {"db_username": "foo"},
  91. {"db_username": "foo", "db_password": "foo"},
  92. {"db_username": "foo", "db_password": "foo", "db_name": "foo"},
  93. ],
  94. )
  95. def test_create_with_missing_parameters(self, properties):
  96. with pytest.raises(MissingRequiredProperty):
  97. MongoCollectionDataNode("foo", Scope.SCENARIO, DataNodeId("dn_id"))
  98. with pytest.raises(MissingRequiredProperty):
  99. MongoCollectionDataNode("foo", Scope.SCENARIO, DataNodeId("dn_id"), properties=properties)
  100. @pytest.mark.parametrize("properties", __properties)
  101. def test_raise_error_invalid_custom_document(self, properties):
  102. custom_properties = properties.copy()
  103. custom_properties["custom_document"] = "foo"
  104. with pytest.raises(InvalidCustomDocument):
  105. MongoCollectionDataNode(
  106. "foo",
  107. Scope.SCENARIO,
  108. properties=custom_properties,
  109. )
  110. @mongomock.patch(servers=(("localhost", 27017),))
  111. @pytest.mark.parametrize("properties", __properties)
  112. def test_read(self, properties):
  113. mock_client = pymongo.MongoClient("localhost")
  114. mock_client[properties["db_name"]][properties["collection_name"]].insert_many(
  115. [
  116. {"foo": "baz", "bar": "qux"},
  117. {"foo": "quux", "bar": "quuz"},
  118. {"foo": "corge"},
  119. {"bar": "grault"},
  120. {"KWARGS_KEY": "KWARGS_VALUE"},
  121. {},
  122. ]
  123. )
  124. mongo_dn = MongoCollectionDataNode(
  125. "foo",
  126. Scope.SCENARIO,
  127. properties=properties,
  128. )
  129. data = mongo_dn.read()
  130. assert isinstance(data, list)
  131. assert isinstance(data[0], MongoDefaultDocument)
  132. assert isinstance(data[1], MongoDefaultDocument)
  133. assert isinstance(data[2], MongoDefaultDocument)
  134. assert isinstance(data[3], MongoDefaultDocument)
  135. assert isinstance(data[4], MongoDefaultDocument)
  136. assert isinstance(data[5], MongoDefaultDocument)
  137. assert isinstance(data[0]._id, ObjectId)
  138. assert data[0].foo == "baz"
  139. assert data[0].bar == "qux"
  140. assert isinstance(data[1]._id, ObjectId)
  141. assert data[1].foo == "quux"
  142. assert data[1].bar == "quuz"
  143. assert isinstance(data[2]._id, ObjectId)
  144. assert data[2].foo == "corge"
  145. assert isinstance(data[3]._id, ObjectId)
  146. assert data[3].bar == "grault"
  147. assert isinstance(data[4]._id, ObjectId)
  148. assert data[4].KWARGS_KEY == "KWARGS_VALUE"
  149. assert isinstance(data[5]._id, ObjectId)
  150. @mongomock.patch(servers=(("localhost", 27017),))
  151. @pytest.mark.parametrize("properties", __properties)
  152. def test_read_empty_as(self, properties):
  153. mongo_dn = MongoCollectionDataNode(
  154. "foo",
  155. Scope.SCENARIO,
  156. properties=properties,
  157. )
  158. data = mongo_dn.read()
  159. assert isinstance(data, list)
  160. assert len(data) == 0
  161. @mongomock.patch(servers=(("localhost", 27017),))
  162. @pytest.mark.parametrize("properties", __properties)
  163. @pytest.mark.parametrize(
  164. "data",
  165. [
  166. ([{"foo": 1, "a": 2}, {"foo": 3, "bar": 4}]),
  167. ({"a": 1, "bar": 2}),
  168. ],
  169. )
  170. def test_read_wrong_object_properties_name(self, properties, data):
  171. custom_properties = properties.copy()
  172. custom_properties["custom_document"] = CustomObjectWithoutArgs
  173. mongo_dn = MongoCollectionDataNode(
  174. "foo",
  175. Scope.SCENARIO,
  176. properties=custom_properties,
  177. )
  178. mongo_dn.write(data)
  179. with pytest.raises(TypeError):
  180. data = mongo_dn.read()
  181. @mongomock.patch(servers=(("localhost", 27017),))
  182. @pytest.mark.parametrize("properties", __properties)
  183. @pytest.mark.parametrize(
  184. "data",
  185. [
  186. ([{"foo": 11, "bar": 22}, {"foo": 33, "bar": 44}]),
  187. ({"foz": 1, "baz": 2}),
  188. ],
  189. )
  190. def test_append(self, properties, data):
  191. mongo_dn = MongoCollectionDataNode("foo", Scope.SCENARIO, properties=properties)
  192. mongo_dn.append(data)
  193. original_data = [{"foo": 1, "bar": 2}, {"foo": 3, "bar": 4}]
  194. mongo_dn.write(original_data)
  195. mongo_dn.append(data)
  196. assert len(mongo_dn.read()) == len(data if isinstance(data, list) else [data]) + len(original_data)
  197. @mongomock.patch(servers=(("localhost", 27017),))
  198. @pytest.mark.parametrize("properties", __properties)
  199. @pytest.mark.parametrize(
  200. "data,written_data",
  201. [
  202. ([{"foo": 1, "bar": 2}, {"foo": 3, "bar": 4}], [{"foo": 1, "bar": 2}, {"foo": 3, "bar": 4}]),
  203. ({"foo": 1, "bar": 2}, [{"foo": 1, "bar": 2}]),
  204. ],
  205. )
  206. def test_write(self, properties, data, written_data):
  207. mongo_dn = MongoCollectionDataNode("foo", Scope.SCENARIO, properties=properties)
  208. mongo_dn.write(data)
  209. read_objects = mongo_dn.read()
  210. for read_object, written_dict in zip(read_objects, written_data):
  211. assert isinstance(read_object._id, ObjectId)
  212. assert read_object.foo == written_dict["foo"]
  213. assert read_object.bar == written_dict["bar"]
  214. @mongomock.patch(servers=(("localhost", 27017),))
  215. @pytest.mark.parametrize("properties", __properties)
  216. @pytest.mark.parametrize(
  217. "data",
  218. [
  219. [],
  220. ],
  221. )
  222. def test_write_empty_list(self, properties, data):
  223. mongo_dn = MongoCollectionDataNode(
  224. "foo",
  225. Scope.SCENARIO,
  226. properties=properties,
  227. )
  228. mongo_dn.write(data)
  229. assert len(mongo_dn.read()) == 0
  230. @mongomock.patch(servers=(("localhost", 27017),))
  231. @pytest.mark.parametrize("properties", __properties)
  232. def test_write_non_serializable(self, properties):
  233. mongo_dn = MongoCollectionDataNode("foo", Scope.SCENARIO, properties=properties)
  234. data = {"a": 1, "b": mongo_dn}
  235. with pytest.raises(InvalidDocument):
  236. mongo_dn.write(data)
  237. @mongomock.patch(servers=(("localhost", 27017),))
  238. @pytest.mark.parametrize("properties", __properties)
  239. def test_write_custom_encoder(self, properties):
  240. custom_properties = properties.copy()
  241. custom_properties["custom_document"] = CustomObjectWithCustomEncoder
  242. mongo_dn = MongoCollectionDataNode("foo", Scope.SCENARIO, properties=custom_properties)
  243. data = [
  244. CustomObjectWithCustomEncoder("1", 1, "abc", datetime.now()),
  245. CustomObjectWithCustomEncoder("2", 2, "def", datetime.now()),
  246. ]
  247. mongo_dn.write(data)
  248. read_data = mongo_dn.read()
  249. assert isinstance(read_data[0], CustomObjectWithCustomEncoder)
  250. assert isinstance(read_data[1], CustomObjectWithCustomEncoder)
  251. assert read_data[0].id == "1"
  252. assert read_data[0].integer == 1
  253. assert read_data[0].text == "abc"
  254. assert isinstance(read_data[0].time, str)
  255. assert read_data[1].id == "2"
  256. assert read_data[1].integer == 2
  257. assert read_data[1].text == "def"
  258. assert isinstance(read_data[1].time, str)
  259. @mongomock.patch(servers=(("localhost", 27017),))
  260. @pytest.mark.parametrize("properties", __properties)
  261. def test_write_custom_encoder_decoder(self, properties):
  262. custom_properties = properties.copy()
  263. custom_properties["custom_document"] = CustomObjectWithCustomEncoderDecoder
  264. mongo_dn = MongoCollectionDataNode("foo", Scope.SCENARIO, properties=custom_properties)
  265. data = [
  266. CustomObjectWithCustomEncoderDecoder("1", 1, "abc", datetime.now()),
  267. CustomObjectWithCustomEncoderDecoder("2", 2, "def", datetime.now()),
  268. ]
  269. mongo_dn.write(data)
  270. read_data = mongo_dn.read()
  271. assert isinstance(read_data[0], CustomObjectWithCustomEncoderDecoder)
  272. assert isinstance(read_data[1], CustomObjectWithCustomEncoderDecoder)
  273. assert read_data[0].id == "1"
  274. assert read_data[0].integer == 1
  275. assert read_data[0].text == "abc"
  276. assert isinstance(read_data[0].time, datetime)
  277. assert read_data[1].id == "2"
  278. assert read_data[1].integer == 2
  279. assert read_data[1].text == "def"
  280. assert isinstance(read_data[1].time, datetime)
  281. @mongomock.patch(servers=(("localhost", 27017),))
  282. @pytest.mark.parametrize("properties", __properties)
  283. def test_filter(self, properties):
  284. mock_client = pymongo.MongoClient("localhost")
  285. mock_client[properties["db_name"]][properties["collection_name"]].insert_many(
  286. [
  287. {"foo": 1, "bar": 1},
  288. {"foo": 1, "bar": 2},
  289. {"foo": 1},
  290. {"foo": 2, "bar": 2},
  291. {"bar": 2},
  292. {"KWARGS_KEY": "KWARGS_VALUE"},
  293. ]
  294. )
  295. mongo_dn = MongoCollectionDataNode(
  296. "foo",
  297. Scope.SCENARIO,
  298. properties=properties,
  299. )
  300. assert len(mongo_dn.filter(("foo", 1, Operator.EQUAL))) == 3
  301. assert len(mongo_dn.filter(("foo", 1, Operator.NOT_EQUAL))) == 3
  302. assert len(mongo_dn.filter(("bar", 2, Operator.EQUAL))) == 3
  303. assert len(mongo_dn.filter([("bar", 1, Operator.EQUAL), ("bar", 2, Operator.EQUAL)], JoinOperator.OR)) == 4
  304. assert mongo_dn["foo"] == [1, 1, 1, 2, None, None]
  305. assert mongo_dn["bar"] == [1, 2, None, 2, 2, None]
  306. assert [m.__dict__ for m in mongo_dn[:3]] == [m.__dict__ for m in mongo_dn.read()[:3]]
  307. assert mongo_dn[["foo", "bar"]] == [
  308. {"foo": 1, "bar": 1},
  309. {"foo": 1, "bar": 2},
  310. {"foo": 1},
  311. {"foo": 2, "bar": 2},
  312. {"bar": 2},
  313. {},
  314. ]
  315. @mongomock.patch(servers=(("localhost", 27017),))
  316. @pytest.mark.parametrize("properties", __properties)
  317. def test_filter_does_not_read_all_entities(self, properties):
  318. mongo_dn = MongoCollectionDataNode("foo", Scope.SCENARIO, properties=properties)
  319. # MongoCollectionDataNode.filter() should not call the MongoCollectionDataNode._read() method
  320. with patch.object(MongoCollectionDataNode, "_read") as read_mock:
  321. mongo_dn.filter(("foo", 1, Operator.EQUAL))
  322. mongo_dn.filter(("bar", 2, Operator.NOT_EQUAL))
  323. mongo_dn.filter([("bar", 1, Operator.EQUAL), ("bar", 2, Operator.EQUAL)], JoinOperator.OR)
  324. assert read_mock["_read"].call_count == 0