commit 0b27ef2b30df1c839841b219e81e6e3008c9c095 Author: jimmy Date: Fri May 23 22:56:59 2025 +1200 Initial working version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..2a39abb --- /dev/null +++ b/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +fastapi = "*" +uvicorn = "*" +jinja2 = "*" + +[dev-packages] + +[requires] +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..28d6def --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,300 @@ +{ + "_meta": { + "hash": { + "sha256": "12e13c349bc30c36612e452e3ee8b8f2f05b156bb788515a14a94f70aed33cf2" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.12" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "annotated-types": { + "hashes": [ + "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", + "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89" + ], + "markers": "python_version >= '3.8'", + "version": "==0.7.0" + }, + "anyio": { + "hashes": [ + "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", + "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c" + ], + "markers": "python_version >= '3.9'", + "version": "==4.9.0" + }, + "click": { + "hashes": [ + "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", + "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b" + ], + "markers": "python_version >= '3.10'", + "version": "==8.2.1" + }, + "fastapi": { + "hashes": [ + "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", + "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==0.115.12" + }, + "h11": { + "hashes": [ + "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", + "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86" + ], + "markers": "python_version >= '3.8'", + "version": "==0.16.0" + }, + "idna": { + "hashes": [ + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + ], + "markers": "python_version >= '3.6'", + "version": "==3.10" + }, + "jinja2": { + "hashes": [ + "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", + "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==3.1.6" + }, + "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" + }, + "pydantic": { + "hashes": [ + "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", + "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7" + ], + "markers": "python_version >= '3.9'", + "version": "==2.11.5" + }, + "pydantic-core": { + "hashes": [ + "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d", + "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac", + "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02", + "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", + "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4", + "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22", + "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", + "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec", + "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d", + "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b", + "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", + "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", + "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052", + "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab", + "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", + "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c", + "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf", + "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27", + "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", + "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8", + "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7", + "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612", + "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1", + "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039", + "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca", + "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", + "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a", + "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6", + "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782", + "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b", + "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7", + "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", + "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", + "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7", + "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", + "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa", + "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", + "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", + "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", + "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51", + "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e", + "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", + "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65", + "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2", + "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954", + "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b", + "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de", + "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", + "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64", + "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb", + "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9", + "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101", + "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d", + "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef", + "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3", + "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", + "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", + "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", + "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d", + "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", + "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e", + "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", + "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808", + "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc", + "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d", + "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", + "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e", + "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640", + "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30", + "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e", + "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", + "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a", + "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", + "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", + "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb", + "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", + "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", + "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d", + "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572", + "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593", + "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29", + "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535", + "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", + "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f", + "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8", + "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf", + "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246", + "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", + "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", + "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9", + "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a", + "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3", + "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", + "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8", + "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a", + "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", + "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c", + "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", + "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d" + ], + "markers": "python_version >= '3.9'", + "version": "==2.33.2" + }, + "sniffio": { + "hashes": [ + "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", + "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "starlette": { + "hashes": [ + "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", + "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5" + ], + "markers": "python_version >= '3.9'", + "version": "==0.46.2" + }, + "typing-extensions": { + "hashes": [ + "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", + "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef" + ], + "markers": "python_version >= '3.8'", + "version": "==4.13.2" + }, + "typing-inspection": { + "hashes": [ + "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", + "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28" + ], + "markers": "python_version >= '3.9'", + "version": "==0.4.1" + }, + "uvicorn": { + "hashes": [ + "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", + "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403" + ], + "index": "pypi", + "markers": "python_version >= '3.9'", + "version": "==0.34.2" + } + }, + "develop": {} +} diff --git a/api/init.py b/api/init.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..e56b40d --- /dev/null +++ b/api/models.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel + +class ColorUpdate(BaseModel): + barId: str + color: str + +class PositionUpdate(BaseModel): + barId: str + x: int + y: int + +class CreateBar(BaseModel): + barId: str + url: str + color: str + x: int + y: int + +class DeleteBar(BaseModel): + barId: str diff --git a/api/routes.py b/api/routes.py new file mode 100644 index 0000000..a40dd23 --- /dev/null +++ b/api/routes.py @@ -0,0 +1,57 @@ +from fastapi import APIRouter, HTTPException +from .models import ColorUpdate, PositionUpdate, CreateBar, DeleteBar +from .settings_manager import SettingsManager + +router = APIRouter(prefix="/api") +settings_manager = SettingsManager() + +@router.get("/settings") +async def get_settings(): + try: + return settings_manager.load_settings() + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error reading settings: {str(e)}") + +@router.post("/settings/color") +async def update_color(color_update: ColorUpdate): + try: + settings_manager.update_color(color_update.barId, color_update.color) + return {"success": True, "message": f"Color updated for {color_update.barId}"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error updating settings: {str(e)}") + +@router.post("/settings/position") +async def update_position(position_update: PositionUpdate): + try: + settings_manager.update_position(position_update.barId, position_update.x, position_update.y) + return {"success": True, "message": f"Position updated for {position_update.barId}"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error updating settings: {str(e)}") + +@router.post("/settings/create") +async def create_bar(create_bar: CreateBar): + try: + if settings_manager.bar_exists(create_bar.barId): + raise HTTPException(status_code=400, detail=f"Bar {create_bar.barId} already exists") + + settings_manager.create_bar( + create_bar.barId, + create_bar.url, + create_bar.color, + create_bar.x, + create_bar.y + ) + return {"success": True, "message": f"Bar {create_bar.barId} created"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error creating bar: {str(e)}") + +@router.delete("/settings/delete") +async def delete_bar(delete_bar: DeleteBar): + try: + if not settings_manager.bar_exists(delete_bar.barId): + raise HTTPException(status_code=404, detail=f"Bar {delete_bar.barId} not found") + + settings_manager.delete_bar(delete_bar.barId) + return {"success": True, "message": f"Bar {delete_bar.barId} deleted"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Error deleting bar: {str(e)}") diff --git a/api/settings_manager.py b/api/settings_manager.py new file mode 100644 index 0000000..e1f0793 --- /dev/null +++ b/api/settings_manager.py @@ -0,0 +1,51 @@ +import json +import os +from typing import Dict, Any + +class SettingsManager: + def __init__(self, settings_file: str = "settings.json"): + self.settings_file = settings_file + + def load_settings(self) -> Dict[str, Any]: + try: + with open(self.settings_file, "r") as f: + return json.load(f) + except FileNotFoundError: + return {} + + def save_settings(self, settings: Dict[str, Any]) -> None: + with open(self.settings_file, "w") as f: + json.dump(settings, f, indent=2) + + def update_color(self, bar_id: str, color: str) -> None: + settings = self.load_settings() + if bar_id in settings: + settings[bar_id]["color"] = color + self.save_settings(settings) + + def update_position(self, bar_id: str, x: int, y: int) -> None: + settings = self.load_settings() + if bar_id in settings: + settings[bar_id]["x"] = x + settings[bar_id]["y"] = y + self.save_settings(settings) + + def create_bar(self, bar_id: str, url: str, color: str, x: int, y: int) -> None: + settings = self.load_settings() + settings[bar_id] = { + "url": url, + "color": color, + "x": x, + "y": y + } + self.save_settings(settings) + + def delete_bar(self, bar_id: str) -> None: + settings = self.load_settings() + if bar_id in settings: + del settings[bar_id] + self.save_settings(settings) + + def bar_exists(self, bar_id: str) -> bool: + settings = self.load_settings() + return bar_id in settings diff --git a/main.py b/main.py new file mode 100644 index 0000000..ea139e9 --- /dev/null +++ b/main.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI, Request, HTTPException +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from api.routes import router +import uvicorn + +app = FastAPI(title="Bar Control System", version="1.0.0") + +app.mount("/static", StaticFiles(directory="static"), name="static") +templates = Jinja2Templates(directory="templates") + +app.include_router(router) + +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + return templates.TemplateResponse("index.html", {"request": request}) + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000, reload=True) diff --git a/settings.json b/settings.json new file mode 100644 index 0000000..b51d69d --- /dev/null +++ b/settings.json @@ -0,0 +1,20 @@ +{ + "dj": { + "url": "ws://192.168.4.1/ws", + "color": "#1d011e", + "x": 614, + "y": 123 + }, + "bar2": { + "url": "ws://192.168.4.1/ws", + "color": "#3d0f00", + "x": 616, + "y": 330 + }, + "bar5": { + "url": "ws://192.168.4.1/ws", + "color": "#300d2a", + "x": 618, + "y": 567 + } +} \ No newline at end of file diff --git a/static/ApiService.js b/static/ApiService.js new file mode 100644 index 0000000..ffee82a --- /dev/null +++ b/static/ApiService.js @@ -0,0 +1,83 @@ +export class ApiService { + static async loadSettings() { + try { + const response = await fetch("/api/settings"); + const settings = await response.json(); + console.log("Settings loaded:", settings); + return settings; + } catch (error) { + console.error("Failed to load settings:", error); + return {}; + } + } + + static async createBar(barData) { + const response = await fetch("/api/settings/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(barData), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail); + } + + return response.json(); + } + + static async deleteBar(barId) { + const response = await fetch("/api/settings/delete", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ barId }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail); + } + + return response.json(); + } + + static async saveColor(barId, color) { + try { + const response = await fetch("/api/settings/color", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ barId, color }), + }); + + if (response.ok) { + console.log(`Color saved for ${barId}: ${color}`); + } + } catch (error) { + console.error("Failed to save color:", error); + } + } + + static async savePosition(barId, x, y) { + try { + const response = await fetch("/api/settings/position", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ barId, x, y }), + }); + + if (response.ok) { + console.log(`Position saved for ${barId}: ${x}, ${y}`); + } + } catch (error) { + console.error("Failed to save position:", error); + } + } +} diff --git a/static/BarControlSystem.js b/static/BarControlSystem.js new file mode 100644 index 0000000..eb8b1ea --- /dev/null +++ b/static/BarControlSystem.js @@ -0,0 +1,270 @@ +import { ApiService } from "./ApiService.js"; +import { DialogManager } from "./DialogManager.js"; +import { throttle, createStyledElement } from "./utils.js"; + +export class BarControlSystem { + constructor() { + this.settings = {}; + this.websockets = {}; + this.init(); + } + + async init() { + await this.loadSettings(); + this.createControlPanel(); + this.renderColorPickers(); + } + + async loadSettings() { + this.settings = await ApiService.loadSettings(); + } + + createControlPanel() { + const panel = createStyledElement("div", { + position: "fixed", + top: "10px", + right: "10px", + padding: "15px", + border: "2px solid #333", + borderRadius: "8px", + backgroundColor: "#f0f0f0", + zIndex: "1000", + }); + panel.id = "control-panel"; + + const title = createStyledElement( + "h3", + { + margin: "0 0 10px 0", + fontFamily: "Arial, sans-serif", + }, + { textContent: "Control Panel" }, + ); + + const buttonStyles = { + padding: "8px 12px", + border: "none", + borderRadius: "4px", + cursor: "pointer", + color: "white", + }; + + const createButton = createStyledElement( + "button", + { + ...buttonStyles, + marginRight: "10px", + backgroundColor: "#4CAF50", + }, + { textContent: "Create New Bar" }, + ); + + const refreshButton = createStyledElement( + "button", + { + ...buttonStyles, + backgroundColor: "#2196F3", + }, + { textContent: "Refresh" }, + ); + + createButton.onclick = () => this.showCreateDialog(); + refreshButton.onclick = () => this.refresh(); + + panel.append(title, createButton, refreshButton); + document.body.appendChild(panel); + } + + showCreateDialog() { + DialogManager.showCreateDialog((barData) => this.createBar(barData)); + } + + async createBar(barData) { + if (!barData.barId) { + alert("Bar ID is required"); + return; + } + + try { + await ApiService.createBar(barData); + console.log(`Bar created: ${barData.barId}`); + await this.refresh(); + } catch (error) { + console.error("Failed to create bar:", error); + alert(error.message || "Failed to create bar"); + } + } + + async deleteBar(barId) { + if (!confirm(`Are you sure you want to delete bar ${barId}?`)) { + return; + } + + try { + await ApiService.deleteBar(barId); + console.log(`Bar deleted: ${barId}`); + await this.refresh(); + } catch (error) { + console.error("Failed to delete bar:", error); + alert(error.message || "Failed to delete bar"); + } + } + + async refresh() { + // Close existing websockets + Object.values(this.websockets).forEach((ws) => { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.close(); + } + }); + + // Remove existing color pickers + Object.keys(this.settings).forEach((barId) => { + const form = document.getElementById("color_form_" + barId); + if (form) { + form.remove(); + } + }); + + this.websockets = {}; + await this.loadSettings(); + this.renderColorPickers(); + } + + renderColorPickers() { + Object.keys(this.settings).forEach((barId) => { + const config = this.settings[barId]; + this.createColorPicker(barId, config); + }); + } + + makeDraggable(element, barId) { + let isDragging = false; + let startX, startY, initialX, initialY; + + element.addEventListener("mousedown", (e) => { + if (e.target.tagName === "BUTTON") return; + + isDragging = true; + startX = e.clientX; + startY = e.clientY; + + const rect = element.getBoundingClientRect(); + initialX = rect.left; + initialY = rect.top; + + element.style.cursor = "move"; + e.preventDefault(); + }); + + document.addEventListener("mousemove", (e) => { + if (!isDragging) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + const newX = initialX + deltaX; + const newY = initialY + deltaY; + + element.style.left = newX + "px"; + element.style.top = newY + "px"; + }); + + document.addEventListener("mouseup", () => { + if (isDragging) { + const rect = element.getBoundingClientRect(); + ApiService.savePosition(barId, rect.left, rect.top); + element.style.cursor = "default"; + } + isDragging = false; + }); + } + + createColorPicker(barId, config) { + const ws = new WebSocket(config["url"]); + this.websockets[barId] = ws; + + const form = createStyledElement("form", { + position: "absolute", + left: config["x"] + "px", + top: config["y"] + "px", + padding: "10px", + border: "1px solid #ccc", + borderRadius: "5px", + backgroundColor: "white", + }); + form.id = "color_form_" + barId; + + const label = createStyledElement( + "label", + { + display: "block", + marginBottom: "5px", + fontFamily: "Arial, sans-serif", + fontSize: "14px", + }, + { + htmlFor: "color_input_" + barId, + textContent: barId, + }, + ); + + const colorInput = createStyledElement( + "input", + {}, + { + type: "color", + id: "color_input_" + barId, + name: barId, + value: config["color"], + }, + ); + + const deleteButton = createStyledElement( + "button", + { + position: "absolute", + top: "2px", + right: "2px", + width: "20px", + height: "20px", + backgroundColor: "#f44336", + color: "white", + border: "none", + borderRadius: "50%", + cursor: "pointer", + fontSize: "12px", + lineHeight: "1", + }, + { + type: "button", + textContent: "×", + }, + ); + + deleteButton.onclick = () => this.deleteBar(barId); + + const throttledColorHandler = throttle((event) => { + const color = event.target.value; + console.log(`Color selected for ${barId}: ${color}`); + + ApiService.saveColor(barId, color); + + if (ws.readyState === WebSocket.OPEN) { + const message = { color1: color }; + ws.send(JSON.stringify(message)); + } else { + console.warn( + `WebSocket not ready for ${barId}. ReadyState: ${ws.readyState}`, + ); + } + }, 500); + + colorInput.addEventListener("input", throttledColorHandler); + + form.append(label, colorInput, deleteButton); + document.body.appendChild(form); + + this.makeDraggable(form, barId); + } +} diff --git a/static/DialogManager.js b/static/DialogManager.js new file mode 100644 index 0000000..58c3dd2 --- /dev/null +++ b/static/DialogManager.js @@ -0,0 +1,113 @@ +import { createStyledElement } from "./utils.js"; + +export class DialogManager { + static showCreateDialog(onCreateCallback) { + const dialog = createStyledElement("div", { + position: "fixed", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + padding: "20px", + border: "2px solid #333", + borderRadius: "8px", + backgroundColor: "white", + zIndex: "1001", + boxShadow: "0 4px 8px rgba(0,0,0,0.3)", + }); + + const title = createStyledElement( + "h3", + { margin: "0 0 15px 0" }, + { textContent: "Create New Bar" }, + ); + + const inputStyles = { + display: "block", + margin: "5px 0", + padding: "8px", + width: "200px", + }; + + const barIdInput = createStyledElement("input", inputStyles, { + type: "text", + placeholder: "Bar ID", + }); + + const urlInput = createStyledElement("input", inputStyles, { + type: "text", + placeholder: "WebSocket URL", + value: "192.168.4.1", + }); + + const colorInput = createStyledElement("input", inputStyles, { + type: "color", + value: "#ff0000", + }); + + const xInput = createStyledElement("input", inputStyles, { + type: "number", + placeholder: "X Position", + value: "100", + }); + + const yInput = createStyledElement("input", inputStyles, { + type: "number", + placeholder: "Y Position", + value: "100", + }); + + const buttonStyles = { + padding: "8px 12px", + border: "none", + borderRadius: "4px", + cursor: "pointer", + color: "white", + }; + + const createBtn = createStyledElement( + "button", + { + ...buttonStyles, + margin: "10px 5px 0 0", + backgroundColor: "#4CAF50", + }, + { textContent: "Create" }, + ); + + const cancelBtn = createStyledElement( + "button", + { + ...buttonStyles, + margin: "10px 0 0 0", + backgroundColor: "#f44336", + }, + { textContent: "Cancel" }, + ); + + createBtn.onclick = () => { + const barData = { + barId: barIdInput.value, + url: `ws://${urlInput.value}/ws`, + color: colorInput.value, + x: parseInt(xInput.value), + y: parseInt(yInput.value), + }; + onCreateCallback(barData); + document.body.removeChild(dialog); + }; + + cancelBtn.onclick = () => document.body.removeChild(dialog); + + dialog.append( + title, + barIdInput, + urlInput, + colorInput, + xInput, + yInput, + createBtn, + cancelBtn, + ); + document.body.appendChild(dialog); + } +} diff --git a/static/main.css b/static/main.css new file mode 100644 index 0000000..9f5b5bf --- /dev/null +++ b/static/main.css @@ -0,0 +1,84 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: Arial, sans-serif; + background-color: #f5f5f5; + padding: 20px; +} + +.container { + max-width: 1200px; + margin: 0 auto; +} + +header { + text-align: center; + margin-bottom: 30px; + padding: 20px; + background: white; + border-radius: 8px; +} + +section { + background: white; + padding: 20px; + border-radius: 8px; + margin-bottom: 20px; +} + +.btn { + padding: 10px 20px; + background-color: #3498db; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + margin-bottom: 20px; +} + +.btn:hover { + background-color: #2980b9; +} + +.color-container { + position: relative; + width: 100%; + height: 400px; + border: 2px solid #ddd; + border-radius: 6px; + background: #f9f9f9; +} + +.color-picker-item { + position: absolute; + background: white; + border: 2px solid #ddd; + border-radius: 8px; + padding: 15px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transform: translate(-50%, -50%); + text-align: center; +} + +.color-picker-item h4 { + margin-bottom: 10px; + color: #333; +} + +.color-picker-item input[type="color"] { + width: 60px; + height: 40px; + border: none; + border-radius: 4px; + cursor: pointer; + margin-bottom: 10px; +} + +.status { + font-size: 12px; + font-weight: bold; +} diff --git a/static/main.js b/static/main.js new file mode 100644 index 0000000..b0aad84 --- /dev/null +++ b/static/main.js @@ -0,0 +1,5 @@ +import { BarControlSystem } from "./BarControlSystem.js"; + +document.addEventListener("DOMContentLoaded", () => { + new BarControlSystem(); +}); diff --git a/static/utils.js b/static/utils.js new file mode 100644 index 0000000..0704169 --- /dev/null +++ b/static/utils.js @@ -0,0 +1,30 @@ +export function throttle(func, delay) { + let timeoutId; + let lastExecTime = 0; + return function (...args) { + const currentTime = Date.now(); + + if (currentTime - lastExecTime > delay) { + func.apply(this, args); + lastExecTime = currentTime; + } else { + clearTimeout(timeoutId); + timeoutId = setTimeout( + () => { + func.apply(this, args); + lastExecTime = Date.now(); + }, + delay - (currentTime - lastExecTime), + ); + } + }; +} + +export function createStyledElement(tag, styles = {}, attributes = {}) { + const element = document.createElement(tag); + + Object.assign(element.style, styles); + Object.assign(element, attributes); + + return element; +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..de5d55f --- /dev/null +++ b/templates/index.html @@ -0,0 +1,11 @@ + + + + + + Bar Control System + + + + +