Initial working version

This commit is contained in:
jimmy 2025-05-23 22:56:59 +12:00
commit 0b27ef2b30
16 changed files with 1078 additions and 0 deletions

0
.gitignore vendored Normal file
View File

14
Pipfile Normal file
View File

@ -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"

300
Pipfile.lock generated Normal file
View File

@ -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": {}
}

0
api/init.py Normal file
View File

20
api/models.py Normal file
View File

@ -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

57
api/routes.py Normal file
View File

@ -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)}")

51
api/settings_manager.py Normal file
View File

@ -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

20
main.py Normal file
View File

@ -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)

20
settings.json Normal file
View File

@ -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
}
}

83
static/ApiService.js Normal file
View File

@ -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);
}
}
}

270
static/BarControlSystem.js Normal file
View File

@ -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);
}
}

113
static/DialogManager.js Normal file
View File

@ -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);
}
}

84
static/main.css Normal file
View File

@ -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;
}

5
static/main.js Normal file
View File

@ -0,0 +1,5 @@
import { BarControlSystem } from "./BarControlSystem.js";
document.addEventListener("DOMContentLoaded", () => {
new BarControlSystem();
});

30
static/utils.js Normal file
View File

@ -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;
}

11
templates/index.html Normal file
View File

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Bar Control System</title>
</head>
<body>
<script type="module" src="static/main.js"></script>
</body>
</html>