From a77d21d7aa9db3fe31151a8491b86c7260f16d02 Mon Sep 17 00:00:00 2001 From: "mich.kolomanski" Date: Thu, 24 Mar 2022 09:17:34 +0100 Subject: [PATCH] [#144] Integrate ElasticSearch --- CHANGELOG.md | 1 + Pipfile | 2 +- Pipfile.lock | 335 +++++++++--------- recommender/api/schemas/recommendation.py | 9 + .../engines/base/base_inference_component.py | 41 ++- .../ncf/inference/ncf_inference_component.py | 11 +- .../rl/inference/rl_inference_component.py | 13 +- .../engines/rl/ml_components/sars_encoder.py | 2 +- .../rl/ml_components/sarses_generator.py | 4 + .../rl/ml_components/search_data_encoder.py | 57 --- .../rl/ml_components/service_encoder.py | 84 +++++ .../engines/rl/ml_components/state_encoder.py | 40 ++- .../synthetic_dataset/dataset.py | 139 +------- .../model_evaluation_step.py | 3 +- recommender/engines/rl/utils.py | 8 +- recommender/errors.py | 4 +- recommender/models/recommendation.py | 1 + recommender/models/state.py | 1 + recommender/services/deserializer.py | 1 + recommender/services/fts.py | 28 +- recommender/utils.py | 56 +-- tests/endpoints/test_recommendations.py | 1 + .../preprocessing/test_search_data_encoder.py | 117 ------ .../preprocessing/test_service_encoder.py | 132 +++++++ .../ml_components/test_normalizer.py | 1 - .../inference/test_ncf_inference_component.py | 35 +- .../inference/test_rl_inference_component.py | 48 ++- .../synthetic_dataset/test_dataset.py | 7 - tests/services/test_fts.py | 57 +-- 29 files changed, 601 insertions(+), 637 deletions(-) delete mode 100644 recommender/engines/rl/ml_components/search_data_encoder.py create mode 100644 recommender/engines/rl/ml_components/service_encoder.py delete mode 100644 tests/engine/preprocessing/test_search_data_encoder.py create mode 100644 tests/engine/preprocessing/test_service_encoder.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ffba179..3a8d9a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - YYYY-MM-DD ### Added +- Integrate elastic_search results from the Marketplace [@Michal-Kolomanski] - JMS Subscriber for user actions and flask cli for it [@michal-szostak] - search_data migration [@Michal-Kolomanski] - tqdm and logging in embedding pipeline [@Michal-Kolomanski] diff --git a/Pipfile b/Pipfile index 504aea0d..09c4d4ae 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,6 @@ markupsafe = "==2.0.1" flask-restx = "==0.3.0" jsonref = "*" json-ref-dict = "*" -pymongo = "*" mongoengine = "*" flask-mongoengine = "*" inflection = "*" @@ -38,6 +37,7 @@ sentry-sdk = {extras = ["flask"], version = "*"} ray = {version = "==1.7.0", extras = ["default"]} celery = {version = "==5.1.2", extras = ["redis"]} "stomp.py" = "==7.0.0" +pymongo = "==3.12" pydantic = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 504ccb0e..d08added 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "64129a64ea77e53f567a10da3bdc45b93a5e49b7b8793a08fb3a28e5c0e3f3b9" + "sha256": "33b131d8d785100c0c34d51fdf628948a5c1a41124158eec65db8fade280f225" }, "pipfile-spec": 6, "requires": { @@ -147,14 +147,6 @@ "markers": "python_full_version >= '3.6.2'", "version": "==3.5.0" }, - "appnope": { - "hashes": [ - "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24", - "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e" - ], - "markers": "platform_system == 'Darwin'", - "version": "==0.1.3" - }, "argon2-cffi": { "hashes": [ "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80", @@ -249,7 +241,7 @@ "sha256:08a1fe86d253b5c88c92cc3d810fd8048a16d15762e1e5b74d502256e5926aa1", "sha256:c6d6cc054bdc9c83b48b8083e236e5f00f238428666d2ce2e083eaa5fd568565" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==5.0.0" }, "blessings": { @@ -378,7 +370,7 @@ "sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667", "sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035" ], - "markers": "python_version < '4.0' and python_full_version >= '3.6.2'", + "markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'", "version": "==0.3.0" }, "click-plugins": { @@ -494,7 +486,7 @@ "sha256:e3513399177dd37af4c1332df52da5da1d0c387e5927dc4c0709e26ee7302e8f", "sha256:eb1946efac0c0c3d411cea0b5ac772fbde744109fd9520fb0c5a51979faf05ad" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==1.6.0" }, "decorator": { @@ -526,7 +518,7 @@ "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e", "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", + "markers": "python_version >= '3.6' and python_full_version < '4.0.0'", "version": "==2.2.1" }, "docopt": { @@ -586,7 +578,7 @@ "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85", "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==3.6.0" }, "flask": { @@ -623,11 +615,11 @@ }, "fonttools": { "hashes": [ - "sha256:14b94aab22439b9bd46c8f44b057b1d2f4b7ebe86556f9b15a1b25694bb12519", - "sha256:65d14ab1fe70cbd2f18fca538d98bd45d73e9b065defb843da71dc3c454deb45" + "sha256:50f0c76985321412123a1c32d1017ccad0e4ab1c71c8e64797cd7ffcbb7a379f", + "sha256:66dd757b8a9d7b07d61c22b8d973282990c45343572b88e5fc722b3a8272598e" ], - "markers": "python_full_version >= '3.7.0'", - "version": "==4.33.0" + "markers": "python_version >= '3.7'", + "version": "==4.33.1" }, "frozenlist": { "hashes": [ @@ -691,7 +683,7 @@ "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2", "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==1.3.0" }, "google-api-core": { @@ -903,7 +895,7 @@ "sha256:0e28273e290858393e86e152b104e5506a79c13d25b951ac6eca220051b4be60", "sha256:2b0987af43c0d4b62cecb13c592755f599f96f29aafe36c01731aaa96df30d39" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==6.13.0" }, "ipython": { @@ -911,7 +903,7 @@ "sha256:468abefc45c15419e3c8e8c0a6a5c115b2127bafa34d7c641b1d443658793909", "sha256:86df2cf291c6c70b5be6a7b608650420e89180c8ec74f376a34e2dc15c3400e7" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==7.32.0" }, "ipython-genutils": { @@ -989,7 +981,7 @@ "sha256:636694eb41b3535ed608fe04129f26542b59ed99808b4f688aa32dcf55317a83", "sha256:77281a1f71684953ee8b3d488371b162419767973789272434bbc3f29d9c8823" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==4.4.0" }, "jupyter-client": { @@ -997,7 +989,7 @@ "sha256:44045448eadc12493d819d965eb1dc9d10d1927698adbb9b14eb9a3a4a45ba53", "sha256:8fdbad344a8baa6a413d86d25bbf87ce21cb2b4aa5a8e0413863b9754eb8eb8a" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==7.2.2" }, "jupyter-core": { @@ -1005,7 +997,7 @@ "sha256:a6de44b16b7b31d7271130c71a6792c4040f077011961138afed5e5e73181aec", "sha256:e7f5212177af7ab34179690140f188aa9bf3d322d8155ed972cbded19f55b6f3" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==4.10.0" }, "jupyter-server": { @@ -1013,7 +1005,7 @@ "sha256:72dd1ff5373d2def94e80632ba4397e504cc9200c5b5f44b5b0af2e062a73353", "sha256:c756f87ad64b84e2aa522ef482445e1a93f7fe4a5fc78358f4636e53c9a0463a" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==1.16.0" }, "jupyterlab": { @@ -1029,7 +1021,7 @@ "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f", "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.2.2" }, "jupyterlab-server": { @@ -1086,7 +1078,7 @@ "sha256:fa4d97d7d2b2c082e67907c0b8d9f31b85aa5d3ba0d33096b7116f03f8061261", "sha256:ffbdb9a96c536f0405895b5e21ee39ec579cb0ed97bdbd169ae2b55f41d73219" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==1.4.2" }, "kombu": { @@ -1094,7 +1086,7 @@ "sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610", "sha256:8b213b24293d3417bcf0d2f5537b7f756079e3ea232a8386dcc89a59fd2361a4" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==5.2.4" }, "markdown": { @@ -1345,7 +1337,7 @@ "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937", "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==6.0.2" }, "nbclassic": { @@ -1353,7 +1345,7 @@ "sha256:36dbaa88ffaf5dc05d149deb97504b86ba648f4a80a60b8a58ac94acab2daeb5", "sha256:89184baa2d66b8ac3c8d3df57cbcf16f34148954d410a2fb3e897d7c18f2479d" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.3.7" }, "nbclient": { @@ -1361,7 +1353,7 @@ "sha256:40c52c9b5e3c31faecaee69f202b3f53e38d7c1c563de0fadde9d7eda0fdafe8", "sha256:47ac905af59379913c1f8f541098d2550153cf8dc58553cbe18c702b181518b0" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.5.13" }, "nbconvert": { @@ -1369,7 +1361,7 @@ "sha256:21163a8e2073c07109ca8f398836e45efdba2aacea68d6f75a8a545fef070d4e", "sha256:e01d219f55cc79f9701c834d605e8aa3acf35725345d3942e3983937f368ce14" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==6.4.5" }, "nbformat": { @@ -1377,7 +1369,7 @@ "sha256:38856d97de49e8292e2d5d8f595e9d26f02abfd87e075d450af4511870b40538", "sha256:fcc5ab8cb74e20b19570b5be809e2dba9b82836fd2761a89066ad43394ba29f5" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==5.3.0" }, "nest-asyncio": { @@ -1393,7 +1385,7 @@ "sha256:709b1856a564fe53054796c80e17a67262071c86bfbdfa6b96aaa346113c555a", "sha256:b4a6baf2eba21ce67a0ca11a793d1781b06b8078f34d06c710742e55f3eee505" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==6.4.11" }, "notebook-shim": { @@ -1401,7 +1393,7 @@ "sha256:02432d55a01139ac16e2100888aa2b56c614720cec73a27e71f40a5387e45324", "sha256:7897e47a36d92248925a2143e3596f19c60597708f7bef50d81fcd31d7263e85" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.1.0" }, "numpy": { @@ -1580,7 +1572,7 @@ "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==9.1.0" }, "pluggy": { @@ -1632,7 +1624,7 @@ "sha256:e250a42f15bf9d5b09fe1b293bdba2801cd520a9f5ea2d7fb7536d4441811d20", "sha256:ff8d8fa42675249bb456f5db06c00de6c2f4c27a065955917b28c4f15978b9c3" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==3.20.1" }, "psutil": { @@ -1796,92 +1788,107 @@ }, "pymongo": { "hashes": [ - "sha256:019a4c13ef1d9accd08de70247068671b116a0383adcd684f6365219f29f41cd", - "sha256:07f50a3b8a3afb086089abcd9ab562fb2a27b63fd7017ca13dfe7b663c8f3762", - "sha256:08a619c92769bd7346434dfc331a3aa8dc63bee80ed0be250bb0e878c69a6f3e", - "sha256:0a3474e6a0df0077a44573727341df6627042df5ca61ea5373c157bb6512ccc7", - "sha256:0b8a1c766de29173ddbd316dbd75a97b19a4cf9ac45a39ad4f53426e5df1483b", - "sha256:0f7e3872fb7b61ec574b7e04302ea03928b670df583f8691cb1df6e54cd42b19", - "sha256:17df40753085ccba38a0e150001f757910d66440d9b5deced30ed4cc8b45b6f3", - "sha256:298908478d07871dbe17e9ccd37a10a27ad3f37cc1faaf0cc4d205da3c3e8539", - "sha256:302ac0f4825501ab0900b8f1a2bb2dc7d28f69c7f15fbc799fb26f9b9ebb1ecb", - "sha256:303d1b3da2461586379d98b344b529598c8156857285ba5bd156dab1c875d1f6", - "sha256:306336dab4537b2343e52ec34017c3051c3aee5a961fff4915ab27f7e6d9b1e9", - "sha256:30d35a8855f328a85e5002f0908b24e500efdf8f5f78b73098995ce111baa2a9", - "sha256:3139c9ddee379c22a9109a0b3bf4cdb64597db2bbd3909f7a2825b47226977a4", - "sha256:32e785c37f6a0e844788c6085ea2c9c0c528348c22cebe91896705a92f2b1b26", - "sha256:33a5693e8d1fbb7743b7e867d43c1095652a0c6fedddab6cefe6020bee2ca393", - "sha256:35d02603c2318676fca5049cdc722bb2e7a378eaccf139ad767365e0eb3bcdbe", - "sha256:4516a5ce2beaebddc74d6e304ed520324dda99573c310ef4078284b026f81e93", - "sha256:49bb36986f11da2da190a2e777a411c0a28eeb8623850091ea8099b84e3860c7", - "sha256:4aa4800530782f7d38aeb169476a5bc692aacc394686f0ca3866e4bb85c9aa3f", - "sha256:4d1cdece06156542c18b691511a01fe78a694b9fa287ffd8e15680dbf2beeed5", - "sha256:4e4d2babb8737d650250d0fa940ffa1b88aa92b8eb399af093734950a1eeca45", - "sha256:4fd5c4f25d8d488ee5701c3ec786f52907dca653b47ce8709bcc2bfb0f5506ae", - "sha256:52c8b7bffd2140818ade2aa28c24cfe47935a7273a3bb976d1d8fb17e716536f", - "sha256:56b856a459762a3c052987e28ed2bd4b874f0be6671d2cc4f74c4891f47f997a", - "sha256:571a3e1ef4abeb4ac719ac381f5aada664627b4ee048d9995e93b4bcd0f70601", - "sha256:5cae9c935cdc53e4729920543b7d990615a115d85f32144773bc4b2b05144628", - "sha256:5d6ef3fa41f3e3be93483a77f81dea8c7ce5ed4411382a31af2b09b9ec5d9585", - "sha256:6396f0db060db9d8751167ea08f3a77a41a71cd39236fade4409394e57b377e8", - "sha256:69beffb048de19f7c18617b90e38cbddfac20077b1826c27c3fe2e3ef8ac5a43", - "sha256:7507439cd799295893b5602f438f8b6a0f483efb00720df1aa33a39102b41bcf", - "sha256:7aa40509dd9f75c256f0a7533d5e2ccef711dbbf0d91c13ac937d21d76d71656", - "sha256:7d69a3d980ecbf7238ab37b9027c87ad3b278bb3742a150fc33b5a8a9d990431", - "sha256:7dae2cf84a09329617b08731b95ad1fc98d50a9b40c2007e351438bd119a2f7a", - "sha256:7f36eacc70849d40ce86c85042ecfcbeab810691b1a3b08062ede32a2d6521ac", - "sha256:7f55a602d55e8f0feafde533c69dfd29bf0e54645ab0996b605613cda6894a85", - "sha256:8357aa727094798f1d831339ecfd8b3e388c01db6015a3cbd51790cb75e39994", - "sha256:84dc6bfeaeba98fe93fc837b12f9af4842694cdbde18083f150e80aec3de88f9", - "sha256:86b18420f00d5977bda477369ac85e04185ef94046a04ae0d85f5a807d1a8eb4", - "sha256:89f32d8450e15b0c11efdc81e2704d68c502c889d48415a50add9fa031144f75", - "sha256:8a1de8931cdad8cd12724e12a6167eef8cb478cc3ee5d2c9f4670c934f2975e1", - "sha256:8f106468062ac7ff03e3522a66cb7b36c662326d8eb7af1be0f30563740ff002", - "sha256:9a4ea87a0401c06b687db29e2ae836b2b58480ab118cb6eea8ac2ef45a4345f8", - "sha256:9ee1b019a4640bf39c0705ab65e934cfe6b89f1a8dc26f389fae3d7c62358d6f", - "sha256:a0d7c6d6fbca62508ea525abd869fca78ecf68cd3bcf6ae67ec478aa37cf39c0", - "sha256:a1417cb339a367a5dfd0e50193a1c0e87e31325547a0e7624ee4ff414c0b53b3", - "sha256:a35f1937b0560587d478fd2259a6d4f66cf511c9d28e90b52b183745eaa77d95", - "sha256:a4a35e83abfdac7095430e1c1476e0871e4b234e936f4a7a7631531b09a4f198", - "sha256:a7d1c8830a7bc10420ceb60a256d25ab5b032a6dad12a46af6ab2e470cee9124", - "sha256:a938d4d5b530f8ea988afb80817209eabc150c53b8c7af79d40080313a35e470", - "sha256:a9a2c377106fe01a57bad0f703653de286d56ee5285ed36c6953535cfa11f928", - "sha256:baf7546afd27be4f96f23307d7c295497fb512875167743b14a7457b95761294", - "sha256:bb21e2f35d6f09aa4a6df0c716f41e036cfcf05a98323b50294f93085ad775e9", - "sha256:bc62ba37bcb42e4146b853940b65a2de31c2962d2b6da9bc3ce28270d13b5c4e", - "sha256:be3ba736aabf856195199208ed37459408c932940cbccd2dc9f6ff2e800b0261", - "sha256:c03eb43d15c8af58159e7561076634d565530aaacaf48cf4e070c3501e88a372", - "sha256:c1349331fa743eed4042f9652200e60596f8beb957554acbcbb42aad4272c606", - "sha256:c3637cfce519560e2a2579d05eb81e912d109283b8ddc8de46f57ec20d273d92", - "sha256:c481cd1af2a77f58f495f7f87c2d715c6f1179d07c1ec927cca1f7977a2d99aa", - "sha256:c575f9499e5f540e034ff87bef894f031ae613a98b0d1d3afcc1f482527d5f1c", - "sha256:c604831daf2e7e5979ecd97a90cb8c4a7bae208ff45bc792e32eae09c3281afb", - "sha256:c759e1e0333664831d8d1d6b26cf59f23f3707758f696c71f506504b33130f81", - "sha256:c8a2743dd50629c0222f26c5f55975e45841d985b4b1c7a54b3f03b53de3427d", - "sha256:cbcac9263f500da94405cc9fc7e7a42a3ba6c2fe88b2cd7039737cba44c66889", - "sha256:cce1b7a680653e31ff2b252f19a39f1ded578a35a96c419ddb9632c62d2af7d8", - "sha256:cf96799b3e5e2e2f6dbca015f72b28e7ae415ce8147472f89a3704a035d6336d", - "sha256:d06ed18917dbc7a938c4231cbbec52a7e474be270b2ef9208abb4d5a34f5ceb9", - "sha256:d4ba5b4f1a0334dbe673f767f28775744e793fcb9ea57a1d72bc622c9f90e6b4", - "sha256:d7b8f25c9b0043cbaf77b8b895814e33e7a3c807a097377c07e1bd49946030d5", - "sha256:d86511ef8217822fb8716460aaa1ece31fe9e8a48900e541cb35acb7c35e9e2e", - "sha256:db8a9cbe965c7343feab2e2bf9a3771f303f8a7ca401dececb6ef28e06b3b18c", - "sha256:dbe92a8808cefb284e235b8f82933d7d2e24ff929fe5d53f1fd3ca55fced4b58", - "sha256:deb83cc9f639045e2febcc8d4306d4b83893af8d895f2ed70aa342a3430b534c", - "sha256:df9084e06efb3d59608a6a443faa9861828585579f0ae8e95f5a4dab70f1a00f", - "sha256:dfb89e92746e4a1e0d091cba73d6cc1e16b4094ebdbb14c2e96a80320feb1ad7", - "sha256:e13ddfe2ead9540e8773cae098f54c5206d6fcef64846a3e5042db47fc3a41ed", - "sha256:e4956384340eec7b526149ac126c8aa11d32441cb3ce77a690cb4821d0d0635c", - "sha256:e6eecd027b6ba5617ea6af3e12e20d578d8f4ad1bf51a9abe69c6fd4835ea532", - "sha256:eff9818b7671a55f1ce781398607e0d8c304cd430c0581fbe15b868a7a371c27", - "sha256:f0aea377b9dfc166c8fa05bb158c30ee3d53d73f0ed2fc05ba6c638d9563422f", - "sha256:f1fba193ab2f25849e24caa4570611aa2f80bc1c1ba791851523734b4ed69e43", - "sha256:f6db4f00d3baad615e99a865539391243d12b113fb628ebda1d7794ce02d5a10", - "sha256:f9405c02af86850e0a8a8ba777b7e7609e0d07bff46adc4f78892cc2d5456018", - "sha256:fb4445e3721720c5ca14c0650f35c263b3430e6e16df9d2504618df914b3fb99" + "sha256:02dc0b0f48ed3cd06c13b7e31b066bf91e00dac5f8147b0a0a45f9009bfab857", + "sha256:053b4ebf91c7395d1fcd2ce6a9edff0024575b7b2de6781554a4114448a8adc9", + "sha256:070a4ef689c9438a999ec3830e69b208ff0d12251846e064d947f97d819d1d05", + "sha256:072ba7cb65c8aa4d5c5659bf6722ee85781c9d7816dc00679b8b6f3dff1ddafc", + "sha256:0b6055e0ef451ff73c93d0348d122a0750dddf323b9361de5835dac2f6cf7fc1", + "sha256:11f9e0cfc84ade088a38df2708d0b958bb76360181df1b2e1e1a41beaa57952b", + "sha256:18290649759f9db660972442aa606f845c368db9b08c4c73770f6da14113569b", + "sha256:186104a94d39b8412f8e3de385acd990a628346a4402d4f3a288a82b8660bd22", + "sha256:1970cfe2aec1bf74b40cf30c130ad10cd968941694630386db33e1d044c22a2e", + "sha256:19d4bd0fc29aa405bb1781456c9cfff9fceabb68543741eb17234952dbc2bbb0", + "sha256:1bab889ae7640eba739f67fcbf8eff252dddc60d4495e6ddd3a87cd9a95fdb52", + "sha256:1bc6fe7279ff40c6818db002bf5284aa03ec181ea1b1ceaeee33c289d412afa7", + "sha256:208debdcf76ed39ebf24f38509f50dc1c100e31e8653817fedb8e1f867850a13", + "sha256:2399a85b54f68008e483b2871f4a458b4c980469c7fe921595ede073e4844f1e", + "sha256:246ec420e4c8744fceb4e259f906211b9c198e1f345e6158dcd7cbad3737e11e", + "sha256:24f8aeec4d6b894a6128844e50ff423dd02462ee83addf503c598ee3a80ddf3d", + "sha256:255a35bf29185f44b412e31a927d9dcedda7c2c380127ecc4fbf2f61b72fa978", + "sha256:2dbfbbded947a83a3dffc2bd1ec4750c17e40904692186e2c55a3ad314ca0222", + "sha256:2e92aa32300a0b5e4175caec7769f482b292769807024a86d674b3f19b8e3755", + "sha256:316c1b8723afa9870567cd6dff35d440b2afeda53aa13da6c5ab85f98ed6f5ca", + "sha256:333bfad77aa9cd11711febfb75eed0bb537a1d022e1c252714dad38993590240", + "sha256:39dafa2eaf577d1969f289dc9a44501859a1897eb45bd589e93ce843fc610800", + "sha256:3ce83f17f641a62a4dfb0ba1b8a3c1ced7c842f511b5450d90c030c7828e3693", + "sha256:46d5ec90276f71af3a29917b30f2aec2315a2759b5f8d45b3b63a07ca8a070a3", + "sha256:48d5bc80ab0af6b60c4163c5617f5cd23f2f880d7600940870ea5055816af024", + "sha256:4ba0def4abef058c0e5101e05e3d5266e6fffb9795bbf8be0fe912a7361a0209", + "sha256:5af390fa9faf56c93252dab09ea57cd020c9123aa921b63a0ed51832fdb492e7", + "sha256:5e574664f1468872cd40f74e4811e22b1aa4de9399d6bcfdf1ee6ea94c017fcf", + "sha256:625befa3bc9b40746a749115cc6a15bf20b9bd7597ca55d646205b479a2c99c7", + "sha256:6261bee7c5abadeac7497f8f1c43e521da78dd13b0a2439f526a7b0fc3788824", + "sha256:657ad80de8ec9ed656f28844efc801a0802961e8c6a85038d97ff6f555ef4919", + "sha256:6b89dc51206e4971c5568c797991eaaef5dc2a6118d67165858ad11752dba055", + "sha256:6e66780f14c2efaf989cd3ac613b03ee6a8e3a0ba7b96c0bb14adca71a427e55", + "sha256:6fb3f85870ae26896bb44e67db94045f2ebf00c5d41e6b66cdcbb5afd644fc18", + "sha256:701e08457183da70ed96b35a6b43e6ba1df0b47c837b063cde39a1fbe1aeda81", + "sha256:70761fd3c576b027eec882b43ee0a8e5b22ff9c20cdf4d0400e104bc29e53e34", + "sha256:73b400fdc22de84bae0dbf1a22613928a41612ec0a3d6ed47caf7ad4d3d0f2ff", + "sha256:7412a36798966624dc4c57d64aa43c2d1100b348abd98daaac8e99e57d87e1d7", + "sha256:78ecb8d42f50d393af912bfb1fb1dcc9aabe9967973efb49ee577e8f1cea494c", + "sha256:7c6a9948916a7bbcc6d3a9f6fb75db1acb5546078023bfb3db6efabcd5a67527", + "sha256:7c72d08acdf573455b2b9d2b75b8237654841d63a48bc2327dc102c6ee89b75a", + "sha256:7d98ce3c42921bb91566121b658e0d9d59a9082a9bd6f473190607ff25ab637f", + "sha256:845a8b83798b2fb11b09928413cb32692866bfbc28830a433d9fa4c8c3720dd0", + "sha256:94d38eba4d1b5eb3e6bfece0651b855a35c44f32fd91f512ab4ba41b8c0d3e66", + "sha256:9a13661681d17e43009bb3e85e837aa1ec5feeea1e3654682a01b8821940f8b3", + "sha256:a0e5dff6701fa615f165306e642709e1c1550d5b237c5a7a6ea299886828bd50", + "sha256:a2239556ff7241584ce57be1facf25081669bb457a9e5cbe68cce4aae6567aa1", + "sha256:a325600c83e61e3c9cebc0c2b1c8c4140fa887f789085075e8f44c8ff2547eb9", + "sha256:a3566acfbcde46911c52810374ecc0354fdb841284a3efef6ff7105bc007e9a8", + "sha256:a634a4730ce0b0934ed75e45beba730968e12b4dafbb22f69b3b2f616d9e644e", + "sha256:a6d055f01b83b1a4df8bb0c61983d3bdffa913764488910af3620e5c2450bf83", + "sha256:a752ecd1a26000a6d67be7c9a2e93801994a8b3f866ac95b672fbc00225ca91a", + "sha256:a9ba2a63777027b06b116e1ea8248e66fd1bedc2c644f93124b81a91ddbf6d88", + "sha256:aaa038eafb7186a4abbb311fcf20724be9363645882bbce540bef4797e812a7a", + "sha256:af586e85144023686fb0af09c8cdf672484ea182f352e7ceead3d832de381e1b", + "sha256:b0a0cf39f589e52d801fdef418305562bc030cdf8929217463c8433c65fd5c2f", + "sha256:b1c4874331ab960429caca81acb9d2932170d66d6d6f87e65dc4507a85aca152", + "sha256:b3b5b3cbc3fdf4fcfa292529df2a85b5d9c7053913a739d3069af1e12e12219f", + "sha256:b542d56ed1b8d5cf3bb36326f814bd2fbe8812dfd2582b80a15689ea433c0e35", + "sha256:b6ea08758b6673610b3c5bdf47189286cf9c58b1077558706a2f6f8744922527", + "sha256:b754240daafecd9d5fce426b0fbaaed03f4ebb130745c8a4ae9231fffb8d75e5", + "sha256:b772bab31cbd9cb911e41e1a611ebc9497f9a32a7348e2747c38210f75c00f41", + "sha256:b88d1742159bc93a078733f9789f563cef26f5e370eba810476a71aa98e5fbc2", + "sha256:b8bf42d3b32f586f4c9e37541769993783a534ad35531ce8a4379f6fa664fba9", + "sha256:bc9ac81e73573516070d24ce15da91281922811f385645df32bd3c8a45ab4684", + "sha256:c188db6cf9e14dbbb42f5254292be96f05374a35e7dfa087cc2140f0ff4f10f6", + "sha256:c55782a55f4a013a78ac5b6ee4b8731a192dea7ab09f1b6b3044c96d5128edd4", + "sha256:c5cab230e7cabdae9ff23c12271231283efefb944c1b79bed79a91beb65ba547", + "sha256:cbf8672edeb7b7128c4a939274801f0e32bbf5159987815e3d1eace625264a46", + "sha256:cc2894fe91f31a513860238ede69fe47fada21f9e7ddfe73f7f9fef93a971e41", + "sha256:cda9e628b1315beec8341e8c04aac9a0b910650b05e0751e42e399d5694aeacb", + "sha256:ceae3ab9e11a27aaab42878f1d203600dfd24f0e43678b47298219a0f10c0d30", + "sha256:ced944dcdd561476deef7cb7bfd4987c69fffbfeff6d02ca4d5d4fd592d559b7", + "sha256:d04ca462cb99077e6c059e97c072957caf2918e6e4191e3161c01c439e0193de", + "sha256:d1131562ddc2ea8a446f66c2648d7dabec2b3816fc818528eb978a75a6d23b2e", + "sha256:d1740776b70367277323fafb76bcf09753a5cc9824f5d705bac22a34ff3668ea", + "sha256:d6e11ffd43184d529d6752d6dcb62b994f903038a17ea2168ef1910c96324d26", + "sha256:d73e10772152605f6648ba4410318594f1043bbfe36d2fadee7c4b8912eff7c5", + "sha256:da8288bc4a7807c6715416deed1c57d94d5e03e93537889e002bf985be503f1a", + "sha256:db93608a246da44d728842b8fa9e45aa9782db76955f634a707739a8d53ff544", + "sha256:dcd3d0009fbb6e454d729f8b22d0063bd9171c31a55e0f0271119bd4f2700023", + "sha256:dd1f49f949a658c4e8f81ed73f9aad25fcc7d4f62f767f591e749e30038c4e1d", + "sha256:dd6ff2192f34bd622883c745a56f492b1c9ccd44e14953e8051c33024a2947d5", + "sha256:e018a4921657c2d3f89c720b7b90b9182e277178a04a7e9542cc79d7d787ca51", + "sha256:e2b7670c0c8c6b501464150dd49dd0d6be6cb7f049e064124911cec5514fa19e", + "sha256:e7a33322e08021c37e89cae8ff06327503e8a1719e97c69f32c31cbf6c30d72c", + "sha256:e8a82e35d52ad6f867e88096a1a2b9bdc7ec4d5e65c7b4976a248bf2d1a32a93", + "sha256:e9faf8d4712d5ea301d74abfcf6dafe4b7f4af7936e91f283b0ad7bf69ed3e3a", + "sha256:ec5ca7c0007ce268048bbe0ffc6846ed1616cf3d8628b136e81d5e64ff3f52a2", + "sha256:eee42a1cc06565f6b21caa1f504ec15e07de7ebfd520ab57f8cb3308bc118e22", + "sha256:f2acf9bbcd514e901f82c4ca6926bbd2ae61716728f110b4343eb0a69612d018", + "sha256:f55c1ddcc1f6050b07d468ce594f55dbf6107b459e16f735d26818d7be1e9538", + "sha256:f6977a520bd96e097c8a37a8cbb9faa1ea99d21bf84190195056e25f688af73d", + "sha256:f94c7d22fb36b184734dded7345a04ec5f95130421c775b8b0c65044ef073f34", + "sha256:fa8957e9a1b202cb45e6b839c241cd986c897be1e722b81d2f32e9c6aeee80b0", + "sha256:fd3854148005c808c485c754a184c71116372263709958b42aefbef2e5dd373a", + "sha256:fe5872ce6f9627deac8314bdffd3862624227c3de4c17ef0cc78bbf0402999eb", + "sha256:ffbae429ba9e42d0582d3ac63fdb410338892468a2107d8ff68228ec9a39a0ed" ], "index": "pypi", - "version": "==4.1.1" + "version": "==3.12" }, "pyparsing": { "hashes": [ @@ -1915,7 +1922,7 @@ "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5", "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.18.1" }, "pytest": { @@ -1923,7 +1930,7 @@ "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63", "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==7.1.1" }, "pytest-factoryboy": { @@ -2177,7 +2184,7 @@ "sha256:ff3fa8ea0e09e38677762afc6e14cad77b5e125b0ea70c9bba1992f02c93b028", "sha256:ff746a69ff2ef25f62b36338c615dd15954ddc3ab8e73530237dd73235e76d62" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==1.0.2" }, "scipy": { @@ -2212,7 +2219,7 @@ "sha256:f7eaea089345a35130bc9a39b89ec1ff69c208efa97b3f8b25ea5d4c41d88094", "sha256:f99d206db1f1ae735a8192ab93bd6028f3a42f6fa08467d37a14eb96c9dd34a3" ], - "markers": "python_version < '3.11' and python_full_version >= '3.7.0'", + "markers": "python_version < '3.11' and python_version >= '3.7'", "version": "==1.7.3" }, "send2trash": { @@ -2238,7 +2245,7 @@ "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==62.1.0" }, "six": { @@ -2307,7 +2314,7 @@ "sha256:874d4ea3183536c1782d13c7c91342ef0cf4e5ee1d53633029cbc972c8760bd8", "sha256:94d1cfab63525993f7d5c9b469a50a18d0cdf39435b59785715539dd41e36c0d" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.13.3" }, "testpath": { @@ -2331,7 +2338,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==2.0.1" }, "torch": { @@ -2444,7 +2451,7 @@ "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7", "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==5.1.1" }, "typing-extensions": { @@ -2452,7 +2459,7 @@ "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==4.2.0" }, "urllib3": { @@ -2460,7 +2467,7 @@ "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'", "version": "==1.26.9" }, "vine": { @@ -2490,7 +2497,7 @@ "sha256:50b21db0058f7a953d67cc0445be4b948d7fc196ecbeb8083d68d94628e4abf6", "sha256:722b171be00f2b90e1d4fb2f2b53146a536ca38db1da8ff49c972a4e1365d0ef" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==1.3.2" }, "werkzeug": { @@ -2587,7 +2594,7 @@ "sha256:6b351bbb12dd58af57ffef05bc78425d08d1914e0fd68ee14143b7ade023c5bc", "sha256:837f2f0e0ca79481b92884962b914eba4e72b7a2daaf1f939c890ed0124b834b" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==3.0.1" }, "yarl": { @@ -2673,7 +2680,7 @@ "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==3.8.0" } }, @@ -2693,14 +2700,6 @@ ], "version": "==1.4.4" }, - "appnope": { - "hashes": [ - "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24", - "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e" - ], - "markers": "platform_system == 'Darwin'", - "version": "==0.1.3" - }, "argon2-cffi": { "hashes": [ "sha256:8c976986f2c5c0e5000919e6de187906cfd81fb1c72bf9d88c01177e77da7f80", @@ -2787,7 +2786,7 @@ "sha256:08a1fe86d253b5c88c92cc3d810fd8048a16d15762e1e5b74d502256e5926aa1", "sha256:c6d6cc054bdc9c83b48b8083e236e5f00f238428666d2ce2e083eaa5fd568565" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==5.0.0" }, "certifi": { @@ -2897,7 +2896,7 @@ "sha256:e3513399177dd37af4c1332df52da5da1d0c387e5927dc4c0709e26ee7302e8f", "sha256:eb1946efac0c0c3d411cea0b5ac772fbde744109fd9520fb0c5a51979faf05ad" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==1.6.0" }, "decorator": { @@ -2975,7 +2974,7 @@ "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85", "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==3.6.0" }, "graphviz": { @@ -3038,7 +3037,7 @@ "sha256:0e28273e290858393e86e152b104e5506a79c13d25b951ac6eca220051b4be60", "sha256:2b0987af43c0d4b62cecb13c592755f599f96f29aafe36c01731aaa96df30d39" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==6.13.0" }, "ipytest": { @@ -3054,7 +3053,7 @@ "sha256:468abefc45c15419e3c8e8c0a6a5c115b2127bafa34d7c641b1d443658793909", "sha256:86df2cf291c6c70b5be6a7b608650420e89180c8ec74f376a34e2dc15c3400e7" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==7.32.0" }, "ipython-genutils": { @@ -3107,7 +3106,7 @@ "sha256:636694eb41b3535ed608fe04129f26542b59ed99808b4f688aa32dcf55317a83", "sha256:77281a1f71684953ee8b3d488371b162419767973789272434bbc3f29d9c8823" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==4.4.0" }, "jupyter": { @@ -3124,7 +3123,7 @@ "sha256:44045448eadc12493d819d965eb1dc9d10d1927698adbb9b14eb9a3a4a45ba53", "sha256:8fdbad344a8baa6a413d86d25bbf87ce21cb2b4aa5a8e0413863b9754eb8eb8a" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==7.2.2" }, "jupyter-console": { @@ -3140,7 +3139,7 @@ "sha256:a6de44b16b7b31d7271130c71a6792c4040f077011961138afed5e5e73181aec", "sha256:e7f5212177af7ab34179690140f188aa9bf3d322d8155ed972cbded19f55b6f3" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==4.10.0" }, "jupyter-server": { @@ -3148,7 +3147,7 @@ "sha256:72dd1ff5373d2def94e80632ba4397e504cc9200c5b5f44b5b0af2e062a73353", "sha256:c756f87ad64b84e2aa522ef482445e1a93f7fe4a5fc78358f4636e53c9a0463a" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==1.16.0" }, "jupyterlab": { @@ -3164,7 +3163,7 @@ "sha256:2405800db07c9f770863bcf8049a529c3dd4d3e28536638bd7c1c01d2748309f", "sha256:7405d7fde60819d905a9fa8ce89e4cd830e318cdad22a0030f7a901da705585d" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.2.2" }, "jupyterlab-server": { @@ -3344,7 +3343,7 @@ "sha256:36dbaa88ffaf5dc05d149deb97504b86ba648f4a80a60b8a58ac94acab2daeb5", "sha256:89184baa2d66b8ac3c8d3df57cbcf16f34148954d410a2fb3e897d7c18f2479d" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.3.7" }, "nbclient": { @@ -3352,7 +3351,7 @@ "sha256:40c52c9b5e3c31faecaee69f202b3f53e38d7c1c563de0fadde9d7eda0fdafe8", "sha256:47ac905af59379913c1f8f541098d2550153cf8dc58553cbe18c702b181518b0" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.5.13" }, "nbconvert": { @@ -3360,7 +3359,7 @@ "sha256:21163a8e2073c07109ca8f398836e45efdba2aacea68d6f75a8a545fef070d4e", "sha256:e01d219f55cc79f9701c834d605e8aa3acf35725345d3942e3983937f368ce14" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==6.4.5" }, "nbformat": { @@ -3368,7 +3367,7 @@ "sha256:38856d97de49e8292e2d5d8f595e9d26f02abfd87e075d450af4511870b40538", "sha256:fcc5ab8cb74e20b19570b5be809e2dba9b82836fd2761a89066ad43394ba29f5" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==5.3.0" }, "nest-asyncio": { @@ -3391,7 +3390,7 @@ "sha256:709b1856a564fe53054796c80e17a67262071c86bfbdfa6b96aaa346113c555a", "sha256:b4a6baf2eba21ce67a0ca11a793d1781b06b8078f34d06c710742e55f3eee505" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==6.4.11" }, "notebook-shim": { @@ -3399,7 +3398,7 @@ "sha256:02432d55a01139ac16e2100888aa2b56c614720cec73a27e71f40a5387e45324", "sha256:7897e47a36d92248925a2143e3596f19c60597708f7bef50d81fcd31d7263e85" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.1.0" }, "packaging": { @@ -3597,7 +3596,7 @@ "sha256:f87cc2863ef33c709e237d4b5f4502a62a00fab450c9e020892e8e2ede5847f5", "sha256:fd8da6d0124efa2f67d86fa70c851022f87c98e205f0594e1fae044e7119a5a6" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.18.1" }, "pytest": { @@ -3605,7 +3604,7 @@ "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63", "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==7.1.1" }, "pytest-factoryboy": { @@ -3861,7 +3860,7 @@ "sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8", "sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==62.1.0" }, "six": { @@ -3893,7 +3892,7 @@ "sha256:874d4ea3183536c1782d13c7c91342ef0cf4e5ee1d53633029cbc972c8760bd8", "sha256:94d1cfab63525993f7d5c9b469a50a18d0cdf39435b59785715539dd41e36c0d" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==0.13.3" }, "tinycss2": { @@ -3917,7 +3916,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==2.0.1" }, "tornado": { @@ -3972,7 +3971,7 @@ "sha256:059f456c5a7c1c82b98c2e8c799f39c9b8128f6d0d46941ee118daace9eb70c7", "sha256:2d313cc50a42cd6c277e7d7dc8d4d7fedd06a2c215f78766ae7b1a66277e0033" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==5.1.1" }, "typed-ast": { @@ -4010,7 +4009,7 @@ "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==4.2.0" }, "urllib3": { @@ -4018,7 +4017,7 @@ "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_full_version < '4.0.0'", "version": "==1.26.9" }, "virtualenv": { @@ -4048,7 +4047,7 @@ "sha256:50b21db0058f7a953d67cc0445be4b948d7fc196ecbeb8083d68d94628e4abf6", "sha256:722b171be00f2b90e1d4fb2f2b53146a536ca38db1da8ff49c972a4e1365d0ef" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==1.3.2" }, "widgetsnbextension": { @@ -4133,7 +4132,7 @@ "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" ], - "markers": "python_full_version >= '3.7.0'", + "markers": "python_version >= '3.7'", "version": "==3.8.0" } } diff --git a/recommender/api/schemas/recommendation.py b/recommender/api/schemas/recommendation.py index 66568ea7..d94c8515 100644 --- a/recommender/api/schemas/recommendation.py +++ b/recommender/api/schemas/recommendation.py @@ -92,6 +92,15 @@ description="Version of the recommendation engine", example="RL", ), + "elastic_services": fields.List( + fields.Integer( + title="Service ID", description="The unique identifier of the service" + ), + required=True, + title="ElasticSearch services list", + description="List of services IDs from ElasticSearch", + example=[1, 2, 3, 4], + ), "search_data": fields.Nested(search_data, required=True), }, ) diff --git a/recommender/engines/base/base_inference_component.py b/recommender/engines/base/base_inference_component.py index 64499bb6..016e755e 100644 --- a/recommender/engines/base/base_inference_component.py +++ b/recommender/engines/base/base_inference_component.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod import random -from typing import Dict, Any, List +from typing import Dict, Any, List, Tuple from recommender.engines.panel_id_to_services_number_mapping import PANEL_ID_TO_K from recommender.errors import ( @@ -46,11 +46,13 @@ def __call__(self, context: Dict[str, Any]) -> List[int]: """ user = self._get_user(context) - search_data = self._get_search_data(context) + elastic_services, search_data = self._get_elastic_services_and_search_data( + context + ) if user is not None: - return self._for_logged_user(user, search_data) - return self._for_anonymous_user(search_data) + return self._for_logged_user(user, elastic_services, search_data) + return self._for_anonymous_user(elastic_services) @abstractmethod def _load_models(self) -> None: @@ -76,26 +78,22 @@ def _get_user(self, context: Dict[str, Any]) -> User: return user - def _get_search_data(self, context: Dict[str, Any]) -> SearchData: - search_data = context.get("search_data") - search_data.pop( - "rating", None - ) # We don't and we shouldn't take rating into consideration - - # To prevent q being None (for SearchPhraseEncoder it must be a string) - search_data["q"] = search_data.get("q", "") - - search_data = SearchData(**search_data) - - return search_data + @staticmethod + def _get_elastic_services_and_search_data( + context: Dict[str, Any] + ) -> [Tuple[int], SearchData]: + return tuple(context.get("elastic_services")), context.get("search_data") @abstractmethod - def _for_logged_user(self, user: User, search_data: SearchData) -> List[int]: + def _for_logged_user( + self, user: User, elastic_services: Tuple[int], search_data: SearchData + ) -> List[int]: """ Generate recommendation for logged user Args: user: user for whom recommendation will be generated. + elastic_services: item space from the Marketplace. search_data: search phrase and filters information for narrowing down an item space. @@ -103,19 +101,20 @@ def _for_logged_user(self, user: User, search_data: SearchData) -> List[int]: recommended_services_ids: List of recommended services ids. """ - def _for_anonymous_user(self, search_data: SearchData) -> List[int]: + def _for_anonymous_user(self, elastic_services: Tuple[int]) -> List[int]: """ Generate recommendation for anonymous user Args: - search_data: search phrase and filters information for narrowing - down an item space. + elastic_services: item space from the Marketplace. Returns: recommended_services_ids: List of recommended services ids. """ - candidate_services = list(retrieve_services_for_recommendation(search_data)) + candidate_services = list( + retrieve_services_for_recommendation(elastic_services) + ) if len(candidate_services) < self.K: raise InsufficientRecommendationSpaceError() recommended_services = random.sample(list(candidate_services), self.K) diff --git a/recommender/engines/ncf/inference/ncf_inference_component.py b/recommender/engines/ncf/inference/ncf_inference_component.py index 29731ffc..4ae8116f 100644 --- a/recommender/engines/ncf/inference/ncf_inference_component.py +++ b/recommender/engines/ncf/inference/ncf_inference_component.py @@ -3,7 +3,7 @@ """Neural Collaborative Filtering Inference Component""" -from typing import List +from typing import List, Tuple import torch @@ -73,11 +73,14 @@ def user_and_services_to_tensors(user, services): return users_ids, users_tensor, services_ids, services_tensor - def _for_logged_user(self, user: User, search_data: SearchData) -> List[int]: + def _for_logged_user( + self, user: User, elastic_services: Tuple[int], search_data: SearchData + ) -> List[int]: """Generate recommendation for logged user. Args: user: user for whom recommendation will be generated. + elastic_services: item space from the Marketplace. search_data: search phrase and filters information for narrowing down an item space. @@ -86,7 +89,9 @@ def _for_logged_user(self, user: User, search_data: SearchData) -> List[int]: """ candidate_services = list( - retrieve_services_for_recommendation(search_data, user.accessed_services) + retrieve_services_for_recommendation( + elastic_services, user.accessed_services + ) ) if len(candidate_services) < self.K: raise InsufficientRecommendationSpaceError() diff --git a/recommender/engines/rl/inference/rl_inference_component.py b/recommender/engines/rl/inference/rl_inference_component.py index a9ed163d..919bd36a 100644 --- a/recommender/engines/rl/inference/rl_inference_component.py +++ b/recommender/engines/rl/inference/rl_inference_component.py @@ -37,15 +37,18 @@ def _load_models(self) -> None: ) self.service_selector = ServiceSelector(self.service_embedder) - def _for_logged_user(self, user: User, search_data: SearchData) -> Tuple[int]: - state = create_state(user, search_data) + def _for_logged_user( + self, user: User, elastic_services: Tuple[int], search_data: SearchData + ) -> Tuple[int]: + state = create_state(user, elastic_services, search_data) state_tensors = self.state_encoder([state]) weights_tensor = self._get_weights(state_tensors) - search_data_mask = self._get_search_data_mask(state_tensors) - service_ids = self.service_selector(weights_tensor, search_data_mask) + services_mask = self._get_service_mask(state_tensors) + service_ids = self.service_selector(weights_tensor, services_mask) return service_ids - def _get_search_data_mask(self, state_tensors): + @staticmethod + def _get_service_mask(state_tensors): return state_tensors[2][0] def _get_weights(self, state_tensors): diff --git a/recommender/engines/rl/ml_components/sars_encoder.py b/recommender/engines/rl/ml_components/sars_encoder.py index fc770709..8542f624 100644 --- a/recommender/engines/rl/ml_components/sars_encoder.py +++ b/recommender/engines/rl/ml_components/sars_encoder.py @@ -62,8 +62,8 @@ def __call__( states = [SARS.state for SARS in SARSes] next_states = [SARS.next_state for SARS in SARSes] all_states = states + next_states - all_states_tensors = self.state_encoder(all_states) + all_states_tensors = self.state_encoder(all_states, verbose=True) states_tensors = tuple(t[: len(states)] for t in all_states_tensors) next_states_tensors = tuple(t[len(states) :] for t in all_states_tensors) diff --git a/recommender/engines/rl/ml_components/sarses_generator.py b/recommender/engines/rl/ml_components/sarses_generator.py index 295a7fec..d06a4474 100644 --- a/recommender/engines/rl/ml_components/sarses_generator.py +++ b/recommender/engines/rl/ml_components/sarses_generator.py @@ -17,6 +17,7 @@ import itertools import time from typing import List, Union +from copy import deepcopy import billiard import torch from mongoengine import DoesNotExist @@ -252,6 +253,7 @@ def skipable_func(*args, **kwargs): def generate_sars(recommendation, root_uas): """Generate sars for given recommendation and root user actions""" + rec = deepcopy(recommendation) # elastic_services memory leak user = recommendation.user or _get_empty_user() # Create reward @@ -266,6 +268,7 @@ def generate_sars(recommendation, root_uas): state = State( user=user, services_history=services_history_before, + elastic_services=rec.elastic_services, search_data=recommendation.search_data, ).save() @@ -283,6 +286,7 @@ def generate_sars(recommendation, root_uas): next_state = State( user=user, services_history=services_history_after, + elastic_services=rec.elastic_services, search_data=next_recommendation.search_data, ).save() diff --git a/recommender/engines/rl/ml_components/search_data_encoder.py b/recommender/engines/rl/ml_components/search_data_encoder.py deleted file mode 100644 index da935631..00000000 --- a/recommender/engines/rl/ml_components/search_data_encoder.py +++ /dev/null @@ -1,57 +0,0 @@ -# pylint: disable=missing-module-docstring, no-member, too-few-public-methods, invalid-name -from typing import List - -import torch - -from recommender.engines.rl.utils import ( - create_index_id_map, - get_service_indices, -) -from recommender.models import SearchData, User, Service -from recommender.services.fts import retrieve_forbidden_services, filter_services -from recommender.engines.rl.ml_components.services_history_generator import ( - get_ordered_services, -) -from recommender.errors import SizeOfUsersAndSearchDataError - - -class SearchDataEncoder: - """Encodes search data and user's ordered services into a binary mask""" - - def __init__(self): - all_services = list(Service.objects.order_by("id")) - self.I = len(all_services) - self.index_id_map = create_index_id_map(all_services) - - forbidden_services = retrieve_forbidden_services() - self.forbidden_service_indices = get_service_indices( - self.index_id_map, forbidden_services.distinct("id") - ) - - def __call__( - self, users: List[User], search_data: List[SearchData] - ) -> torch.Tensor: - if not len(users) == len(search_data): - raise SizeOfUsersAndSearchDataError() - - batch_size = len(users) - - mask = torch.zeros(batch_size, self.I) - - for i in range(batch_size): - filtered_services = filter_services(search_data[i]) - ordered_services = get_ordered_services(users[i]) - - filtered_service_indices = get_service_indices( - self.index_id_map, filtered_services.distinct("id") - ) - - ordered_service_indices = get_service_indices( - self.index_id_map, [s.id for s in ordered_services] - ) - - mask[i, filtered_service_indices] = 1 - mask[i, ordered_service_indices] = 0 - mask[i, self.forbidden_service_indices] = 0 - - return mask diff --git a/recommender/engines/rl/ml_components/service_encoder.py b/recommender/engines/rl/ml_components/service_encoder.py new file mode 100644 index 00000000..a51f6d1b --- /dev/null +++ b/recommender/engines/rl/ml_components/service_encoder.py @@ -0,0 +1,84 @@ +# pylint: disable=missing-module-docstring, no-member, too-few-public-methods, invalid-name, line-too-long +from typing import List +from copy import deepcopy +import torch +from tqdm import tqdm +from recommender.engines.rl.utils import ( + create_index_id_map, + get_service_indices, +) +from recommender.models import User, Service, State +from recommender.services.fts import retrieve_forbidden_services +from recommender.engines.rl.ml_components.services_history_generator import ( + get_ordered_services, +) +from recommender.errors import SizeOfUsersAndElasticServicesError +from logger_config import get_logger + +logger = get_logger(__name__) + + +class ServiceEncoder: + """Encodes search data and user's ordered services into a binary mask""" + + def __init__(self): + all_services = list(Service.objects.order_by("id")) + self.I = len(all_services) + self.index_id_map = create_index_id_map(all_services) + + forbidden_services = retrieve_forbidden_services() + self.forbidden_service_indices = get_service_indices( + self.index_id_map, forbidden_services.distinct("id") + ) + + def __call__( + self, users: List[User], states: List[State], verbose=False + ) -> torch.Tensor: + batch_size = len(users) + states_len = len(states) + + if batch_size != states_len: + # Each state has elastic_services + raise SizeOfUsersAndElasticServicesError + + mask = torch.zeros(batch_size, self.I) + + if verbose: + logger.info("Creating mask...") + + for i in tqdm(range(batch_size), desc="States", disable=not verbose): + state = states.pop(0) + + if states_len == 1: + # Model evaluation step - searchdata there is not saved, so it cannot be accessed + # This avoids mongodb ValidationError + state.search_data = None + + # Just reading the tuple of references (which is the elastic_services) + # causes the IDs to be replaced with entire service objects in all referenced tuple. + # To avoid memory leak: + # - state has 3 references - direct manipulation on state object will result in memory leak + # to avoid that, there is deepcopy made + state = deepcopy(state) + elastic_services = state.elastic_services + services_context = self.get_id_from_services(elastic_services) + + ordered_services = get_ordered_services(users[i]) + + services_context_indices = get_service_indices( + self.index_id_map, services_context + ) + ordered_service_indices = get_service_indices( + self.index_id_map, [s.id for s in ordered_services] + ) + + mask[i, services_context_indices] = 1 + mask[i, ordered_service_indices] = 0 + mask[i, self.forbidden_service_indices] = 0 + + return mask + + @staticmethod + def get_id_from_services(services: List[Service]) -> List[int]: + """Get services IDs""" + return [service.id for service in services] diff --git a/recommender/engines/rl/ml_components/state_encoder.py b/recommender/engines/rl/ml_components/state_encoder.py index 47038134..dbbfd45d 100644 --- a/recommender/engines/rl/ml_components/state_encoder.py +++ b/recommender/engines/rl/ml_components/state_encoder.py @@ -4,16 +4,16 @@ """Implementation of the State Encoder""" from typing import Tuple, List - import torch from torch.nn.utils.rnn import pad_sequence - -from recommender.engines.rl.ml_components.search_data_encoder import ( - SearchDataEncoder, +from recommender.engines.rl.ml_components.service_encoder import ( + ServiceEncoder, ) from recommender.engines.autoencoders.ml_components.embedder import Embedder -from recommender.models import Service -from recommender.models import State +from recommender.models import Service, State +from logger_config import get_logger + +logger = get_logger(__name__) class StateEncoder: @@ -30,9 +30,11 @@ def __init__( self.service_embedder = service_embedder self.use_cached_embeddings = use_cached_embeddings self.save_cached_embeddings = save_cached_embeddings - self.search_data_encoder = SearchDataEncoder() + self.service_encoder = ServiceEncoder() - def __call__(self, states: List[State]) -> Tuple[(torch.Tensor,) * 3]: + def __call__( + self, states: List[State], verbose: bool = False + ) -> Tuple[(torch.Tensor,) * 3]: """ Encode given states to the tuple of tensors using appropriate encoders and embedders as follows: @@ -43,8 +45,8 @@ def __call__(self, states: List[State]) -> Tuple[(torch.Tensor,) * 3]: -> services_one_hot_tensors --(service_embedder)--> services_dense_tensors --(concat)--> services_histories_tensor - - state.search_data - --(search_data_encoder)--> search_data_tensor (mask) + - state.elastic_services + --(service encoder)--> service_tensor (mask) It makes batches from parts of states whenever it is possible to @@ -52,13 +54,14 @@ def __call__(self, states: List[State]) -> Tuple[(torch.Tensor,) * 3]: Args: states: List of state objects. Each of them contains information - about user, user's services history and search data. + about user, user's services history and elastic services. + verbose: be verbose? Returns: Tuple of following tensors (in this order): - user of shape [B, UE] - service_histories_batch of shape [B, N, SE] - - search_data_mask of shape [B, I] + - service_mask of shape [B, I] where: - B is the batch_size and it is equal to len(states) @@ -69,10 +72,14 @@ def __call__(self, states: List[State]) -> Tuple[(torch.Tensor,) * 3]: - K is the first mask dim - I is equal to itemspace size """ - + if verbose: + logger.info("Getting all users from states") users = [state.user for state in states] + + if verbose: + logger.info("Getting all services_histories from states") services_histories = [state.services_history for state in states] - search_data_list = [state.search_data for state in states] + with torch.no_grad(): users_batch, _ = self.user_embedder( users, @@ -82,9 +89,10 @@ def __call__(self, states: List[State]) -> Tuple[(torch.Tensor,) * 3]: service_histories_batch = self._create_service_histories_batch( services_histories ) - search_data_masks_batch = self.search_data_encoder(users, search_data_list) - encoded_states = users_batch, service_histories_batch, search_data_masks_batch + service_masks_batch = self.service_encoder(users, states, verbose=verbose) + + encoded_states = users_batch, service_histories_batch, service_masks_batch return encoded_states diff --git a/recommender/engines/rl/ml_components/synthetic_dataset/dataset.py b/recommender/engines/rl/ml_components/synthetic_dataset/dataset.py index 2c43f863..b383cb03 100644 --- a/recommender/engines/rl/ml_components/synthetic_dataset/dataset.py +++ b/recommender/engines/rl/ml_components/synthetic_dataset/dataset.py @@ -15,7 +15,7 @@ NormalizationMode, ) from recommender.models import Service, State, SearchData, Sars, User -from recommender.services.fts import retrieve_services_for_recommendation +from recommender.services.fts import retrieve_services_for_synthetic_sarses from recommender.engines.rl.ml_components.synthetic_dataset.rewards import ( synthesize_reward, RewardGeneration, @@ -23,9 +23,6 @@ from recommender.engines.rl.ml_components.synthetic_dataset.service_engagement import ( approx_service_engagement, ) -from recommender.engines.rl.ml_components.synthetic_dataset.users import ( - synthesize_users, -) def _normalize_embedded_services(embedded_services: torch.Tensor) -> torch.Tensor: @@ -56,137 +53,23 @@ def _get_ordered_services(rewards_dict: Dict[Service, List[str]]) -> List[Servic def _get_relevant_search_data(user, ordered_services, k): for c in user.categories: search_data = SearchData(categories=[c]) - if len(retrieve_services_for_recommendation(search_data, ordered_services)) > k: + if ( + len(retrieve_services_for_synthetic_sarses(search_data, ordered_services)) + > k + ): return search_data.save() for sd in user.scientific_domains: search_data = SearchData(scientific_domains=[sd]) - if len(retrieve_services_for_recommendation(search_data, ordered_services)) > k: + if ( + len(retrieve_services_for_synthetic_sarses(search_data, ordered_services)) + > k + ): return search_data.save() return SearchData().save() -def generate_dataset( - user_samples: int, - service_embedder: Embedder, - K: int = 3, - interactions_range: Tuple[int, int] = (3, 10), - reward_generation_mode: RewardGeneration = RewardGeneration.COMPLEX, - simple_reward_threshold: int = 0.5, - cluster_distributions: Tuple[(float,) * 7] = ( - 0.12, - 0.2, - 0.2, - 0.12, - 0.12, - 0.12, - 0.12, - ), # experimentally chosen -) -> Tuple[List[Sars], List[User]]: - """ - Generates artificial SARS dataset that will be used for training - and benchmarking the RL agent. - It generates users belonging to a predefined category and scientific domain clusters - and for each one generates specified number of SARSes by: - - choosing an action randomly (LIRD is a off-policy RL algorithm) - - approximating heuristically the engagement of a user in a given service - - generates rewards based on this approximation for each service - (simulates the page transitions in the Marketplace portal) - - Args: - user_samples: number of users to generate - K: number of recommended services in a single recommendation - interactions_range: range of recommendations - (as well as SARSes generated for each user) - reward_generation_mode: either "complex" or "simple". Specifies if - the simulated reward should be "complex" (drawn from the binomial - distribution + simulating user MP transitions) or "simple" - (high reward above a threshold - see "simple_reward_threshold" parameter) - simple_reward_threshold: defines the threshold above which - the simulated reward is high - cluster_distributions: defines the distribution of user clusters - - Returns: - Generated SARSes and synthetic users - """ - - synthesized_users = synthesize_users( - user_samples, cluster_distributions=cluster_distributions - ) - - all_services = Service.objects.order_by("id") - service_embedded_tensors, index_id_map = service_embedder(all_services) - - normalized_services = _normalize_embedded_services(service_embedded_tensors) - transition_rewards_df = pd.read_csv(TRANSITION_REWARDS_CSV_PATH, index_col="source") - sarses = [] - - for user in synthesized_users: - engaged_services = [] - ordered_services = [] - - state = State( - user=user, - services_history=[], - search_data=_get_relevant_search_data(user, ordered_services, K), - synthetic=True, - ).save() - - for _ in range( - np.random.randint(interactions_range[0], interactions_range[1] + 1) - ): - # Here we could even use PreAgent or even RLAgent if it's trained - action = random.sample( - list( - retrieve_services_for_recommendation( - state.search_data, ordered_services - ) - ), - k=K, - ) - - service_engagements = { - s: approx_service_engagement( - user, s, engaged_services, normalized_services, index_id_map - ) - for s in action - } - - rewards_dict = { - s: synthesize_reward( - transition_rewards_df, - engagement, - mode=reward_generation_mode, - simple_mode_threshold=simple_reward_threshold, - ) - for s, engagement in service_engagements.items() - } - - engaged_services += _get_engaged_services(rewards_dict) - ordered_services += _get_ordered_services(rewards_dict) - - next_state = State( - user=user, - services_history=engaged_services, - search_data=_get_relevant_search_data(user, ordered_services, K), - synthetic=True, - ).save() - - sars = Sars( - state=state, - action=action, - reward=list(rewards_dict.values()), - next_state=next_state, - synthetic=True, - ).save() - - sarses.append(sars) - state = next_state - - return sarses, synthesized_users - - def generate_synthetic_sarses( service_embedder: Embedder, K: List[int] = 3, @@ -232,6 +115,7 @@ def generate_synthetic_sarses( state = State( user=user, services_history=[], + elastic_services=Service.objects.distinct("id"), search_data=SearchData().save(), synthetic=True, ).save() @@ -242,7 +126,7 @@ def generate_synthetic_sarses( # Here we could even use PreAgent or even RLAgent if it's trained action = random.sample( list( - retrieve_services_for_recommendation( + retrieve_services_for_synthetic_sarses( state.search_data, ordered_services ) ), @@ -272,6 +156,7 @@ def generate_synthetic_sarses( next_state = State( user=user, services_history=engaged_services, + elastic_services=Service.objects.distinct("id"), search_data=SearchData().save(), synthetic=True, ).save() diff --git a/recommender/engines/rl/training/model_evaluation_step/model_evaluation_step.py b/recommender/engines/rl/training/model_evaluation_step/model_evaluation_step.py index f2a67043..5131c338 100644 --- a/recommender/engines/rl/training/model_evaluation_step/model_evaluation_step.py +++ b/recommender/engines/rl/training/model_evaluation_step/model_evaluation_step.py @@ -123,7 +123,8 @@ def _evaluate_recommendation_time(self, actor, sarses): for _ in range(self.time_measurement_samples): start = time() example_user = random.choice(sarses).state.user - example_state = create_state(example_user, SearchData()) + elastic_services = [service.id for service in Service.objects] + example_state = create_state(example_user, elastic_services, SearchData()) self._get_recommendation(actor, example_state) end = time() recommendation_durations.append(end - start) diff --git a/recommender/engines/rl/utils.py b/recommender/engines/rl/utils.py index 1537dbac..481d17c2 100644 --- a/recommender/engines/rl/utils.py +++ b/recommender/engines/rl/utils.py @@ -101,21 +101,25 @@ def get_service_indices(index_id_map: pd.DataFrame, ids: List[int]) -> List[int] return indices -def create_state(user: User, search_data: SearchData) -> State: +def create_state( + user: User, elastic_services: Tuple[int], search_data: SearchData +) -> State: """ Get needed information from context and create state. Args: user: MongoEngine User object. + elastic_services: Services search context from the Marketplace search_data: SearchData object. Returns: - state: State containing information about user and search_data + state: State containing information about user, elastic_services and search_data """ state = State( user=user, services_history=generate_services_history(user), + elastic_services=elastic_services, search_data=search_data, ) diff --git a/recommender/errors.py b/recommender/errors.py index 71601b05..305d91e0 100644 --- a/recommender/errors.py +++ b/recommender/errors.py @@ -170,6 +170,6 @@ def message(self): ) -class SizeOfUsersAndSearchDataError(Exception): +class SizeOfUsersAndElasticServicesError(Exception): def message(self): - return "Length of users and search data is not equal" + return "Length of users and elastic_services is not equal" diff --git a/recommender/models/recommendation.py b/recommender/models/recommendation.py index 8e239a90..19380364 100644 --- a/recommender/models/recommendation.py +++ b/recommender/models/recommendation.py @@ -24,6 +24,7 @@ class Recommendation(Document): panel_id = StringField() engine_version = StringField(blank=True) services = ListField(ReferenceField(Service)) + elastic_services = ListField(ReferenceField(Service)) search_data = ReferenceField(SearchData) processed = BooleanField(blank=True) diff --git a/recommender/models/state.py b/recommender/models/state.py index e3c014dc..b6cc81df 100644 --- a/recommender/models/state.py +++ b/recommender/models/state.py @@ -15,5 +15,6 @@ class State(Document): user = ReferenceField(User, blank=True) services_history = ListField(ReferenceField(Service)) + elastic_services = ListField(ReferenceField(Service)) search_data = ReferenceField(SearchData) synthetic = BooleanField(default=False) diff --git a/recommender/services/deserializer.py b/recommender/services/deserializer.py index 0e3b6ee6..33ad4814 100644 --- a/recommender/services/deserializer.py +++ b/recommender/services/deserializer.py @@ -46,6 +46,7 @@ def deserialize_recommendation(cls, json_dict): panel_id=json_dict.get("panel_id"), engine_version=json_dict.get("engine_version"), services=json_dict.get("services"), + elastic_services=json_dict.get("elastic_services"), search_data=search_data, ) diff --git a/recommender/services/fts.py b/recommender/services/fts.py index 94f37626..e0043f41 100644 --- a/recommender/services/fts.py +++ b/recommender/services/fts.py @@ -2,7 +2,7 @@ """Mongo FTS Operations module""" -from typing import Iterable, Optional +from typing import Iterable, Optional, List, Tuple from mongoengine import QuerySet from recommender.models import Service, SearchData @@ -44,6 +44,32 @@ def filter_services(search_data: SearchData) -> QuerySet: def retrieve_services_for_recommendation( + elastic_services: Tuple[int], accessed_services: Optional[Iterable] = None +) -> QuerySet: + """ + Selecting candidates for recommendation + + Args: + elastic_services: Marketplace's context + accessed_services: Services that user accessed + """ + q = list(elastic_services) + q = filter_available_and_recommendable_services(q) + q = q(id__nin=[s.id for s in accessed_services]) if accessed_services else q + return q + + +def filter_available_and_recommendable_services(q: List[int]) -> QuerySet: + """ + - Check if services exist in the RS database. + - Check if the status of a service allows the service to be recommended + """ + q = Service.objects(id__in=q) + q = q(status__in=AVAILABLE_FOR_RECOMMENDATION) + return q + + +def retrieve_services_for_synthetic_sarses( search_data: SearchData, accessed_services: Optional[Iterable] = None ): """Applies search info from MP and filters MongoDB by them""" diff --git a/recommender/utils.py b/recommender/utils.py index 444c24df..e36f1839 100644 --- a/recommender/utils.py +++ b/recommender/utils.py @@ -5,7 +5,6 @@ import json import random -import functools from time import time from datetime import datetime from typing import Dict, List, Union, Optional, Any @@ -14,10 +13,9 @@ import graphviz from bson import SON, ObjectId from mongoengine import Document -from torch.utils.tensorboard import SummaryWriter from tqdm.auto import tqdm -from definitions import ROOT_DIR, RUN_DIR +from definitions import ROOT_DIR from recommender.engines.panel_id_to_services_number_mapping import PANEL_ID_TO_K from recommender.engines.rl.utils import _get_visit_ids from recommender.models import User, Service, UserAction, Recommendation @@ -210,58 +208,6 @@ def load_examples() -> Dict: return examples -def timeit(func): - if "performance_measurements" not in globals(): - global performance_measurements - performance_measurements = {} - - if "writer" not in globals(): - global writer - writer = SummaryWriter(log_dir=RUN_DIR) - - @functools.wraps(func) - def newfunc(*args, **kwargs): - start = time() - ret_val = func(*args, **kwargs) - end = time() - elapsed = end - start - - key = f"{func.__name__}" - if isinstance(performance_measurements.get(key), list): - performance_measurements[key].append(elapsed) - elif performance_measurements.get(key) is None: - performance_measurements[key] = [elapsed] - - step = len(performance_measurements[key]) - writer.add_scalars("Performance", {key: elapsed}, step) - writer.flush() - - return ret_val - - return newfunc - - -def show_times(): - if "performance_measurements" not in globals(): - global performance_measurements - performance_measurements = {} - - for key, value in performance_measurements.items(): - records = len(value) - mean = sum(value) / records - logger.info( - "[%s] Mean execution time: %s, records number: %s", key, mean, records - ) - - return performance_measurements - - -def clear_times(): - if "performance_measurements" not in globals(): - global performance_measurements - performance_measurements = {} - - def visualize_uas(filename=None, view=True, save=False): """Visualize all user actions as a graph in the svg file""" diff --git a/tests/endpoints/test_recommendations.py b/tests/endpoints/test_recommendations.py index 1ce75c4d..6ff41692 100644 --- a/tests/endpoints/test_recommendations.py +++ b/tests/endpoints/test_recommendations.py @@ -16,6 +16,7 @@ def recommendation_data(): "page_id": "some_page_identifier", "panel_id": "v1", "engine_version": "NCF", + "elastic_services": [1, 2, 3], "search_data": { "q": "Cloud GPU", "categories": [1], diff --git a/tests/engine/preprocessing/test_search_data_encoder.py b/tests/engine/preprocessing/test_search_data_encoder.py deleted file mode 100644 index 77c26b57..00000000 --- a/tests/engine/preprocessing/test_search_data_encoder.py +++ /dev/null @@ -1,117 +0,0 @@ -# pylint: disable-all - -import pytest -import torch - -from recommender.engines.rl.ml_components.search_data_encoder import ( - SearchDataEncoder, -) -from recommender.models import SearchData -from tests.factories.marketplace import ServiceFactory, UserFactory -from tests.factories.marketplace.category import CategoryFactory -from tests.factories.marketplace.scientific_domain import ScientificDomainFactory - - -@pytest.fixture -def categories(mongo): - return CategoryFactory.create_batch(4) - - -@pytest.fixture -def scientific_domains(mongo): - return ScientificDomainFactory.create_batch(4) - - -@pytest.fixture -def services(mongo, categories, scientific_domains): - return [ - ServiceFactory( - id=2, categories=categories[:2], scientific_domains=scientific_domains[2:] - ), # i=1 - ServiceFactory( - id=8, categories=categories, scientific_domains=scientific_domains - ), # i=4 - ServiceFactory( - id=4, categories=categories[2:], scientific_domains=[scientific_domains[3]] - ), # i=2 - ServiceFactory( - id=6, categories=[categories[3]], scientific_domains=scientific_domains - ), # i=3 - ServiceFactory(id=11, status="draft"), # i=5 - ServiceFactory(id=1, status="errored"), # i=0 - ] - - -@pytest.fixture -def users(mongo, services): - return [ - UserFactory(accessed_services=[2, 1]), - UserFactory(accessed_services=[6, 11]), - ] - - -@pytest.fixture -def encoder(mongo, services): - return SearchDataEncoder() - - -def test_search_data_encoder_init(encoder, services, users): - assert encoder.I == len(services) - assert encoder.forbidden_service_indices == [0, 5] - - -def test_search_data_encoder_empty_search(encoder, services, users): - desired_mask = torch.Tensor([[0, 0, 1, 1, 1, 0], [0, 1, 1, 0, 1, 0]]) - - encoder_mask = encoder(users, [SearchData()] * len(users)) - assert torch.equal(encoder_mask, desired_mask) - - -def test_search_data_encoder_full_search( - encoder, services, users, categories, scientific_domains -): - search_data = SearchData( - categories=categories, scientific_domains=scientific_domains - ) - - desired_mask = torch.Tensor([[0, 0, 1, 1, 1, 0], [0, 1, 1, 0, 1, 0]]) - - encoder_mask = encoder(users, [search_data] * len(users)) - assert torch.equal(encoder_mask, desired_mask) - - -def test_search_data_encoder_single_search( - encoder, services, users, categories, scientific_domains -): - sd1 = SearchData(categories=categories[:2]) - sd2 = SearchData(categories=categories[2:]) - sd3 = SearchData(scientific_domains=scientific_domains[:2]) - sd4 = SearchData(scientific_domains=[scientific_domains[2]]) - - desired_mask1 = torch.Tensor( - [ - [0, 0, 0, 0, 1, 0], - [0, 0, 1, 0, 1, 0], - ] - ) - - desired_mask2 = torch.Tensor([[0, 0, 0, 1, 1, 0], [0, 1, 0, 0, 1, 0]]) - - assert torch.equal(encoder(users, [sd1, sd2]), desired_mask1) - assert torch.equal(encoder(users, [sd3, sd4]), desired_mask2) - - -def test_search_data_encoder_multiple_search( - encoder, services, users, categories, scientific_domains -): - sd1 = SearchData( - categories=[categories[1]], scientific_domains=[scientific_domains[2]] - ) - sd2 = SearchData( - categories=[categories[0]], scientific_domains=[scientific_domains[3]] - ) - - desired_mask = torch.Tensor([[0, 0, 0, 0, 1, 0], [0, 1, 0, 0, 1, 0]]) - - encoder_mask = encoder(users, [sd1, sd2]) - assert torch.equal(encoder_mask, desired_mask) diff --git a/tests/engine/preprocessing/test_service_encoder.py b/tests/engine/preprocessing/test_service_encoder.py new file mode 100644 index 00000000..f09b205f --- /dev/null +++ b/tests/engine/preprocessing/test_service_encoder.py @@ -0,0 +1,132 @@ +# pylint: disable-all + +import pytest +import torch + +from recommender.engines.rl.ml_components.service_encoder import ( + ServiceEncoder, +) +from tests.factories.search_data import SearchDataFactory +from tests.factories.state import StateFactory +from tests.factories.marketplace import ServiceFactory, UserFactory +from tests.factories.marketplace.category import CategoryFactory +from tests.factories.marketplace.scientific_domain import ScientificDomainFactory +from recommender.errors import SizeOfUsersAndElasticServicesError + + +@pytest.fixture +def categories(mongo): + return CategoryFactory.create_batch(4) + + +@pytest.fixture +def scientific_domains(mongo): + return ScientificDomainFactory.create_batch(4) + + +@pytest.fixture +def services(mongo, categories, scientific_domains): + return [ + ServiceFactory( + id=2, categories=categories[:2], scientific_domains=scientific_domains[2:] + ), # i=1 + ServiceFactory( + id=8, categories=categories, scientific_domains=scientific_domains + ), # i=4 + ServiceFactory( + id=4, categories=categories[2:], scientific_domains=[scientific_domains[3]] + ), # i=2 + ServiceFactory( + id=6, categories=[categories[3]], scientific_domains=scientific_domains + ), # i=3 + ServiceFactory(id=11, status="draft"), # i=5 + ServiceFactory(id=1, status="errored"), # i=0 + ] + + +@pytest.fixture +def states(services): + return [ + StateFactory( + elastic_services=[services[0], services[1], services[-1]], + search_data=SearchDataFactory(q=None), + non_empty_history=True, + ), + StateFactory( + elastic_services=[services[0], services[1], services[-1]], + search_data=SearchDataFactory(q=None), + non_empty_history=True, + ), + ] + + +@pytest.fixture +def users(mongo, services): + return [ + UserFactory(accessed_services=[2, 1]), + UserFactory(accessed_services=[6, 11]), + ] + + +@pytest.fixture +def encoder(services): + return ServiceEncoder() + + +def test_service_encoder_init(encoder, services, users): + assert encoder.I == len(services) + assert encoder.forbidden_service_indices == [0, 5] + + +def test_service_encoder_one_state_one_user(encoder): + state = StateFactory( + elastic_services=[2, 8, 4, 6, 11, 1], + search_data=SearchDataFactory(q=None), + non_empty_history=True, + ) + user = UserFactory(accessed_services=[2, 1]) + + desired_mask = torch.Tensor([[0, 0, 1, 1, 1, 0]]) + encoder_mask = encoder([user], [state]) + + assert torch.equal(encoder_mask, desired_mask) + + +def test_service_encoder_multiple_states_users(encoder, states, users): + desired_mask = torch.Tensor([[0, 0, 0, 0, 1, 0], [0, 1, 0, 0, 1, 0]]) + + assert torch.equal(encoder(users, states), desired_mask) + + +def test_service_encoder_exception(encoder): + state = StateFactory( + elastic_services=[2, 8, 4, 6, 11, 1], + search_data=SearchDataFactory(q=None), + non_empty_history=True, + ) + user = [ + UserFactory(accessed_services=[2, 1]), + UserFactory(accessed_services=[8, 2]), + ] + + # 1 state and 2 users + with pytest.raises(SizeOfUsersAndElasticServicesError): + encoder(user, state) + + state = [ + StateFactory( + elastic_services=[2, 8, 4, 6, 11, 1], + search_data=SearchDataFactory(q=None), + non_empty_history=True, + ), + StateFactory( + elastic_services=[2, 8, 1], + search_data=SearchDataFactory(q=None), + non_empty_history=True, + ), + ] + user = [UserFactory(accessed_services=[2, 1])] + + # 2 states and 1 user + with pytest.raises(SizeOfUsersAndElasticServicesError): + encoder(user, state) diff --git a/tests/engines/autoencoders/ml_components/test_normalizer.py b/tests/engines/autoencoders/ml_components/test_normalizer.py index d506d7f3..7c675069 100644 --- a/tests/engines/autoencoders/ml_components/test_normalizer.py +++ b/tests/engines/autoencoders/ml_components/test_normalizer.py @@ -83,7 +83,6 @@ def test_normalizer_call_on_tensors_dim_wise(batch): def test_normalizer_call_on_tensors_norm_wise(batch): normalizer = Normalizer(mode=NormalizationMode.NORM_WISE) output, _ = normalizer(batch) - print(output) desired = torch.Tensor( [ [0.2411, -0.1002, -0.2520], diff --git a/tests/engines/ncf/inference/test_ncf_inference_component.py b/tests/engines/ncf/inference/test_ncf_inference_component.py index 5d3a5070..802fd9ae 100644 --- a/tests/engines/ncf/inference/test_ncf_inference_component.py +++ b/tests/engines/ncf/inference/test_ncf_inference_component.py @@ -1,6 +1,7 @@ # pylint: disable-all import pytest +import random from torch import Tensor from recommender.engines.autoencoders.ml_components.embedder import ( @@ -39,16 +40,26 @@ def test_ncf_inference_component( with pytest.raises(NoSavedMLComponentError): K = 2 - ncf_inference_component = NCFInferenceComponent(K) + NCFInferenceComponent(K) + + elastic_services = [service.id for service in Service.objects] # With-model case ncf_model.save(version=NEURAL_CF) + for panel_id_version, K in list(PANEL_ID_TO_K.items()): ncf_inference_component = NCFInferenceComponent(K) - user = User.objects.first() - context = {"panel_id": panel_id_version, "search_data": {}, "user_id": user.id} + user = random.choice(list(User.objects)) + + context = { + "panel_id": panel_id_version, + "elastic_services": elastic_services, + "search_data": {}, + "user_id": user.id, + } services_ids_1 = ncf_inference_component(context) + assert isinstance( ncf_inference_component.neural_cf_model, NeuralCollaborativeFilteringModel ) @@ -56,13 +67,20 @@ def test_ncf_inference_component( assert isinstance(services_ids_1, list) assert len(services_ids_1) == PANEL_ID_TO_K.get(context["panel_id"]) assert all([isinstance(service_id, int) for service_id in services_ids_1]) + assert all(service in elastic_services for service in services_ids_1) services_ids_2 = ncf_inference_component(context) assert services_ids_1 == services_ids_2 for panel_id_version, K in list(PANEL_ID_TO_K.items()): ncf_inference_component = NCFInferenceComponent(K) - context = {"panel_id": panel_id_version, "search_data": {}, "user_id": -1} + + context = { + "panel_id": panel_id_version, + "elastic_services": elastic_services, + "search_data": {}, + "user_id": -1, + } services_ids_1 = ncf_inference_component(context) assert isinstance( @@ -72,13 +90,19 @@ def test_ncf_inference_component( assert isinstance(services_ids_1, list) assert len(services_ids_1) == PANEL_ID_TO_K.get(context["panel_id"]) assert all([isinstance(service_id, int) for service_id in services_ids_1]) + assert all(service in elastic_services for service in services_ids_1) services_ids_2 = ncf_inference_component(context) assert services_ids_1 != services_ids_2 for panel_id_version, K in list(PANEL_ID_TO_K.items()): ncf_inference_component = NCFInferenceComponent(K) - context = {"panel_id": panel_id_version, "search_data": {}} + + context = { + "panel_id": panel_id_version, + "elastic_services": elastic_services, + "search_data": {}, + } services_ids_1 = ncf_inference_component(context) assert isinstance( @@ -88,6 +112,7 @@ def test_ncf_inference_component( assert isinstance(services_ids_1, list) assert len(services_ids_1) == PANEL_ID_TO_K.get(context["panel_id"]) assert all([isinstance(service_id, int) for service_id in services_ids_1]) + assert all(service in elastic_services for service in services_ids_1) services_ids_2 = ncf_inference_component(context) assert services_ids_1 != services_ids_2 diff --git a/tests/engines/rl/inference/test_rl_inference_component.py b/tests/engines/rl/inference/test_rl_inference_component.py index 0e1330f1..36ac8479 100644 --- a/tests/engines/rl/inference/test_rl_inference_component.py +++ b/tests/engines/rl/inference/test_rl_inference_component.py @@ -6,7 +6,7 @@ from recommender import User from recommender.engines.panel_id_to_services_number_mapping import PANEL_ID_TO_K from recommender.engines.rl.inference.rl_inference_component import RLInferenceComponent -from recommender.models import Category +from recommender.models import Category, Service @pytest.mark.parametrize("ver", ["v1", "v2"]) @@ -15,13 +15,21 @@ def test_known_user(mongo, generate_users_and_services, mock_rl_pipeline_exec, v inference_component = RLInferenceComponent(K=K) user = random.choice(list(User.objects)) - context = {"panel_id": ver, "search_data": {}, "user_id": user.id} + elastic_services = [service.id for service in Service.objects] + + context = { + "panel_id": ver, + "elastic_services": elastic_services, + "search_data": {}, + "user_id": user.id, + } services_ids_1 = inference_component(context) assert isinstance(services_ids_1, list) assert len(services_ids_1) == PANEL_ID_TO_K.get(context["panel_id"]) assert all([isinstance(service_id, int) for service_id in services_ids_1]) + assert all(service in elastic_services for service in services_ids_1) services_ids_2 = inference_component(context) assert services_ids_1 == services_ids_2 @@ -31,10 +39,20 @@ def test_known_user(mongo, generate_users_and_services, mock_rl_pipeline_exec, v def test_unknown_user(mongo, generate_users_and_services, mock_rl_pipeline_exec, ver): K = PANEL_ID_TO_K[ver] inference_component = RLInferenceComponent(K=K) + elastic_services = [service.id for service in Service.objects] contexts = [ - {"panel_id": ver, "search_data": {}, "user_id": -1}, - {"panel_id": ver, "search_data": {}}, + { + "panel_id": ver, + "elastic_services": elastic_services, + "search_data": {}, + "user_id": -1, + }, + { + "panel_id": ver, + "elastic_services": elastic_services, + "search_data": {}, + }, ] for context in contexts: @@ -46,28 +64,6 @@ def test_unknown_user(mongo, generate_users_and_services, mock_rl_pipeline_exec, assert services_ids_1 != services_ids_2 -@pytest.mark.parametrize("ver", ["v1", "v2"]) -def test_not_empty_search_data( - mongo, generate_users_and_services, mock_rl_pipeline_exec, ver -): - K = PANEL_ID_TO_K[ver] - inference_component = RLInferenceComponent(K=K) - - user = User.objects.first() - context = { - "panel_id": ver, - "search_data": {"categories": list(Category.objects.distinct("id"))}, - "user_id": user.id, - } - - services_ids_1 = inference_component(context) - _check_proper(services_ids_1, ver) - - services_ids_2 = inference_component(context) - _check_proper(services_ids_2, ver) - assert services_ids_1 == services_ids_2 - - def _check_proper(services, ver): assert isinstance(services, list) assert len(services) == PANEL_ID_TO_K.get(ver) diff --git a/tests/services/synthetic_dataset/test_dataset.py b/tests/services/synthetic_dataset/test_dataset.py index e13548db..f315cd8c 100644 --- a/tests/services/synthetic_dataset/test_dataset.py +++ b/tests/services/synthetic_dataset/test_dataset.py @@ -38,10 +38,3 @@ def test__normalize_embedded_services(service_embeddings, normalized_embedded_se normalized_embedded_services, atol=10e-5, ) - - -@pytest.mark.skip( - reason="no way of currently testing this as it requires infeasible amount of setup" -) -def test_generate_dataset(): - pass diff --git a/tests/services/test_fts.py b/tests/services/test_fts.py index 34c6eaf9..8c7f913b 100644 --- a/tests/services/test_fts.py +++ b/tests/services/test_fts.py @@ -1,8 +1,10 @@ # pylint: disable-all +import pytest from recommender.models import Service, SearchData from recommender.services.fts import ( retrieve_services_for_recommendation, + retrieve_services_for_synthetic_sarses, retrieve_forbidden_services, filter_services, ) @@ -14,6 +16,19 @@ from tests.factories.marketplace.target_user import TargetUserFactory +@pytest.fixture +def create_services(mongo): + services = [ + ServiceFactory(status="published"), + ServiceFactory(status="unverified"), + ServiceFactory(status="unverified"), + ServiceFactory(status="errored"), + ServiceFactory(status="deleted"), + ServiceFactory(status="draft"), + ] + return services + + def test_filter_services(mongo, mocker): categories = CategoryFactory.create_batch(2) providers = ProviderFactory.create_batch(2) @@ -102,32 +117,32 @@ def test_filter_services(mongo, mocker): ) == [s1] -def test_retrieve_forbidden_services(mongo): - services = [ - ServiceFactory(status="published"), - ServiceFactory(status="unverified"), - ServiceFactory(status="errored"), - ServiceFactory(status="deleted"), - ServiceFactory(status="draft"), - ServiceFactory(status="unverified"), - ] +def test_retrieve_forbidden_services(create_services): + services = create_services - assert list(retrieve_forbidden_services()) == services[2:5] + assert list(retrieve_forbidden_services()) == services[3:6] -def test_retrieve_services_for_recommendations(mongo): - services = [ - ServiceFactory(status="published"), - ServiceFactory(status="published"), - ServiceFactory(status="unverified"), - ServiceFactory(status="unverified"), - ServiceFactory(status="deleted"), - ServiceFactory(status="draft"), - ] +def test_retrieve_services_for_recommendation(create_services): + services = create_services + services_id = [] - assert list(retrieve_services_for_recommendation(SearchData())) == services[:4] + [services_id.append(service.id) for service in services] + + assert list(retrieve_services_for_recommendation(services_id)) == services[:3] assert list( retrieve_services_for_recommendation( + services_id, accessed_services=[services[0], services[2]] + ) + ) == [services[1]] + + +def test_retrieve_services_for_synthetic_sarses(create_services): + services = create_services + + assert list(retrieve_services_for_synthetic_sarses(SearchData())) == services[:3] + assert list( + retrieve_services_for_synthetic_sarses( SearchData(), accessed_services=[services[0], services[2]] ) - ) == [services[1], services[3]] + ) == [services[1]]