├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── data └── spotify.dump └── src ├── api_models.py ├── config.py ├── database_models.py └── main.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | 162 | .vscode 163 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 neo4j-field 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | neomodel = "~=5.2.0" 8 | fastapi = "*" 9 | uvicorn = "*" 10 | 11 | [dev-packages] 12 | 13 | [requires] 14 | python_version = "3.12" 15 | 16 | [scripts] 17 | dev = "uvicorn --host=0.0.0.0 --port=8000 src.main:app --reload" 18 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "c0b92892151d71e1a71e1ec88f339e12016218efed8e619db550a1b69b8ba394" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.12" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "annotated-types": { 20 | "hashes": [ 21 | "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", 22 | "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" 23 | ], 24 | "markers": "python_version >= '3.8'", 25 | "version": "==0.6.0" 26 | }, 27 | "anyio": { 28 | "hashes": [ 29 | "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", 30 | "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5" 31 | ], 32 | "markers": "python_version >= '3.7'", 33 | "version": "==3.7.1" 34 | }, 35 | "click": { 36 | "hashes": [ 37 | "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", 38 | "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" 39 | ], 40 | "markers": "python_version >= '3.7'", 41 | "version": "==8.1.7" 42 | }, 43 | "fastapi": { 44 | "hashes": [ 45 | "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241", 46 | "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae" 47 | ], 48 | "index": "pypi", 49 | "version": "==0.104.1" 50 | }, 51 | "h11": { 52 | "hashes": [ 53 | "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", 54 | "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" 55 | ], 56 | "markers": "python_version >= '3.7'", 57 | "version": "==0.14.0" 58 | }, 59 | "idna": { 60 | "hashes": [ 61 | "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", 62 | "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" 63 | ], 64 | "markers": "python_version >= '3.5'", 65 | "version": "==3.6" 66 | }, 67 | "neo4j": { 68 | "hashes": [ 69 | "sha256:00a776a687267150f9e1950017316b751cf63db7b734a699b1405ac20fd4a731" 70 | ], 71 | "markers": "python_version >= '3.7'", 72 | "version": "==5.12.0" 73 | }, 74 | "neobolt": { 75 | "hashes": [ 76 | "sha256:1d0d5efce7221fc4f0ffc4a315bc5272708be5aa2aef5434269e800372d8db89" 77 | ], 78 | "version": "==1.7.17" 79 | }, 80 | "neomodel": { 81 | "hashes": [ 82 | "sha256:4da18248c510b8fce8133131d7b3ef498343062ea1181517f0c7a4fc9328e73a", 83 | "sha256:816ef2150b3f99ad76d626001da6da10244b868f18bb69a8b0672b40d4db55f2" 84 | ], 85 | "index": "pypi", 86 | "version": "==5.2.0" 87 | }, 88 | "pydantic": { 89 | "hashes": [ 90 | "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0", 91 | "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd" 92 | ], 93 | "markers": "python_version >= '3.7'", 94 | "version": "==2.5.2" 95 | }, 96 | "pydantic-core": { 97 | "hashes": [ 98 | "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b", 99 | "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b", 100 | "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d", 101 | "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8", 102 | "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124", 103 | "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189", 104 | "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c", 105 | "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d", 106 | "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f", 107 | "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520", 108 | "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4", 109 | "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6", 110 | "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955", 111 | "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3", 112 | "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b", 113 | "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a", 114 | "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68", 115 | "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3", 116 | "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd", 117 | "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de", 118 | "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b", 119 | "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634", 120 | "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7", 121 | "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459", 122 | "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7", 123 | "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3", 124 | "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331", 125 | "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf", 126 | "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d", 127 | "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36", 128 | "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59", 129 | "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937", 130 | "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc", 131 | "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093", 132 | "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753", 133 | "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706", 134 | "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca", 135 | "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260", 136 | "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997", 137 | "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588", 138 | "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71", 139 | "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb", 140 | "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e", 141 | "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69", 142 | "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5", 143 | "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07", 144 | "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1", 145 | "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0", 146 | "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd", 147 | "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8", 148 | "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944", 149 | "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26", 150 | "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda", 151 | "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4", 152 | "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9", 153 | "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00", 154 | "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe", 155 | "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6", 156 | "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada", 157 | "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4", 158 | "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7", 159 | "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325", 160 | "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4", 161 | "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b", 162 | "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88", 163 | "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04", 164 | "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863", 165 | "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0", 166 | "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911", 167 | "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b", 168 | "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e", 169 | "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144", 170 | "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5", 171 | "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720", 172 | "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab", 173 | "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d", 174 | "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789", 175 | "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec", 176 | "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2", 177 | "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db", 178 | "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f", 179 | "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef", 180 | "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3", 181 | "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209", 182 | "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc", 183 | "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651", 184 | "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8", 185 | "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e", 186 | "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66", 187 | "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7", 188 | "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550", 189 | "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd", 190 | "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405", 191 | "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27", 192 | "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093", 193 | "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077", 194 | "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113", 195 | "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3", 196 | "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6", 197 | "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf", 198 | "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed", 199 | "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88", 200 | "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe", 201 | "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18", 202 | "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867" 203 | ], 204 | "markers": "python_version >= '3.7'", 205 | "version": "==2.14.5" 206 | }, 207 | "pytz": { 208 | "hashes": [ 209 | "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", 210 | "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" 211 | ], 212 | "version": "==2023.3.post1" 213 | }, 214 | "six": { 215 | "hashes": [ 216 | "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", 217 | "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" 218 | ], 219 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", 220 | "version": "==1.16.0" 221 | }, 222 | "sniffio": { 223 | "hashes": [ 224 | "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101", 225 | "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384" 226 | ], 227 | "markers": "python_version >= '3.7'", 228 | "version": "==1.3.0" 229 | }, 230 | "starlette": { 231 | "hashes": [ 232 | "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", 233 | "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91" 234 | ], 235 | "markers": "python_version >= '3.7'", 236 | "version": "==0.27.0" 237 | }, 238 | "typing-extensions": { 239 | "hashes": [ 240 | "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", 241 | "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" 242 | ], 243 | "markers": "python_version >= '3.8'", 244 | "version": "==4.8.0" 245 | }, 246 | "uvicorn": { 247 | "hashes": [ 248 | "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e", 249 | "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e" 250 | ], 251 | "index": "pypi", 252 | "version": "==0.24.0.post1" 253 | } 254 | }, 255 | "develop": {} 256 | } 257 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neo4j-python-api-sample 2 | This is a starter / sample app for a Python API using FastAPI and neomodel 3 | 4 | ## Requirements 5 | * Python >= 3.7 6 | * Neo4j >= 5.11 7 | 8 | ## Installation 9 | 10 | ### Neo4j 11 | 12 | You need a running instance of Neo4j. See the [Installation guide](https://neo4j.com/docs/operations-manual/current/installation/) here for options (including AuraDB). 13 | 14 | Then, load the dump provided in the [data](/data/spotify.dump) folder. See how to proceed here : for [Aura](https://neo4j.com/docs/aura/auradb/importing/import-database/), or for [self-managed](https://neo4j.com/docs/operations-manual/current/backup-restore/restore-dump/). 15 | 16 | This database was created using a [Kaggle Spotify dataset](https://www.kaggle.com/datasets/thedevastator/popularity-of-spotify-top-tracks-by-genre). 17 | 18 | ### API 19 | 20 | To install and run the API, run the following : 21 | 22 | ```cmd 23 | pip install pipenv 24 | pipenv install 25 | ``` 26 | 27 | In main.py, change the value of `config.DATABASE_URL` to match your database information 28 | 29 | Finally, run : 30 | 31 | ```cmd 32 | pipenv run dev 33 | ``` 34 | 35 | ### Querying the API 36 | 37 | Go to [http://localhost:8000](http://localhost:8000) in your web browser and you should see the number of songs in your database. 38 | 39 | Next, you can navigate to [http://localhost:8000/artists?page_size=10&page_number=5](http://localhost:8000/artists?page_size=10&page_number=5) and you will see a list of artists and their songs. Pick a couple of song uids for the next step. 40 | 41 | Finally, you can create your own playlist using the following command : 42 | 43 | ```cmd 44 | curl -X POST -H "Content-Type: application/json" -d '{ 45 | "title": "The Hives Best Of", 46 | "songs": ["7r68k5cbrF0GiFGSY538MW", "05pYGBaNqoIqsy5FjsnL1o"] 47 | }' http://localhost:8000/playlists 48 | ``` 49 | 50 | You should get a similar result. Note that the API went and fetched the properties of the songs after saving the playlist to the database. 51 | 52 | ```json 53 | { 54 | "uid":"2721c44d238f4de4888ddd7f7eca834e", 55 | "title":"The Hives Best Of", 56 | "songs":[ 57 | {"uid":"05pYGBaNqoIqsy5FjsnL1o","title":"1000 Answers","popularity":0}, 58 | {"uid":"7r68k5cbrF0GiFGSY538MW","title":"A.K.A. I-D-I-O-T","popularity":0} 59 | ]} 60 | ``` 61 | 62 | ### Indexes and constraints 63 | 64 | To let neomodel figure out the constraints and indexes from the model description, run the following script : 65 | 66 | ```cmd 67 | neomodel_install_labels src/database_models.py --db=bolt://neo4j:password@localhost:7687/spotify 68 | ``` 69 | 70 | This will look at [database_models.py](/src/database_models.py) and create constraints for properties with `unique_index=True` and indexes for properties with `index=True`. 71 | 72 | 73 | ## Customization 74 | 75 | If you want to start tuning this sample API to your existing Neo4j database, you can use the inspection script, available from neomodel version 5.2.0 : 76 | 77 | ```cmd 78 | # This will create a file with all classes to match your db, including indexes and constraints 79 | neomodel_inspect_database --db=bolt://neo4j:password@localhost:7687/spotify --write-to src/custom_database_models.py 80 | ``` 81 | 82 | -------------------------------------------------------------------------------- /data/spotify.dump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j-field/neo4j-python-api-sample/8dcb6a47dd42c7822128934bfb82f981bd14b33a/data/spotify.dump -------------------------------------------------------------------------------- /src/api_models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Classes for representing music data, from the API point of view. 3 | 4 | It provides both classes as returned by the API, and also Input classes for creating objects. 5 | 6 | Classes: 7 | - AlbumAPI: Represents an album in the music database. 8 | - SongAPI: Represents a song in the music database. 9 | - ArtistAPI: Represents an artist in the music database. 10 | - PlaylistAPI: Represents a playlist in the music database. 11 | - PlaylistInput: Input to provide in a POST request to create a playlist. 12 | 13 | """ 14 | from typing import List, Optional 15 | from pydantic import BaseModel 16 | 17 | 18 | class AlbumAPI(BaseModel): 19 | uid: str 20 | 21 | 22 | class SongAPI(BaseModel): 23 | uid: str 24 | title: str 25 | loudness: float 26 | liveness: float 27 | tempo: float 28 | valence: float 29 | instrumentalness: float 30 | danceability: float 31 | speechiness: float 32 | duration: int 33 | mode: bool 34 | popularity: int 35 | acousticness: float 36 | key: int 37 | energy: float 38 | 39 | albums: Optional[List[AlbumAPI]] = None 40 | 41 | 42 | class ArtistAPI(BaseModel): 43 | uid: str 44 | name: str 45 | songs: Optional[List[SongAPI]] 46 | 47 | 48 | class PlaylistAPI(BaseModel): 49 | uid: str 50 | title: str 51 | songs: Optional[List[SongAPI]] 52 | 53 | 54 | class PlaylistInput(BaseModel): 55 | title: str 56 | songs: List[str] 57 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | """Configuration parameters""" 2 | from neomodel import config 3 | 4 | config.DATABASE_URL = "bolt://neo4j:password@localhost:7687/spotify" 5 | -------------------------------------------------------------------------------- /src/database_models.py: -------------------------------------------------------------------------------- 1 | """ 2 | The module provides classes for representing music data in a Neo4j database using neomodel. 3 | 4 | Classes: 5 | - Album: Represents an album in the music database. 6 | - Song: Represents a song in the music database. 7 | - Artist: Represents an artist in the music database. 8 | - Playlist: Represents a playlist in the music database. 9 | 10 | Dependencies: 11 | - neomodel: The module relies on the neomodel library for interacting with the Neo4j database. 12 | """ 13 | 14 | from neomodel import ( 15 | StructuredNode, 16 | StringProperty, 17 | IntegerProperty, 18 | FloatProperty, 19 | BooleanProperty, 20 | RelationshipTo, 21 | ZeroOrMore, 22 | OneOrMore, 23 | UniqueIdProperty, 24 | ) 25 | 26 | 27 | class Album(StructuredNode): 28 | """ 29 | An album is represented only by its unique identifier. 30 | 31 | Properties: 32 | - uid: str 33 | 34 | Relationships: None 35 | """ 36 | 37 | uid = UniqueIdProperty() 38 | 39 | 40 | class Song(StructuredNode): 41 | """ 42 | A song has a title and musical characteristics defined by Spotify. 43 | 44 | It is released in at least one album. 45 | 46 | Properties: 47 | - uid: str 48 | - title: str 49 | - ... see class 50 | 51 | Relationships: 52 | - albums: One or more 53 | """ 54 | 55 | uid = UniqueIdProperty() 56 | title = StringProperty(index=True) 57 | loudness = FloatProperty() 58 | liveness = FloatProperty() 59 | tempo = FloatProperty() 60 | valence = FloatProperty() 61 | instrumentalness = FloatProperty() 62 | danceability = FloatProperty() 63 | speechiness = FloatProperty() 64 | duration = IntegerProperty() 65 | mode = BooleanProperty() 66 | popularity = IntegerProperty() 67 | acousticness = FloatProperty() 68 | key = IntegerProperty() 69 | energy = FloatProperty() 70 | 71 | albums = RelationshipTo(Album, "RELEASED_IN", cardinality=OneOrMore) 72 | 73 | 74 | class Artist(StructuredNode): 75 | """ 76 | An artist has a name, and can have recorded songs. 77 | 78 | Properties: 79 | - uid: str 80 | - name: str 81 | 82 | Relationships: 83 | - songs: Zero or more 84 | """ 85 | 86 | uid = UniqueIdProperty() 87 | name = StringProperty(index=True) 88 | 89 | songs = RelationshipTo(Song, "RECORDED", cardinality=ZeroOrMore) 90 | 91 | 92 | class Playlist(StructuredNode): 93 | """ 94 | A playlist has a title, and at least one song. 95 | 96 | Properties: 97 | - uid: str 98 | - title: str 99 | 100 | Relationships: 101 | - songs: One or more 102 | """ 103 | 104 | uid = UniqueIdProperty() 105 | title = StringProperty(index=True) 106 | 107 | songs = RelationshipTo(Song, "CONTAINS", cardinality=OneOrMore) 108 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | """Application main file.""" 2 | from fastapi import FastAPI, HTTPException 3 | 4 | from neomodel import DoesNotExist 5 | from .database_models import Artist, Song, Playlist 6 | from . import config 7 | from .api_models import AlbumAPI, ArtistAPI, PlaylistAPI, SongAPI, PlaylistInput 8 | 9 | # Create app 10 | app = FastAPI(title="neomodel sample project") 11 | 12 | 13 | @app.get("/") 14 | async def root(): 15 | songs = Song.nodes.all() 16 | return {"# of songs": len(songs)} 17 | 18 | 19 | @app.get("/artists") 20 | async def get_artists(page_size: int = 10, page_number: int = 0): 21 | artists = Artist.nodes.order_by("name")[ 22 | page_number * page_size : page_number * page_size + page_size 23 | ] 24 | return [ 25 | ArtistAPI( 26 | uid=a.uid, 27 | name=a.name, 28 | songs=[ 29 | SongAPI( 30 | uid=s.uid, 31 | title=s.title, 32 | popularity=s.popularity, 33 | loudness=s.loudness, 34 | liveness=s.liveness, 35 | tempo=s.tempo, 36 | valence=s.valence, 37 | instrumentalness=s.instrumentalness, 38 | danceability=s.danceability, 39 | speechiness=s.speechiness, 40 | duration=s.duration, 41 | mode=s.mode, 42 | acousticness=s.acousticness, 43 | key=s.key, 44 | energy=s.energy, 45 | ) 46 | for s in a.songs.all() 47 | ], 48 | ) 49 | for a in artists 50 | ] 51 | 52 | 53 | @app.get("/songs") 54 | async def get_song(song_uid: str): 55 | song = Song.nodes.get(uid=song_uid) 56 | return SongAPI( 57 | uid=song.uid, 58 | title=song.title, 59 | loudness=song.loudness, 60 | liveness=song.liveness, 61 | tempo=song.tempo, 62 | valence=song.valence, 63 | instrumentalness=song.instrumentalness, 64 | danceability=song.danceability, 65 | speechiness=song.speechiness, 66 | duration=song.duration, 67 | mode=song.mode, 68 | popularity=song.popularity, 69 | acousticness=song.acousticness, 70 | key=song.key, 71 | energy=song.energy, 72 | albums=[AlbumAPI(uid=a.uid) for a in song.albums.all()], 73 | ) 74 | 75 | 76 | @app.post("/playlists") 77 | async def create_playlist(input: PlaylistInput): 78 | try: 79 | playlist = Playlist( 80 | title=input.title, 81 | ).save() 82 | 83 | for _song in input.songs: 84 | song = Song.nodes.get(uid=_song) 85 | playlist.songs.connect(song) 86 | 87 | except DoesNotExist as exc: 88 | raise HTTPException(status_code=500, detail="Item not found") from exc 89 | return PlaylistAPI( 90 | uid=playlist.uid, 91 | title=playlist.title, 92 | songs=[ 93 | SongAPI(uid=s.uid, title=s.title, popularity=s.popularity) 94 | for s in playlist.songs.all() 95 | ], 96 | ) 97 | --------------------------------------------------------------------------------