diff --git a/examples/music-licensing-challenge/Dockerfile b/examples/music-licensing-challenge/Dockerfile new file mode 100755 index 0000000..7d5bd2b --- /dev/null +++ b/examples/music-licensing-challenge/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "src.app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] \ No newline at end of file diff --git a/examples/music-licensing-challenge/Pipfile b/examples/music-licensing-challenge/Pipfile new file mode 100755 index 0000000..abdc752 --- /dev/null +++ b/examples/music-licensing-challenge/Pipfile @@ -0,0 +1,19 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +fastapi = "==0.104.1" +uvicorn = "==0.24.0" +sqlalchemy = "==2.0.23" +psycopg2-binary = "==2.9.9" +python-dotenv = "==1.0.0" +pydantic = "==2.5.2" +python-multipart = "==0.0.6" +alembic = "==1.12.1" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/examples/music-licensing-challenge/Pipfile.lock b/examples/music-licensing-challenge/Pipfile.lock new file mode 100755 index 0000000..a3cde80 --- /dev/null +++ b/examples/music-licensing-challenge/Pipfile.lock @@ -0,0 +1,521 @@ +{ + "_meta": { + "hash": { + "sha256": "70fc092629e7eeafad096be3b0ac2c92510eaf614a7608ba88c062164b5c543b" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "alembic": { + "hashes": [ + "sha256:47d52e3dfb03666ed945becb723d6482e52190917fdb47071440cfdba05d92cb", + "sha256:bca5877e9678b454706347bc10b97cb7d67f300320fa5c3a94423e8266e2823f" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==1.12.1" + }, + "annotated-types": { + "hashes": [ + "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" + ], + "markers": "python_version >= '3.8'", + "version": "==0.7.0" + }, + "anyio": { + "hashes": [ + "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780", + "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5" + ], + "markers": "python_version >= '3.7'", + "version": "==3.7.1" + }, + "click": { + "hashes": [ + "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", + "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.8" + }, + "fastapi": { + "hashes": [ + "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241", + "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.104.1" + }, + "greenlet": { + "hashes": [ + "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0", + "sha256:04e781447a4722e30b4861af728cb878d73a3df79509dc19ea498090cea5d204", + "sha256:0e14541f9024a280adb9645143d6a0a51fda6f7c5695fd96cb4d542bb563442f", + "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5", + "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1", + "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038", + "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f", + "sha256:1cf89e2d92bae0d7e2d6093ce0bed26feeaf59a5d588e3984e35fcd46fc41090", + "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7", + "sha256:1dcb1108449b55ff6bc0edac9616468f71db261a4571f27c47ccf3530a7f8b97", + "sha256:211a9721f540e454a02e62db7956263e9a28a6cf776d4b9a7213844e36426333", + "sha256:23f56a0103deb5570c8d6a0bb4ddf8a7a28931973ad7ed7a883460a67e599b32", + "sha256:2688b3bd3198cc4bad7a79648a95fee088c24a0f6abd05d3639e6c3040ded015", + "sha256:2919b126eeb63ca5fa971501cd20cd6cdb5522369a8e39548bbc73a3e10b8b41", + "sha256:29449a2b82ed7ce11f8668c31ef20d31e9d88cd8329eb933098fab5a8608a93a", + "sha256:2b986f1a6467710e7ffeeeac1777da0318c95bbfcc467acbd0bd35abc775f558", + "sha256:33ea7e7269d6f7275ce31f593d6dcfedd97539c01f63fbdc8d84e493e20b1b2c", + "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b", + "sha256:39801e633a978c3f829f21022501e7b0c3872683d7495c1850558d1a6fb95ed0", + "sha256:4174fa6fa214e8924cedf332b6f2395ba2b9879f250dacd3c361b2fca86f58af", + "sha256:430cba962c85e339767235a93450a6aaffed6f9c567e73874ea2075f5aae51e1", + "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6", + "sha256:58ef3d637c54e2f079064ca936556c4af3989144e4154d80cfd4e2a59fc3769c", + "sha256:598da3bd464c2cc411b723e3d4afc27b13c219ac077ba897bac88443ae45f5ec", + "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280", + "sha256:5e57ff52315bfc0c5493917f328b8ba3ae0c0515d94524453c4d24e7638cbb53", + "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c", + "sha256:6017a4d430fad5229e397ad464db504ae70cb7b903757c4688cee6c25d6ce8d8", + "sha256:60e77242e38e99ecaede853755bbd8165e0b20a2f1f3abcaa6f0dceb826a7411", + "sha256:6fad8a9ca98b37951a053d7d2d2553569b151cd8c4ede744806b94d50d7f8f73", + "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517", + "sha256:78b721dfadc60e3639141c0e1f19d23953c5b4b98bfcaf04ce40f79e4f01751c", + "sha256:7b162de2fb61b4c7f4b5d749408bf3280cae65db9b5a6aaf7f922ac829faa67c", + "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790", + "sha256:7d08b88ee8d506ca1f5b2a58744e934d33c6a1686dd83b81e7999dfc704a912f", + "sha256:7f163d04f777e7bd229a50b937ecc1ae2a5b25296e6001445e5433e4f51f5191", + "sha256:7fee6f518868e8206c617f4084a83ad4d7a3750b541bf04e692dfa02e52e805d", + "sha256:82a68a25a08f51fc8b66b113d1d9863ee123cdb0e8f1439aed9fc795cd6f85cf", + "sha256:844acfd479ee380f3810415e682c9ee941725fb90b45e139bb7fd6f85c6c9a30", + "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95", + "sha256:8b3538711e7c0efd5f7a8fc1096c4db9598d6ed99dc87286b31e4ce9f8a8da67", + "sha256:8fd2583024ff6cd5d4f842d446d001de4c4fe1264fdb5f28ddea28f6488866df", + "sha256:a0bc5776ac2831c022e029839bf1b9d3052332dcf5f431bb88c8503e27398e31", + "sha256:b2392cc41eeed4055978c6b52549ccd9effd263bb780ffd639c0e1e7e2055ab0", + "sha256:b7a7b7f2bad3ca72eb2fa14643f1c4ca11d115614047299d89bc24a3b11ddd09", + "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be", + "sha256:b99de16560097b9984409ded0032f101f9555e1ab029440fc6a8b5e76dbba7ac", + "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21", + "sha256:ce531d7c424ef327a391de7a9777a6c93a38e1f89e18efa903a1c4ba11f85905", + "sha256:d3f32d7c70b1c26844fd0e4e56a1da852b493e4e1c30df7b07274a1e5a9b599e", + "sha256:d97bc1be4bad83b70d8b8627ada6724091af41139616696e59b7088f358583b9", + "sha256:e61d426969b68b2170a9f853cc36d5318030494576e9ec0bfe2dc2e2afa15a68", + "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336", + "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821", + "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b" + ], + "markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))", + "version": "==3.2.0" + }, + "h11": { + "hashes": [ + "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", + "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" + ], + "markers": "python_version >= '3.7'", + "version": "==0.14.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "mako": { + "hashes": [ + "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", + "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.10" + }, + "markupsafe": { + "hashes": [ + "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", + "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", + "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", + "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", + "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", + "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", + "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", + "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", + "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", + "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", + "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", + "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", + "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", + "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", + "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", + "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", + "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", + "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", + "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", + "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", + "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", + "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", + "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", + "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", + "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", + "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", + "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", + "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", + "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", + "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", + "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", + "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", + "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", + "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", + "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", + "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", + "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", + "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", + "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", + "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", + "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", + "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", + "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", + "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", + "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", + "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", + "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", + "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", + "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", + "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", + "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", + "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", + "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", + "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", + "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", + "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", + "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", + "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", + "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", + "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", + "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" + ], + "markers": "python_version >= '3.9'", + "version": "==3.0.2" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9", + "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77", + "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e", + "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84", + "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3", + "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2", + "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67", + "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876", + "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152", + "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f", + "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a", + "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6", + "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503", + "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f", + "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493", + "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996", + "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f", + "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e", + "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59", + "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94", + "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7", + "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682", + "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420", + "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae", + "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291", + "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe", + "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980", + "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93", + "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692", + "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119", + "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716", + "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472", + "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b", + "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2", + "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc", + "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c", + "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5", + "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab", + "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984", + "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9", + "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf", + "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0", + "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f", + "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212", + "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb", + "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be", + "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90", + "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041", + "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7", + "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860", + "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d", + "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245", + "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27", + "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417", + "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359", + "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202", + "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0", + "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7", + "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba", + "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1", + "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd", + "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07", + "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98", + "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55", + "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d", + "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972", + "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f", + "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e", + "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26", + "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957", + "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53", + "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.9.9" + }, + "pydantic": { + "hashes": [ + "sha256:80c50fb8e3dcecfddae1adbcc00ec5822918490c99ab31f6cf6140ca1c1429f0", + "sha256:ff177ba64c6faf73d7afa2e8cad38fd456c0dbe01c9954e71038001cd15a6edd" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.5.2" + }, + "pydantic-core": { + "hashes": [ + "sha256:038c9f763e650712b899f983076ce783175397c848da04985658e7628cbe873b", + "sha256:074f3d86f081ce61414d2dc44901f4f83617329c6f3ab49d2bc6c96948b2c26b", + "sha256:079206491c435b60778cf2b0ee5fd645e61ffd6e70c47806c9ed51fc75af078d", + "sha256:09b0e985fbaf13e6b06a56d21694d12ebca6ce5414b9211edf6f17738d82b0f8", + "sha256:0f6116a558fd06d1b7c2902d1c4cf64a5bd49d67c3540e61eccca93f41418124", + "sha256:103ef8d5b58596a731b690112819501ba1db7a36f4ee99f7892c40da02c3e189", + "sha256:16e29bad40bcf97aac682a58861249ca9dcc57c3f6be22f506501833ddb8939c", + "sha256:206ed23aecd67c71daf5c02c3cd19c0501b01ef3cbf7782db9e4e051426b3d0d", + "sha256:2248485b0322c75aee7565d95ad0e16f1c67403a470d02f94da7344184be770f", + "sha256:27548e16c79702f1e03f5628589c6057c9ae17c95b4c449de3c66b589ead0520", + "sha256:2d0ae0d8670164e10accbeb31d5ad45adb71292032d0fdb9079912907f0085f4", + "sha256:3128e0bbc8c091ec4375a1828d6118bc20404883169ac95ffa8d983b293611e6", + "sha256:3387277f1bf659caf1724e1afe8ee7dbc9952a82d90f858ebb931880216ea955", + "sha256:34708cc82c330e303f4ce87758828ef6e457681b58ce0e921b6e97937dd1e2a3", + "sha256:35613015f0ba7e14c29ac6c2483a657ec740e5ac5758d993fdd5870b07a61d8b", + "sha256:3ad873900297bb36e4b6b3f7029d88ff9829ecdc15d5cf20161775ce12306f8a", + "sha256:40180930807ce806aa71eda5a5a5447abb6b6a3c0b4b3b1b1962651906484d68", + "sha256:439c9afe34638ace43a49bf72d201e0ffc1a800295bed8420c2a9ca8d5e3dbb3", + "sha256:45e95333b8418ded64745f14574aa9bfc212cb4fbeed7a687b0c6e53b5e188cd", + "sha256:4641e8ad4efb697f38a9b64ca0523b557c7931c5f84e0fd377a9a3b05121f0de", + "sha256:49b08aae5013640a3bfa25a8eebbd95638ec3f4b2eaf6ed82cf0c7047133f03b", + "sha256:4bc536201426451f06f044dfbf341c09f540b4ebdb9fd8d2c6164d733de5e634", + "sha256:4ce601907e99ea5b4adb807ded3570ea62186b17f88e271569144e8cca4409c7", + "sha256:4e40f2bd0d57dac3feb3a3aed50f17d83436c9e6b09b16af271b6230a2915459", + "sha256:4e47a76848f92529879ecfc417ff88a2806438f57be4a6a8bf2961e8f9ca9ec7", + "sha256:513b07e99c0a267b1d954243845d8a833758a6726a3b5d8948306e3fe14675e3", + "sha256:531f4b4252fac6ca476fbe0e6f60f16f5b65d3e6b583bc4d87645e4e5ddde331", + "sha256:57d52fa717ff445cb0a5ab5237db502e6be50809b43a596fb569630c665abddf", + "sha256:59986de5710ad9613ff61dd9b02bdd2f615f1a7052304b79cc8fa2eb4e336d2d", + "sha256:5baab5455c7a538ac7e8bf1feec4278a66436197592a9bed538160a2e7d11e36", + "sha256:5c7d5b5005f177764e96bd584d7bf28d6e26e96f2a541fdddb934c486e36fd59", + "sha256:60b7607753ba62cf0739177913b858140f11b8af72f22860c28eabb2f0a61937", + "sha256:615a0a4bff11c45eb3c1996ceed5bdaa2f7b432425253a7c2eed33bb86d80abc", + "sha256:61ea96a78378e3bd5a0be99b0e5ed00057b71f66115f5404d0dae4819f495093", + "sha256:652c1988019752138b974c28f43751528116bcceadad85f33a258869e641d753", + "sha256:6637560562134b0e17de333d18e69e312e0458ee4455bdad12c37100b7cad706", + "sha256:678265f7b14e138d9a541ddabbe033012a2953315739f8cfa6d754cc8063e8ca", + "sha256:699156034181e2ce106c89ddb4b6504c30db8caa86e0c30de47b3e0654543260", + "sha256:6b9ff467ffbab9110e80e8c8de3bcfce8e8b0fd5661ac44a09ae5901668ba997", + "sha256:6c327e9cd849b564b234da821236e6bcbe4f359a42ee05050dc79d8ed2a91588", + "sha256:6d30226dfc816dd0fdf120cae611dd2215117e4f9b124af8c60ab9093b6e8e71", + "sha256:6e227c40c02fd873c2a73a98c1280c10315cbebe26734c196ef4514776120aeb", + "sha256:6e4d090e73e0725b2904fdbdd8d73b8802ddd691ef9254577b708d413bf3006e", + "sha256:70f4b4851dbb500129681d04cc955be2a90b2248d69273a787dda120d5cf1f69", + "sha256:70f947628e074bb2526ba1b151cee10e4c3b9670af4dbb4d73bc8a89445916b5", + "sha256:774de879d212db5ce02dfbf5b0da9a0ea386aeba12b0b95674a4ce0593df3d07", + "sha256:77fa384d8e118b3077cccfcaf91bf83c31fe4dc850b5e6ee3dc14dc3d61bdba1", + "sha256:79e0a2cdbdc7af3f4aee3210b1172ab53d7ddb6a2d8c24119b5706e622b346d0", + "sha256:7e88f5696153dc516ba6e79f82cc4747e87027205f0e02390c21f7cb3bd8abfd", + "sha256:7f8210297b04e53bc3da35db08b7302a6a1f4889c79173af69b72ec9754796b8", + "sha256:81982d78a45d1e5396819bbb4ece1fadfe5f079335dd28c4ab3427cd95389944", + "sha256:823fcc638f67035137a5cd3f1584a4542d35a951c3cc68c6ead1df7dac825c26", + "sha256:853a2295c00f1d4429db4c0fb9475958543ee80cfd310814b5c0ef502de24dda", + "sha256:88e74ab0cdd84ad0614e2750f903bb0d610cc8af2cc17f72c28163acfcf372a4", + "sha256:8aa1768c151cf562a9992462239dfc356b3d1037cc5a3ac829bb7f3bda7cc1f9", + "sha256:8c8a8812fe6f43a3a5b054af6ac2d7b8605c7bcab2804a8a7d68b53f3cd86e00", + "sha256:95b15e855ae44f0c6341ceb74df61b606e11f1087e87dcb7482377374aac6abe", + "sha256:96581cfefa9123accc465a5fd0cc833ac4d75d55cc30b633b402e00e7ced00a6", + "sha256:9bd18fee0923ca10f9a3ff67d4851c9d3e22b7bc63d1eddc12f439f436f2aada", + "sha256:a33324437018bf6ba1bb0f921788788641439e0ed654b233285b9c69704c27b4", + "sha256:a6a16f4a527aae4f49c875da3cdc9508ac7eef26e7977952608610104244e1b7", + "sha256:a717aef6971208f0851a2420b075338e33083111d92041157bbe0e2713b37325", + "sha256:a71891847f0a73b1b9eb86d089baee301477abef45f7eaf303495cd1473613e4", + "sha256:aae7ea3a1c5bb40c93cad361b3e869b180ac174656120c42b9fadebf685d121b", + "sha256:ab1cdb0f14dc161ebc268c09db04d2c9e6f70027f3b42446fa11c153521c0e88", + "sha256:ab4ea451082e684198636565224bbb179575efc1658c48281b2c866bfd4ddf04", + "sha256:abf058be9517dc877227ec3223f0300034bd0e9f53aebd63cf4456c8cb1e0863", + "sha256:af36f36538418f3806048f3b242a1777e2540ff9efaa667c27da63d2749dbce0", + "sha256:b53e9ad053cd064f7e473a5f29b37fc4cc9dc6d35f341e6afc0155ea257fc911", + "sha256:b7851992faf25eac90bfcb7bfd19e1f5ffa00afd57daec8a0042e63c74a4551b", + "sha256:b9b759b77f5337b4ea024f03abc6464c9f35d9718de01cfe6bae9f2e139c397e", + "sha256:ba39688799094c75ea8a16a6b544eb57b5b0f3328697084f3f2790892510d144", + "sha256:ba6b6b3846cfc10fdb4c971980a954e49d447cd215ed5a77ec8190bc93dd7bc5", + "sha256:bb4c2eda937a5e74c38a41b33d8c77220380a388d689bcdb9b187cf6224c9720", + "sha256:c0b97ec434041827935044bbbe52b03d6018c2897349670ff8fe11ed24d1d4ab", + "sha256:c1452a1acdf914d194159439eb21e56b89aa903f2e1c65c60b9d874f9b950e5d", + "sha256:c2027d05c8aebe61d898d4cffd774840a9cb82ed356ba47a90d99ad768f39789", + "sha256:c2adbe22ab4babbca99c75c5d07aaf74f43c3195384ec07ccbd2f9e3bddaecec", + "sha256:c2d97e906b4ff36eb464d52a3bc7d720bd6261f64bc4bcdbcd2c557c02081ed2", + "sha256:c339dabd8ee15f8259ee0f202679b6324926e5bc9e9a40bf981ce77c038553db", + "sha256:c6eae413494a1c3f89055da7a5515f32e05ebc1a234c27674a6956755fb2236f", + "sha256:c949f04ecad823f81b1ba94e7d189d9dfb81edbb94ed3f8acfce41e682e48cef", + "sha256:c97bee68898f3f4344eb02fec316db93d9700fb1e6a5b760ffa20d71d9a46ce3", + "sha256:ca61d858e4107ce5e1330a74724fe757fc7135190eb5ce5c9d0191729f033209", + "sha256:cb4679d4c2b089e5ef89756bc73e1926745e995d76e11925e3e96a76d5fa51fc", + "sha256:cb774298da62aea5c80a89bd58c40205ab4c2abf4834453b5de207d59d2e1651", + "sha256:ccd4d5702bb90b84df13bd491be8d900b92016c5a455b7e14630ad7449eb03f8", + "sha256:cf9d3fe53b1ee360e2421be95e62ca9b3296bf3f2fb2d3b83ca49ad3f925835e", + "sha256:d2ae91f50ccc5810b2f1b6b858257c9ad2e08da70bf890dee02de1775a387c66", + "sha256:d37f8ec982ead9ba0a22a996129594938138a1503237b87318392a48882d50b7", + "sha256:d81e6987b27bc7d101c8597e1cd2bcaa2fee5e8e0f356735c7ed34368c471550", + "sha256:dcf4e6d85614f7a4956c2de5a56531f44efb973d2fe4a444d7251df5d5c4dcfd", + "sha256:de790a3b5aa2124b8b78ae5faa033937a72da8efe74b9231698b5a1dd9be3405", + "sha256:e47e9a08bcc04d20975b6434cc50bf82665fbc751bcce739d04a3120428f3e27", + "sha256:e60f112ac88db9261ad3a52032ea46388378034f3279c643499edb982536a093", + "sha256:e87fc540c6cac7f29ede02e0f989d4233f88ad439c5cdee56f693cc9c1c78077", + "sha256:eac5c82fc632c599f4639a5886f96867ffced74458c7db61bc9a66ccb8ee3113", + "sha256:ebb4e035e28f49b6f1a7032920bb9a0c064aedbbabe52c543343d39341a5b2a3", + "sha256:ec1e72d6412f7126eb7b2e3bfca42b15e6e389e1bc88ea0069d0cc1742f477c6", + "sha256:ef98ca7d5995a82f43ec0ab39c4caf6a9b994cb0b53648ff61716370eadc43cf", + "sha256:f0cbc7fff06a90bbd875cc201f94ef0ee3929dfbd5c55a06674b60857b8b85ed", + "sha256:f4791cf0f8c3104ac668797d8c514afb3431bc3305f5638add0ba1a5a37e0d88", + "sha256:f5e412d717366e0677ef767eac93566582518fe8be923361a5c204c1a62eaafe", + "sha256:fb2ed8b3fe4bf4506d6dab3b93b83bbc22237e230cba03866d561c3577517d18", + "sha256:fe0a5a1025eb797752136ac8b4fa21aa891e3d74fd340f864ff982d649691867" + ], + "markers": "python_version >= '3.7'", + "version": "==2.14.5" + }, + "python-dotenv": { + "hashes": [ + "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba", + "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.0.0" + }, + "python-multipart": { + "hashes": [ + "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132", + "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.0.6" + }, + "sniffio": { + "hashes": [ + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "sqlalchemy": { + "hashes": [ + "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3", + "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884", + "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74", + "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d", + "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc", + "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca", + "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d", + "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf", + "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846", + "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306", + "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221", + "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5", + "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89", + "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55", + "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72", + "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea", + "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8", + "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577", + "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df", + "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4", + "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d", + "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34", + "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4", + "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24", + "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6", + "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965", + "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35", + "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b", + "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab", + "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22", + "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4", + "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204", + "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855", + "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d", + "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab", + "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69", + "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693", + "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e", + "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8", + "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0", + "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45", + "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab", + "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1", + "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d", + "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda", + "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b", + "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18", + "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac", + "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.0.23" + }, + "starlette": { + "hashes": [ + "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75", + "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91" + ], + "markers": "python_version >= '3.7'", + "version": "==0.27.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", + "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" + ], + "markers": "python_version >= '3.8'", + "version": "==4.13.2" + }, + "uvicorn": { + "hashes": [ + "sha256:368d5d81520a51be96431845169c225d771c9dd22a58613e1a181e6c4512ac33", + "sha256:3d19f13dfd2c2af1bfe34dd0f7155118ce689425fdf931177abe832ca44b8a04" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.24.0" + } + }, + "develop": {} +} diff --git a/examples/music-licensing-challenge/README.md b/examples/music-licensing-challenge/README.md new file mode 100755 index 0000000..3714334 --- /dev/null +++ b/examples/music-licensing-challenge/README.md @@ -0,0 +1,90 @@ + +# Music Licensing Platform Backend + +This is the backend service for the Music Licensing Workflow application. It provides both REST and GraphQL APIs, along with WebSocket support for real-time updates. + +This application is built using **FastAPI**, **Strawberry GraphQL**, **SQLAlchemy**, **Pydantic**, and **PostgreSQL**. + +## Key Features + +* **REST API**: Comprehensive set of RESTful endpoints for CRUD operations on movies, songs, scenes, and licenses. +* **GraphQL API**: A flexible GraphQL API that allows for detailed queries and mutations, enhancing data retrieval and manipulation. +* **WebSocket Support**: Real-time updates for license statuses, ensuring that clients are always up-to-date with the latest changes. +* **Database Integration**: Robust PostgreSQL database to store and manage all application data. +* **Docker Support**: Containerized deployment with Docker and Docker Compose for easy setup and scalability. + +## Tech Stack + +* **Python 3.11**: The core programming language for the backend. +* **FastAPI**: High-performance web framework for building APIs. +* **Strawberry GraphQL**: Library for creating GraphQL APIs in Python. +* **SQLAlchemy**: Powerful ORM for database interaction. +* **Pydantic**: Data validation and settings management using Python type annotations. +* **PostgreSQL**: Robust relational database management system. +* **WebSockets**: For real-time communication and updates. + +## Project Structure + +The project is organized as follows: + +## Setup + +- Install dependencies: + +```bash +pip install -r requirements.txt +``` + +- Set up environment variables: + +```bash +cp env.example .env +# Edit .env with your configuration +``` + +- Run the application: + +```bash +uvicorn src.main:app --reload +``` + +## API Documentation + +- REST API: +- GraphQL Playground: + +## Docker + +To run the application using Docker: + +```bash +docker-compose up --build +``` + +## API Endpoints + +### REST API + +- `GET /api/movies` - List all movies +- `GET /api/movies/?id={id}` - Get movie details +- `GET /api/movies/scenes` - Get all scenes +- `GET /api/movies/scenes/?id={id}` - Get scene details + +### GraphQL + +#### Queries + +- allMovies: [Movie!]! +- movie(id: ID!): Movie +- scene(id: ID!): Scene +- allScenes: [Scene!]! +- allLicenseStatus: [LicenseStatus!]! + -song(id: ID!): Song + +#### Mutations + +- updateSong(id: ID!, licenseStatus: LicenseStatusEnum = null): Song + +### WebSocket + +- `ws://localhost:8000/api/graphql` - example WebSocket endpoint for real-time license status updates diff --git a/examples/music-licensing-challenge/docker-compose.yml b/examples/music-licensing-challenge/docker-compose.yml new file mode 100644 index 0000000..13f542c --- /dev/null +++ b/examples/music-licensing-challenge/docker-compose.yml @@ -0,0 +1,27 @@ +version: '3.8' + +services: + backend: + build: . + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql://postgres:postgres@db:5432/music_licensing + depends_on: + - db + volumes: + - ./:/app + + db: + image: postgres:15 + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=music_licensing + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: \ No newline at end of file diff --git a/examples/music-licensing-challenge/env.example b/examples/music-licensing-challenge/env.example new file mode 100755 index 0000000..4664d01 --- /dev/null +++ b/examples/music-licensing-challenge/env.example @@ -0,0 +1 @@ +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/music_licensing \ No newline at end of file diff --git a/examples/music-licensing-challenge/requirements.txt b/examples/music-licensing-challenge/requirements.txt new file mode 100755 index 0000000..cba36b1 --- /dev/null +++ b/examples/music-licensing-challenge/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.104.1 +uvicorn==0.24.0 +sqlalchemy==2.0.23 +psycopg2-binary==2.9.9 +python-dotenv==1.0.0 +pydantic==2.5.2 +python-multipart==0.0.6 +alembic==1.12.1 +strawberry-graphql==0.208.0 +websockets==11.0.3 \ No newline at end of file diff --git a/examples/music-licensing-challenge/src/app/api/__init__.py b/examples/music-licensing-challenge/src/app/api/__init__.py new file mode 100755 index 0000000..195357e --- /dev/null +++ b/examples/music-licensing-challenge/src/app/api/__init__.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter +from .movies import router as movies_router +from .scenes import router as scenes_router +from .graphql import router as graphql_app + +router = APIRouter() + +router.include_router(movies_router, prefix="/movies", tags=["movies"]) +router.include_router(scenes_router, prefix="/movies/scenes", tags=["scenes"]) +router.include_router(graphql_app, prefix="/graphql", tags=["graphql"]) diff --git a/examples/music-licensing-challenge/src/app/api/graphql.py b/examples/music-licensing-challenge/src/app/api/graphql.py new file mode 100644 index 0000000..db48c5e --- /dev/null +++ b/examples/music-licensing-challenge/src/app/api/graphql.py @@ -0,0 +1,4 @@ +from strawberry.fastapi import GraphQLRouter +from ..graphql.schema import schema + +router = GraphQLRouter(schema) diff --git a/examples/music-licensing-challenge/src/app/api/movies.py b/examples/music-licensing-challenge/src/app/api/movies.py new file mode 100755 index 0000000..a39eb74 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/api/movies.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List, Optional +from ..db.database import get_db +from ..models.movie import Movie +from ..schemas.movies import MovieWithAllData +from ..repository.movies import MovieRepository + +router = APIRouter() + + +@router.get("/", response_model=List[MovieWithAllData] | MovieWithAllData) +def read_movies( + id: Optional[str] = None, + db: Session = Depends(get_db), +): + if id is None: + movies = MovieRepository(db) + return movies.get_all_movies_with_details() + else: + movie = db.query(Movie).filter(Movie.id == id).first() + if movie is None: + raise HTTPException(status_code=404, detail="Movie not found") + return movie diff --git a/examples/music-licensing-challenge/src/app/api/scenes.py b/examples/music-licensing-challenge/src/app/api/scenes.py new file mode 100755 index 0000000..3b4c825 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/api/scenes.py @@ -0,0 +1,24 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List, Optional +from ..db.database import get_db +from ..models.scene import Scene +from ..schemas.scenes import SceneWithAllData +from ..repository.scenes import SceneRepository + +router = APIRouter() + + +@router.get("/", response_model=List[SceneWithAllData] | SceneWithAllData) +def read_scenes( + id: Optional[str] = None, + db: Session = Depends(get_db), +): + if id is None: + scenes = SceneRepository(db) + return scenes.get_all_scenes_with_details() + else: + scene = db.query(Scene).filter(Scene.id == id).first() + if scene is None: + raise HTTPException(status_code=404, detail="Scene not found") + return scene diff --git a/examples/music-licensing-challenge/src/app/db/database.py b/examples/music-licensing-challenge/src/app/db/database.py new file mode 100755 index 0000000..a930b16 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/db/database.py @@ -0,0 +1,19 @@ +import os +from dotenv import load_dotenv +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +load_dotenv() + +SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "") + +engine = create_engine(SQLALCHEMY_DATABASE_URL, future=True) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + + +def get_db(): + db = SessionLocal() + try: + return db + finally: + db.close() diff --git a/examples/music-licensing-challenge/src/app/graphql/__init__.py b/examples/music-licensing-challenge/src/app/graphql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/music-licensing-challenge/src/app/graphql/mutations.py b/examples/music-licensing-challenge/src/app/graphql/mutations.py new file mode 100644 index 0000000..8f98a17 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/mutations.py @@ -0,0 +1,28 @@ +from typing import Optional + +from sqlalchemy.orm import Session +from strawberry import ID, mutation, type + +from ..db.database import get_db +from ..repository.songs import SongRepository +from ..schemas.songs import LicenseStatusEnum +from .pubsub import trigger_license_change_subscription +from .types.song import Song + + +@type +class Mutations: + @mutation + async def update_song( + self, + id: ID, + license_status: Optional[LicenseStatusEnum] = None, + ) -> Optional[Song]: + db: Session = get_db() + song_repository = SongRepository(db) + song = song_repository.update_song(id, license_status) + if song is None: + return None + if license_status: + await trigger_license_change_subscription(song) + return Song.from_model(song) diff --git a/examples/music-licensing-challenge/src/app/graphql/pubsub.py b/examples/music-licensing-challenge/src/app/graphql/pubsub.py new file mode 100644 index 0000000..a2b8275 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/pubsub.py @@ -0,0 +1,10 @@ +import asyncio + +from .types.song import Song + +song_update_queue: asyncio.Queue[Song] = asyncio.Queue() + + +async def trigger_license_change_subscription(song_model): + song = Song.from_model(song_model) + await song_update_queue.put(song) diff --git a/examples/music-licensing-challenge/src/app/graphql/queries.py b/examples/music-licensing-challenge/src/app/graphql/queries.py new file mode 100644 index 0000000..80242bb --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/queries.py @@ -0,0 +1,52 @@ +from typing import List, Optional + +from strawberry import ID, field, type + +from ..db.database import get_db +from ..repository.licenses import LicenseRepository +from ..repository.movies import MovieRepository +from ..repository.scenes import SceneRepository +from ..repository.songs import SongRepository +from .types.movie import Movie +from .types.scene import Scene +from .types.song import Song +from .types.song import LicenseStatus + + +@type +class Query: + @field + def all_movies(self) -> List[Movie]: + db = get_db() + movies = MovieRepository(db).get_all_movies_with_details() + return [Movie.from_model(m) for m in movies] + + @field + def movie(self, id: ID) -> Optional[Movie]: + db = get_db() + movie = MovieRepository(db).get_movie_by_id_with_details(id) + return Movie.from_model(movie) if movie else None + + @field + def scene(self, id: ID) -> Optional[Scene]: + db = get_db() + scene = SceneRepository(db).get_scene_by_id_with_details(id) + return Scene.from_model(scene) if scene else None + + @field + def all_scenes(self) -> List[Scene]: + db = get_db() + scenes = SceneRepository(db).get_all_scenes_with_details() + return [Scene.from_model(s) for s in scenes] + + @field + def all_license_status(self) -> List[LicenseStatus]: + db = get_db() + licenses = LicenseRepository(db).get_all_licenses() + return [LicenseStatus.from_model(s) for s in licenses] + + @field + def song(self, id: ID) -> Optional[Song]: + db = get_db() + song = SongRepository(db).get_song_by_id(id) + return Song.from_model(song) if song else None \ No newline at end of file diff --git a/examples/music-licensing-challenge/src/app/graphql/schema.py b/examples/music-licensing-challenge/src/app/graphql/schema.py new file mode 100644 index 0000000..87f84ed --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/schema.py @@ -0,0 +1,7 @@ +import strawberry + +from .mutations import Mutations +from .queries import Query +from .subscriptions import Subscription + +schema = strawberry.Schema(query=Query, mutation=Mutations, subscription=Subscription) diff --git a/examples/music-licensing-challenge/src/app/graphql/subscriptions.py b/examples/music-licensing-challenge/src/app/graphql/subscriptions.py new file mode 100644 index 0000000..756b3c6 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/subscriptions.py @@ -0,0 +1,15 @@ +from typing import AsyncGenerator + +import strawberry + +from .pubsub import song_update_queue +from .types.song import Song + + +@strawberry.type +class Subscription: + @strawberry.subscription + async def license_changed(self) -> AsyncGenerator[Song, None]: + while True: + song = await song_update_queue.get() + yield song diff --git a/examples/music-licensing-challenge/src/app/graphql/types/__init__.py b/examples/music-licensing-challenge/src/app/graphql/types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/music-licensing-challenge/src/app/graphql/types/genre.py b/examples/music-licensing-challenge/src/app/graphql/types/genre.py new file mode 100644 index 0000000..4a34fa4 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/types/genre.py @@ -0,0 +1,16 @@ +import strawberry + +from ...models.genre import Genre as GenreModel + + +@strawberry.type +class Genre: + id: int + name: str + + @classmethod + def from_model(cls, model: GenreModel) -> "Genre": + return cls( + id=model.id, + name=model.name, + ) diff --git a/examples/music-licensing-challenge/src/app/graphql/types/movie.py b/examples/music-licensing-challenge/src/app/graphql/types/movie.py new file mode 100644 index 0000000..186a72a --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/types/movie.py @@ -0,0 +1,32 @@ +from typing import List + +import strawberry + +from ...models.movie import Movie as MovieModel +from .genre import Genre +from .scene import Scene + + +@strawberry.type +class Movie: + id: str + title: str + year: int + director: str + description: str + poster: str + genres: List[Genre] + scenes: List[Scene] + + @classmethod + def from_model(cls, model: MovieModel) -> "Movie": + return cls( + id=model.id, + title=model.title, + year=model.year, + director=model.director, + description=model.description, + poster=model.poster, + genres=[Genre.from_model(g) for g in model.genres], + scenes=[Scene.from_model(s) for s in model.scenes], + ) diff --git a/examples/music-licensing-challenge/src/app/graphql/types/scene.py b/examples/music-licensing-challenge/src/app/graphql/types/scene.py new file mode 100644 index 0000000..a616353 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/types/scene.py @@ -0,0 +1,24 @@ +from typing import List + +import strawberry + +from ...models.scene import Scene as SceneModel +from .track import Track + +@strawberry.type +class Scene: + id: str + movie_id: str + scene_number: int + description: str + tracks: List[Track] + + @classmethod + def from_model(cls, model: SceneModel) -> "Scene": + return cls( + id=model.id, + movie_id=model.movie_id, + scene_number=model.scene_number, + description=model.description, + tracks=[Track.from_model(t) for t in model.tracks], + ) diff --git a/examples/music-licensing-challenge/src/app/graphql/types/song.py b/examples/music-licensing-challenge/src/app/graphql/types/song.py new file mode 100644 index 0000000..c38abb4 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/types/song.py @@ -0,0 +1,32 @@ +from typing import Optional + +import strawberry + +from ...models.song import Song as SongModel + + +@strawberry.type +class LicenseStatus: + id: int + status: str + + @classmethod + def from_model(cls, model): + return cls(id=model.id, status=model.name) + + +@strawberry.type +class Song: + id: str + title: str + artist: Optional[str] + license_status: str + + @classmethod + def from_model(cls, model: SongModel) -> "Song": + return cls( + id=model.id, + title=model.title, + artist=model.artist, + license_status=model.license_status.name if model.license_status else None, + ) diff --git a/examples/music-licensing-challenge/src/app/graphql/types/track.py b/examples/music-licensing-challenge/src/app/graphql/types/track.py new file mode 100644 index 0000000..d2df7ef --- /dev/null +++ b/examples/music-licensing-challenge/src/app/graphql/types/track.py @@ -0,0 +1,21 @@ +from typing import List + +import strawberry + +from ...models.track import Track as TrackModel +from .song import Song + + +@strawberry.type +class Track: + id: str + track_type: str + songs: List[Song] + + @classmethod + def from_model(cls, model: TrackModel) -> "Track": + return cls( + id=model.id, + track_type=model.track_type, + songs=[Song.from_model(song) for song in model.songs], + ) diff --git a/examples/music-licensing-challenge/src/app/main.py b/examples/music-licensing-challenge/src/app/main.py new file mode 100755 index 0000000..3acf8cb --- /dev/null +++ b/examples/music-licensing-challenge/src/app/main.py @@ -0,0 +1,25 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from .api import router as api_router + +app = FastAPI( + title="Music Licensing API", + description="API for managing music licensing workflow", + version="1.0.0", +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(api_router, prefix="/api") + + +@app.get("/") +async def root(): + return {"message": "Welcome to Music Licensing API"} diff --git a/examples/music-licensing-challenge/src/app/models/__init__.py b/examples/music-licensing-challenge/src/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/music-licensing-challenge/src/app/models/associations.py b/examples/music-licensing-challenge/src/app/models/associations.py new file mode 100644 index 0000000..9ab0f8a --- /dev/null +++ b/examples/music-licensing-challenge/src/app/models/associations.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, ForeignKey, Integer, String, Table + +from .database import Base + +movie_genres_table = Table( + "movie_genres", + Base.metadata, + Column("movie_id", String, ForeignKey("movies.id"), primary_key=True), + Column("genre_id", Integer, ForeignKey("genres.id"), primary_key=True), +) + +track_songs_table = Table( + "track_songs", + Base.metadata, + Column("track_id", Integer, ForeignKey("tracks.id"), primary_key=True), + Column("song_id", Integer, ForeignKey("songs.id"), primary_key=True), +) \ No newline at end of file diff --git a/examples/music-licensing-challenge/src/app/models/database.py b/examples/music-licensing-challenge/src/app/models/database.py new file mode 100644 index 0000000..7c2377a --- /dev/null +++ b/examples/music-licensing-challenge/src/app/models/database.py @@ -0,0 +1,3 @@ +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() \ No newline at end of file diff --git a/examples/music-licensing-challenge/src/app/models/genre.py b/examples/music-licensing-challenge/src/app/models/genre.py new file mode 100644 index 0000000..105376c --- /dev/null +++ b/examples/music-licensing-challenge/src/app/models/genre.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship + +from .associations import movie_genres_table +from .database import Base + + +class Genre(Base): + __tablename__ = "genres" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, unique=True, index=True) + + movies = relationship( + "Movie", secondary=movie_genres_table, back_populates="genres" + ) diff --git a/examples/music-licensing-challenge/src/app/models/licenses.py b/examples/music-licensing-challenge/src/app/models/licenses.py new file mode 100644 index 0000000..a6cc37f --- /dev/null +++ b/examples/music-licensing-challenge/src/app/models/licenses.py @@ -0,0 +1,11 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship + +from .database import Base + + +class LicenseStatus(Base): + __tablename__ = "license_statuses" + id = Column(Integer, primary_key=True, index=True) + name = Column(String, unique=True, nullable=False) + songs = relationship("Song", back_populates="license_status") diff --git a/examples/music-licensing-challenge/src/app/models/movie.py b/examples/music-licensing-challenge/src/app/models/movie.py new file mode 100644 index 0000000..99019ef --- /dev/null +++ b/examples/music-licensing-challenge/src/app/models/movie.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, Integer, String +from sqlalchemy.orm import relationship + +from .associations import movie_genres_table +from .database import Base + + +class Movie(Base): + __tablename__ = "movies" + + id = Column(String, primary_key=True, index=True) + title = Column(String, index=True) + year = Column(Integer) + director = Column(String, nullable=True) + description = Column(String, nullable=True) + poster = Column(String, nullable=True) + + genres = relationship( + "Genre", secondary=movie_genres_table, back_populates="movies" + ) + scenes = relationship("Scene", back_populates="movie", cascade="all, delete-orphan") diff --git a/examples/music-licensing-challenge/src/app/models/scene.py b/examples/music-licensing-challenge/src/app/models/scene.py new file mode 100644 index 0000000..a5f3223 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/models/scene.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .database import Base + + +class Scene(Base): + __tablename__ = "scenes" + + id = Column(Integer, primary_key=True, index=True) + movie_id = Column(Integer, ForeignKey("movies.id")) + scene_number = Column(Integer) + description = Column(String, nullable=True) + + movie = relationship("Movie", back_populates="scenes") + tracks = relationship("Track", back_populates="scene", cascade="all, delete-orphan") diff --git a/examples/music-licensing-challenge/src/app/models/song.py b/examples/music-licensing-challenge/src/app/models/song.py new file mode 100644 index 0000000..5ab4683 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/models/song.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .associations import track_songs_table +from .database import Base + + +class Song(Base): + __tablename__ = "songs" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, nullable=True) + artist = Column(String, nullable=True) + license_status_id = Column( + Integer, ForeignKey("license_statuses.id"), nullable=False + ) + + tracks = relationship("Track", secondary=track_songs_table, back_populates="songs") + license_status = relationship("LicenseStatus", back_populates="songs") diff --git a/examples/music-licensing-challenge/src/app/models/track.py b/examples/music-licensing-challenge/src/app/models/track.py new file mode 100644 index 0000000..e1733fa --- /dev/null +++ b/examples/music-licensing-challenge/src/app/models/track.py @@ -0,0 +1,16 @@ +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship + +from .associations import track_songs_table +from .database import Base + + +class Track(Base): + __tablename__ = "tracks" + + id = Column(Integer, primary_key=True, index=True) + scene_id = Column(Integer, ForeignKey("scenes.id")) + track_type = Column(String, nullable=True) + + scene = relationship("Scene", back_populates="tracks") + songs = relationship("Song", secondary=track_songs_table, back_populates="tracks") diff --git a/examples/music-licensing-challenge/src/app/repository/licenses.py b/examples/music-licensing-challenge/src/app/repository/licenses.py new file mode 100644 index 0000000..edb764d --- /dev/null +++ b/examples/music-licensing-challenge/src/app/repository/licenses.py @@ -0,0 +1,18 @@ +from typing import List + +from sqlalchemy.future import select +from sqlalchemy.orm import Session + +from ..models.licenses import LicenseStatus + + +class LicenseRepository: + def __init__(self, db: Session): + self.db = db + + def get_all_licenses(self) -> List[LicenseStatus]: + """ + Retrieves all license status data. + """ + result = self.db.execute(select(LicenseStatus)) + return result.unique().scalars().all() diff --git a/examples/music-licensing-challenge/src/app/repository/movies.py b/examples/music-licensing-challenge/src/app/repository/movies.py new file mode 100644 index 0000000..d2b95ac --- /dev/null +++ b/examples/music-licensing-challenge/src/app/repository/movies.py @@ -0,0 +1,43 @@ +from typing import List + +from sqlalchemy.future import select +from sqlalchemy.orm import Session, joinedload, selectinload + +from ..models.movie import Movie +from ..models.scene import Scene +from ..models.track import Track + + +class MovieRepository: + def __init__(self, db: Session): + self.db = db + + def get_all_movies_with_details(self) -> List[Movie]: + """ + Retrieves all movies with their associated genres, scenes, tracks, and songs. + """ + result = self.db.execute( + select(Movie).options( + joinedload(Movie.genres), + selectinload(Movie.scenes) + .subqueryload(Scene.tracks) + .subqueryload(Track.songs), + ) + ) + return result.unique().scalars().all() + + def get_movie_by_id_with_details(self, movie_id: str) -> Movie | None: + """ + Retrieves a specific movie by ID with all its associated details. + """ + result = self.db.execute( + select(Movie) + .where(Movie.id == movie_id) + .options( + joinedload(Movie.genres), + selectinload(Movie.scenes) + .subqueryload(Scene.tracks) + .subqueryload(Track.songs), + ) + ) + return result.unique().scalar_one_or_none() diff --git a/examples/music-licensing-challenge/src/app/repository/scenes.py b/examples/music-licensing-challenge/src/app/repository/scenes.py new file mode 100644 index 0000000..b431dee --- /dev/null +++ b/examples/music-licensing-challenge/src/app/repository/scenes.py @@ -0,0 +1,30 @@ +from typing import List + +from sqlalchemy.future import select +from sqlalchemy.orm import Session, selectinload + +from ..models.scene import Scene +from ..models.track import Track + + +class SceneRepository: + def __init__(self, db: Session): + self.db = db + + def get_all_scenes_with_details(self) -> List[Scene]: + result = self.db.execute( + select(Scene).options( + selectinload(Scene.tracks).subqueryload(Track.songs), + ) + ) + return result.unique().scalars().all() + + def get_scene_by_id_with_details(self, scene_id: str) -> Scene | None: + result = self.db.execute( + select(Scene) + .where(Scene.id == scene_id) + .options( + selectinload(Scene.tracks).subqueryload(Track.songs), + ) + ) + return result.unique().scalar_one_or_none() diff --git a/examples/music-licensing-challenge/src/app/repository/songs.py b/examples/music-licensing-challenge/src/app/repository/songs.py new file mode 100644 index 0000000..0b56773 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/repository/songs.py @@ -0,0 +1,41 @@ +from typing import Optional + +from sqlalchemy.orm import Session + +from ..models.licenses import LicenseStatus +from ..models.song import Song +from ..schemas.songs import LicenseStatusEnum + + +class SongRepository: + def __init__(self, db: Session): + self.db = db + + def get_song_by_id(self, song_id: str) -> Optional[Song]: + return self.db.query(Song).filter(Song.id == song_id).first() + + def update_song( + self, + song_id: str, + license_status: Optional[LicenseStatusEnum], + ) -> Optional[Song]: + song = self.get_song_by_id(song_id) + if song: + if license_status is not None: + db_license_status = ( + self.db.query(LicenseStatus) + .filter(LicenseStatus.name == license_status.value) + .first() + ) + if db_license_status: + song.license_status = db_license_status + else: + print( + f"Warning: License status '{license_status.value}' not found in database." + ) + return None + + self.db.commit() + self.db.refresh(song) + return song + return None diff --git a/examples/music-licensing-challenge/src/app/schemas/movies.py b/examples/music-licensing-challenge/src/app/schemas/movies.py new file mode 100644 index 0000000..8db7fc5 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/schemas/movies.py @@ -0,0 +1,33 @@ +from typing import List, Optional + +from pydantic import BaseModel, ConfigDict, Field + +from .scenes import Scene + + +class Genre(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: int = Field(...) + name: str = Field(...) + + +class MovieBase(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + title: str = Field(...) + year: int = Field(...) + director: Optional[str] = Field(None) + description: Optional[str] = Field(None) + poster: Optional[str] = Field(None) + + +class MovieWithAllData(MovieBase): + model_config = ConfigDict( + populate_by_name=True, + ) + id: str = Field(...) + genres: List[Genre] = Field(...) + scenes: List[Scene] = Field(...) diff --git a/examples/music-licensing-challenge/src/app/schemas/scenes.py b/examples/music-licensing-challenge/src/app/schemas/scenes.py new file mode 100644 index 0000000..0fc0d00 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/schemas/scenes.py @@ -0,0 +1,47 @@ +from typing import List, Optional + +from pydantic import BaseModel, ConfigDict, Field + +from .songs import Song + + +class Track(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: int = Field(...) + track_type: Optional[str] = Field( + None, + alias="trackType", + ) + songs: List[Song] = Field(...) + + +class Scene(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: int = Field(...) + scene_number: int = Field( + ..., + alias="sceneNumber", + ) + description: Optional[str] = Field(None) + tracks: List[Track] = Field(...) + + +class SceneBase(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + scene_number: int = Field(..., alias="sceneNumber") + movie_id: str = Field(..., alias="movieId") + description: Optional[str] = Field(None) + + +class SceneWithAllData(SceneBase): + model_config = ConfigDict( + populate_by_name=True, + ) + id: int = Field(...) + tracks: List[Track] = Field(...) diff --git a/examples/music-licensing-challenge/src/app/schemas/songs.py b/examples/music-licensing-challenge/src/app/schemas/songs.py new file mode 100644 index 0000000..9137d50 --- /dev/null +++ b/examples/music-licensing-challenge/src/app/schemas/songs.py @@ -0,0 +1,34 @@ +import enum +from typing import Optional + +import strawberry +from pydantic import BaseModel, ConfigDict, Field + + +@strawberry.enum +class LicenseStatusEnum(enum.Enum): + NOT_LICENSED = "NOT_LICENSED" + PENDING = "PENDING" + LICENSED = "LICENSED" + + +class LicenseStatus(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + from_attributes=True, + ) + id: int = Field(...) + name: str = Field(...) + + +class Song(BaseModel): + model_config = ConfigDict( + populate_by_name=True, + ) + id: int = Field(...) + title: str = Field(...) + artist: Optional[str] = Field(None) + license_status: Optional[LicenseStatus] = Field( + None, + alias="licenseStatus", + )