From d2e6b45413db8049f6b1063dba261d0a5469b0b6 Mon Sep 17 00:00:00 2001 From: "codefather.ua" Date: Wed, 6 Mar 2024 13:12:16 +0200 Subject: [PATCH 1/8] not done yet --- Dockerfile | 34 ++ Pipfile | 15 + Pipfile.lock | 570 ++++++++++++++++++++++++ docker-compose.yml | 41 ++ entrypoint.sh | 15 + junk/synthetic_test.py | 7 +- sqlmigrations/001_create_schema.sql | 8 + task_manager/core/storage.py | 46 +- task_manager/core/utils.py | 10 + task_manager/storage/__init__.py | 3 +- task_manager/storage/asyncpg_storage.py | 127 ++++++ task_manager/storage/in_memory.py | 38 +- task_manager/storage/utils.py | 0 test_postgres_storage.py | 87 ++++ 14 files changed, 963 insertions(+), 38 deletions(-) create mode 100644 Dockerfile create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 sqlmigrations/001_create_schema.sql create mode 100644 task_manager/storage/asyncpg_storage.py create mode 100644 task_manager/storage/utils.py create mode 100644 test_postgres_storage.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ba0e405 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,34 @@ +# pull official base image +FROM python:3.11.3-alpine as app + +# set work directory +WORKDIR /usr/src/app + +# set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# install psycopg2 +RUN apk update \ + && apk add --virtual build-deps gcc python3-dev musl-dev \ + && apk add postgresql postgresql-contrib postgresql-dev \ + && pip install psycopg2 \ + && pip install -U pip setuptools wheel \ + && apk del build-deps + +RUN apk update && apk add python3-dev \ + gcc \ + libc-dev \ + libffi-dev + +# copy project +COPY . /usr/src/app/ + +# install dependencies +RUN pip install --upgrade pip +RUN pip install pipenv +RUN pipenv install + +RUN chmod +x /usr/src/app/entrypoint.sh + +EXPOSE $APP_PORT diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..3da1890 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pydantic = "~=2.6" +aiohttp = "~=3.9" +asyncpg = "~=0.29" + +[dev-packages] + +[requires] +python_version = "3.11" +python_full_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..b00403d --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,570 @@ +{ + "_meta": { + "hash": { + "sha256": "98d8f0c8970e1948e2c96defecc6f51a7e6078e85ac16bea988b0b40190c8741" + }, + "pipfile-spec": 6, + "requires": { + "python_full_version": "3.11", + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiohttp": { + "hashes": [ + "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168", + "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb", + "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5", + "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f", + "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc", + "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c", + "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29", + "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4", + "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc", + "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc", + "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63", + "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e", + "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d", + "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a", + "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60", + "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38", + "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b", + "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2", + "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53", + "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5", + "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4", + "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96", + "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58", + "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa", + "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321", + "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae", + "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce", + "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8", + "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194", + "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c", + "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf", + "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d", + "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869", + "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b", + "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52", + "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528", + "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5", + "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1", + "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4", + "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8", + "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d", + "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7", + "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5", + "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54", + "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3", + "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5", + "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c", + "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29", + "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3", + "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747", + "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672", + "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5", + "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11", + "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca", + "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768", + "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6", + "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2", + "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533", + "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6", + "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266", + "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d", + "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec", + "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5", + "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1", + "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b", + "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679", + "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283", + "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb", + "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b", + "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3", + "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051", + "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511", + "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e", + "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d", + "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542", + "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.9.3" + }, + "aiosignal": { + "hashes": [ + "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", + "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.1" + }, + "annotated-types": { + "hashes": [ + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.0" + }, + "async-timeout": { + "hashes": [ + "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", + "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" + ], + "markers": "python_full_version < '3.12.0'", + "version": "==4.0.3" + }, + "asyncpg": { + "hashes": [ + "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9", + "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7", + "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548", + "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23", + "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3", + "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675", + "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe", + "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175", + "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83", + "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385", + "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da", + "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106", + "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870", + "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449", + "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc", + "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178", + "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9", + "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b", + "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169", + "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610", + "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772", + "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2", + "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c", + "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb", + "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac", + "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408", + "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22", + "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb", + "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02", + "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59", + "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8", + "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3", + "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e", + "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4", + "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364", + "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f", + "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775", + "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3", + "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090", + "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810", + "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==0.29.0" + }, + "attrs": { + "hashes": [ + "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", + "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2.0" + }, + "frozenlist": { + "hashes": [ + "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", + "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", + "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", + "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", + "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", + "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", + "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", + "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", + "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", + "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", + "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", + "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", + "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", + "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", + "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", + "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", + "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", + "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", + "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", + "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", + "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", + "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", + "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", + "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", + "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", + "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", + "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", + "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", + "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", + "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", + "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", + "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", + "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", + "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", + "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", + "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", + "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", + "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", + "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", + "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", + "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", + "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", + "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", + "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", + "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", + "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", + "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", + "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", + "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", + "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", + "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", + "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", + "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", + "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", + "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", + "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", + "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", + "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", + "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", + "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", + "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", + "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", + "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", + "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", + "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", + "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", + "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", + "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", + "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", + "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", + "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", + "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", + "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", + "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", + "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", + "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", + "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" + ], + "markers": "python_version >= '3.8'", + "version": "==1.4.1" + }, + "idna": { + "hashes": [ + "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", + "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" + ], + "markers": "python_version >= '3.5'", + "version": "==3.6" + }, + "multidict": { + "hashes": [ + "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", + "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", + "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", + "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", + "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", + "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", + "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", + "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", + "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", + "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", + "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", + "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", + "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", + "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", + "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", + "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", + "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", + "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", + "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", + "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", + "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", + "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", + "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", + "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", + "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", + "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", + "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", + "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", + "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", + "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", + "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", + "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", + "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", + "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", + "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", + "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", + "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", + "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", + "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", + "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", + "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", + "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", + "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", + "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", + "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", + "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", + "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", + "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", + "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", + "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", + "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", + "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", + "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", + "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", + "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", + "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", + "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", + "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", + "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", + "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", + "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", + "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", + "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", + "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", + "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", + "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", + "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", + "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", + "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", + "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", + "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", + "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", + "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", + "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", + "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", + "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", + "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", + "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", + "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", + "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", + "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", + "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", + "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", + "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", + "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", + "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", + "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", + "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", + "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", + "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" + ], + "markers": "python_version >= '3.7'", + "version": "==6.0.5" + }, + "pydantic": { + "hashes": [ + "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a", + "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==2.6.3" + }, + "pydantic-core": { + "hashes": [ + "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a", + "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed", + "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979", + "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff", + "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5", + "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45", + "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340", + "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad", + "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23", + "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6", + "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7", + "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241", + "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda", + "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187", + "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba", + "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c", + "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2", + "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c", + "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132", + "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf", + "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972", + "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db", + "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade", + "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4", + "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8", + "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f", + "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9", + "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48", + "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec", + "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d", + "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9", + "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb", + "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4", + "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89", + "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c", + "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9", + "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da", + "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac", + "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b", + "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf", + "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e", + "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137", + "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1", + "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b", + "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8", + "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e", + "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053", + "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01", + "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe", + "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd", + "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805", + "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183", + "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8", + "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99", + "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820", + "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074", + "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256", + "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8", + "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975", + "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad", + "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e", + "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca", + "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df", + "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b", + "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a", + "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a", + "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721", + "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a", + "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f", + "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2", + "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97", + "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6", + "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed", + "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc", + "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1", + "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe", + "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120", + "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f", + "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a" + ], + "markers": "python_version >= '3.8'", + "version": "==2.16.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" + ], + "markers": "python_version >= '3.8'", + "version": "==4.10.0" + }, + "yarl": { + "hashes": [ + "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", + "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", + "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", + "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", + "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", + "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", + "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", + "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", + "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", + "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", + "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", + "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", + "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", + "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", + "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", + "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", + "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", + "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", + "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", + "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", + "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", + "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", + "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", + "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", + "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", + "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", + "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", + "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", + "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", + "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", + "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", + "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", + "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", + "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", + "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", + "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", + "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", + "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", + "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", + "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", + "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", + "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", + "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", + "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", + "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", + "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", + "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", + "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", + "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", + "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", + "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", + "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", + "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", + "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", + "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", + "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", + "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", + "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", + "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", + "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", + "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", + "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", + "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", + "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", + "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", + "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", + "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", + "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", + "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", + "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", + "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", + "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", + "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", + "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", + "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", + "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", + "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", + "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", + "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", + "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", + "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", + "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", + "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", + "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", + "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", + "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", + "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", + "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", + "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", + "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" + ], + "markers": "python_version >= '3.7'", + "version": "==1.9.4" + } + }, + "develop": {} +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..932c76b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,41 @@ +version: '3' + + +services: + app: +# restart: always + volumes: + - .:/usr/src/app/ + env_file: .env + build: + context: . + dockerfile: Dockerfile + + depends_on: + postgres: + condition: service_healthy + links: + - postgres + entrypoint: sh entrypoint.sh + command: pipenv run python3 synthetic_test.py + + postgres: + image: postgres:15-alpine + container_name: "postgres" + volumes: + - .:/usr/src/app/ + - postgres_data:/var/lib/postgresql/data + env_file: + - .env + ports: + - "5432:5432" + working_dir: /usr/src/app/ + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U task_manager" ] + interval: 1s + timeout: 10s + retries: 0 + +volumes: + .: {} + postgres_data: {} \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..cfc84b4 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,15 @@ +echo "--- starts entrypoint" +#!/bin/sh + +# create db and role if not exists +psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -tc "SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_NAME'" | + grep -q 1 || + psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "CREATE USER $POSTGRES_USER WITH ENCRYPTED PASSWORD '$POSTGRES_PASS';" + psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "CREATE DATABASE $POSTGRES_NAME;" + psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "GRANT ALL PRIVILEGES ON DATABASE $POSTGRES_NAME TO $POSTGRES_USER;" + +# run migrations +echo "--- starts migrations" +psql "user=$POSTGRES_USER host=$POSTGRES_HOST port=$POSTGRES_PORT password=$POSTGRES_PASS dbname=$POSTGRES_NAME" -f ./sqlmigrations/001_create_schema.sql +echo "--- ends entrypoint" +exec "$@" \ No newline at end of file diff --git a/junk/synthetic_test.py b/junk/synthetic_test.py index c2247fe..1cbda16 100644 --- a/junk/synthetic_test.py +++ b/junk/synthetic_test.py @@ -2,9 +2,10 @@ from task_manager.core import TaskManager from task_manager.core import SessionClosedException -from task_manager.storage import InMemoryStorage +from task_manager.storage import InMemoryStorage, AsyncPGStorage -storage = InMemoryStorage() +# storage = InMemoryStorage() +storage = AsyncPGStorage() engine = TaskManager(storage) @@ -62,6 +63,7 @@ async def main(): # test session closing session = engine.new_session() + async def close_session(): await asyncio.sleep(1) print('clossing session') @@ -76,4 +78,5 @@ async def close_session(): await asyncio.sleep(2) + asyncio.run(main()) diff --git a/sqlmigrations/001_create_schema.sql b/sqlmigrations/001_create_schema.sql new file mode 100644 index 0000000..a7bf931 --- /dev/null +++ b/sqlmigrations/001_create_schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS tasks ( + idn UUID PRIMARY KEY, + topic VARCHAR (50) NOT NULL, + payload JSON NOT NULL, + status int NOT NULL, + error VARCHAR (255) NULL, + description VARCHAR (255) NULL +) diff --git a/task_manager/core/storage.py b/task_manager/core/storage.py index 3ad80d4..a25b73b 100644 --- a/task_manager/core/storage.py +++ b/task_manager/core/storage.py @@ -1,6 +1,14 @@ +import asyncio +import json from dataclasses import dataclass from typing import Callable, Awaitable, Any, TypeVar, Generic +from task_manager.core.utils import UUIDEncoder + +NEW = 0 +IN_PROGRESS = 1 +FINISHED = 2 + @dataclass class ConsumedTask: @@ -28,7 +36,7 @@ class StorageInterface: async def create_task(self, queue, payload) -> str: raise NotImplemented() - async def finish_task(self, idn: str, error: None|str = None, message: str = ''): + async def finish_task(self, idn: str, error: None | str = None, message: str = ''): raise NotImplemented() async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: @@ -39,3 +47,39 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con def add_on_task_callback(self, callback: OnTaskCallback): raise NotImplemented() + + +@dataclass +class Task: + idn: str + topic: str + payload: dict + status: int + error: None | str + description: str = '' + + @staticmethod + def from_dict(data: dict): + if isinstance(data.get('payload'), str): + data['payload'] = json.loads(data['payload']) + return Task(**json.loads(json.dumps(data, cls=UUIDEncoder))) + + +class ConsumedTaskResult(TransactionalResult[ConsumedTask]): + def __init__(self, task: Task, lock: asyncio.Lock): + self.task = task + self.lock = lock + + async def get_data(self) -> ConsumedTask: + return ConsumedTask(self.task.idn, self.task.topic, self.task.payload) + + async def commit(self): + # FIXME pg impl conflicts + # self.lock.release() + ... + + async def rollback(self): + self.task.status = NEW + # FIXME pg impl conflicts + # self.lock.release() + ... diff --git a/task_manager/core/utils.py b/task_manager/core/utils.py index ca431d4..c6387a2 100644 --- a/task_manager/core/utils.py +++ b/task_manager/core/utils.py @@ -1,4 +1,6 @@ import asyncio +import json +from uuid import UUID class TaskWaiter: @@ -8,3 +10,11 @@ def __init__(self, topics: list[str]): async def wait(self): return await asyncio.wait_for(self.result, None) + + +class UUIDEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, UUID): + # if the obj is uuid, we simply return the value of uuid + return str(obj) + return json.JSONEncoder.default(self, obj) diff --git a/task_manager/storage/__init__.py b/task_manager/storage/__init__.py index bc3c528..4b45b7b 100644 --- a/task_manager/storage/__init__.py +++ b/task_manager/storage/__init__.py @@ -1 +1,2 @@ -from .in_memory import InMemoryStorage \ No newline at end of file +from .in_memory import InMemoryStorage +from .asyncpg_storage import AsyncPGStorage \ No newline at end of file diff --git a/task_manager/storage/asyncpg_storage.py b/task_manager/storage/asyncpg_storage.py new file mode 100644 index 0000000..2a8a103 --- /dev/null +++ b/task_manager/storage/asyncpg_storage.py @@ -0,0 +1,127 @@ +import asyncio +import json +import os +import uuid +from typing import Optional + +import asyncpg +from asyncpg import Connection +from asyncpg.transaction import Transaction + +from task_manager.core.storage import StorageInterface, OnTaskCallback, TransactionalResult, ConsumedTask, NEW, \ + IN_PROGRESS, ConsumedTaskResult, FINISHED, Task + + +class AsyncPGStorage(StorageInterface): + def __init__(self): + self.conn: Optional[Connection] = None + self.lock = asyncio.Lock() + self.tasks = [] # todo postgres impl + + # todo postgres impl + # self.conn.add_listener() ? + self.on_task_callbacks = [] + + async def create_connection(self): + if not self.conn or self.conn.is_closed(): + self.conn = await asyncpg.connect( + user=os.environ.get("POSTGRES_USER", "postgres"), + password=os.environ.get("POSTGRES_PASS", "task_manager_password"), + database=os.environ.get("POSTGRES_NAME", "task_manager"), + host=os.environ.get("POSTGRES_HOST", "127.0.0.1"), + port=os.environ.get("POSTGRES_PORT", "5432") + ) + + async def close_connection(self): + await self.conn.close() + + async def create_task(self, queue, payload) -> str: + """ + idn UUID PRIMARY KEY, + topic VARCHAR (50) NOT NULL, + payload JSON NOT NULL, + status int NOT NULL, + error VARCHAR (255) NULL, + description VARCHAR (255) NULL + """ + task = Task(str(uuid.uuid4()), queue, payload, NEW, None) + + q = "INSERT INTO tasks (" \ + "idn, topic, payload, status" \ + ") VALUES (" \ + f"'{task.idn}', '{queue}', '{json.dumps(payload)}', {NEW}" \ + ")" + + await self.conn.fetch(q) + + self.tasks.append(task) + + # todo postgres impl + for callback in self.on_task_callbacks: + asyncio.create_task(callback(task.idn)) + + return task.idn + + async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: + """ + idn UUID PRIMARY KEY, + topic VARCHAR (50) NOT NULL, + payload JSON NOT NULL, + status int NOT NULL, + error VARCHAR (255) NULL, + description VARCHAR (255) NULL + """ + + # FIXME pg impl conflicts + # await self.lock.acquire() + + q = ("UPDATE tasks " + f"SET status = {IN_PROGRESS} " + f"WHERE (idn='{idn}' and status={NEW}) RETURNING * ;") + + res: list | asyncpg.Record = await self.conn.fetch(q) + + if res: + res: asyncpg.Record = res[0] + task = Task.from_dict(dict(res)) + return ConsumedTaskResult( + task, + self.lock + ) + + # FIXME pg impl conflicts + # self.lock.release() + return None + + # todo postgres impl + async def take_first_pending(self, topics: list[str]) -> TransactionalResult[ConsumedTask] | None: + # FIXME pg impl conflicts + # await self.lock.acquire() + + for task in self.tasks: + if task.topic in topics and task.status == NEW: + task.status = IN_PROGRESS + return ConsumedTaskResult( + task, + self.lock + ) + + # FIXME pg impl conflicts + # self.lock.release() + return None + + # todo postgres impl + async def finish_task(self, idn: str, error: None | str = None, message: str = ''): + for task in self.tasks: + if task.idn == idn: + task.status = FINISHED + task.error = error + task.message = message + return + + # todo: add exceptions inheritance level to interface and raise appropriate one + raise Exception(f"Can't finish task. Task '{idn}' not found in storage") + + def add_on_task_callback(self, callback: OnTaskCallback): + # todo postgres impl + self.on_task_callbacks.append(callback) diff --git a/task_manager/storage/in_memory.py b/task_manager/storage/in_memory.py index 7a4e458..62b3e48 100644 --- a/task_manager/storage/in_memory.py +++ b/task_manager/storage/in_memory.py @@ -1,38 +1,8 @@ import asyncio import uuid -from dataclasses import dataclass -from task_manager.core.storage import StorageInterface, ConsumedTask, OnTaskCallback, TransactionalResult - -NEW = 0 -IN_PROGRESS = 1 -FINISHED = 2 - - -@dataclass -class Task: - idn: str - topic: str - payload: dict - status: int - error: None|str - description: str = '' - - -class ConsumedTaskResult(TransactionalResult[ConsumedTask]): - def __init__(self, task: Task, lock: asyncio.Lock): - self.task = task - self.lock = lock - - async def get_data(self) -> ConsumedTask: - return ConsumedTask(self.task.idn, self.task.topic, self.task.payload) - - async def commit(self): - self.lock.release() - - async def rollback(self): - self.task.status = NEW - self.lock.release() +from task_manager.core.storage import StorageInterface, Task, NEW, TransactionalResult, ConsumedTask, IN_PROGRESS, \ + ConsumedTaskResult, FINISHED, OnTaskCallback # todo: refactor this horror @@ -67,7 +37,7 @@ async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: async def take_first_pending(self, topics: list[str]) -> TransactionalResult[ConsumedTask] | None: await self.lock.acquire() - + for task in self.tasks: if task.topic in topics and task.status == NEW: task.status = IN_PROGRESS @@ -79,7 +49,7 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con self.lock.release() return None - async def finish_task(self, idn: str, error: None|str = None, message: str = ''): + async def finish_task(self, idn: str, error: None | str = None, message: str = ''): for task in self.tasks: if task.idn == idn: task.status = FINISHED diff --git a/task_manager/storage/utils.py b/task_manager/storage/utils.py new file mode 100644 index 0000000..e69de29 diff --git a/test_postgres_storage.py b/test_postgres_storage.py new file mode 100644 index 0000000..64d6b4f --- /dev/null +++ b/test_postgres_storage.py @@ -0,0 +1,87 @@ +import asyncio + +from task_manager.core import TaskManager +from task_manager.core import SessionClosedException +from task_manager.storage import InMemoryStorage, AsyncPGStorage + +# storage = InMemoryStorage() +storage = AsyncPGStorage() +engine = TaskManager(storage) + +loop = asyncio.new_event_loop() +asyncio.set_event_loop(loop) + + +def make_consumer(name, max_messages): + async def consumer(): + session = engine.new_session() + + for i in range(max_messages): + task = await session.consume_task(['test']) + print(f"consumer '{name}' got task '{task.idn}' with payload {task.payload} in '{task.topic}'") + await asyncio.sleep(3) + print(f"consumer '{name}' finished task '{task.idn}' with payload {task.payload} in '{task.topic}'") + + return consumer() + + +async def main(): + await storage.create_connection() + + asyncio.create_task(make_consumer("1 message", 1)) + asyncio.create_task(make_consumer("permanent", 100)) + + await asyncio.sleep(0.1) + + publisher = engine.new_session() + + # publish when consumers are running + for i in range(3): + print(f'publish {i}') + await publisher.publish_task('test', {"id": i}) + + print(f'publish nonce') + await publisher.publish_task('another', {"nonce": "nonce"}) + + # publish when permanent consumer still in progress + await asyncio.sleep(3) + print(f'publish 3') + await publisher.publish_task('test', {"id": 3}) + + # publish when permanent consumer pending + await asyncio.sleep(7) + + print(f'publish nonce') + await publisher.publish_task('another', {"nonce": "nonce"}) + + print(f'publish 4') + await publisher.publish_task('test', {"id": 4}) + + # publish when permanent consumer in progress and run new consumer after 1 sec passed + print(f'publish 5') + await publisher.publish_task('test', {"id": 5}) + + await asyncio.sleep(1) + asyncio.create_task(make_consumer('new', 100)) + + await asyncio.sleep(3.5) + + # test session closing + session = engine.new_session() + + async def close_session(): + await asyncio.sleep(1) + print('clossing session') + session.close() + + asyncio.create_task(close_session()) + try: + print('waiting for new task') + task = await session.consume_task(['test']) + except SessionClosedException as e: + print('successfully caught exception') + + await asyncio.sleep(2) + + +loop.run_until_complete(main()) From 38e6fdcf591734e24461400116a120947d1668e5 Mon Sep 17 00:00:00 2001 From: "codefather.ua" Date: Wed, 6 Mar 2024 14:31:50 +0200 Subject: [PATCH 2/8] not done yet --- task_manager/storage/asyncpg_storage.py | 58 +++++++++++++++++-------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/task_manager/storage/asyncpg_storage.py b/task_manager/storage/asyncpg_storage.py index 2a8a103..8252591 100644 --- a/task_manager/storage/asyncpg_storage.py +++ b/task_manager/storage/asyncpg_storage.py @@ -93,34 +93,56 @@ async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: # self.lock.release() return None - # todo postgres impl async def take_first_pending(self, topics: list[str]) -> TransactionalResult[ConsumedTask] | None: # FIXME pg impl conflicts # await self.lock.acquire() + """ + idn UUID PRIMARY KEY, + topic VARCHAR (50) NOT NULL, + payload JSON NOT NULL, + status int NOT NULL, + error VARCHAR (255) NULL, + description VARCHAR (255) NULL + """ - for task in self.tasks: - if task.topic in topics and task.status == NEW: - task.status = IN_PROGRESS - return ConsumedTaskResult( - task, - self.lock - ) + q = ( + "UPDATE tasks " + f"SET status = {IN_PROGRESS} " + f"WHERE (" + f"status = {NEW} and " + f"topic in ({str([f'{i}' for i in topics])[1:-1]})) " + f"RETURNING * ;" + ) + + res: list | asyncpg.Record = await self.conn.fetch(q) + + if res: + res: asyncpg.Record = res[0] + task = Task.from_dict(dict(res)) + return ConsumedTaskResult( + task, + self.lock + ) # FIXME pg impl conflicts # self.lock.release() return None - # todo postgres impl async def finish_task(self, idn: str, error: None | str = None, message: str = ''): - for task in self.tasks: - if task.idn == idn: - task.status = FINISHED - task.error = error - task.message = message - return - - # todo: add exceptions inheritance level to interface and raise appropriate one - raise Exception(f"Can't finish task. Task '{idn}' not found in storage") + + q = ( + "UPDATE tasks " + f"SET status = {FINISHED}, " + f"error = {error}, " + f"description = {message} " + f"WHERE idn = '{idn}'" + f"RETURNING *;" + ) + res: list | asyncpg.Record = await self.conn.fetch(q) + + if not res: + # todo: add exceptions inheritance level to interface and raise appropriate one + raise Exception(f"Can't finish task. Task '{idn}' not found in storage") def add_on_task_callback(self, callback: OnTaskCallback): # todo postgres impl From 4fbb0b62096cc6af488f3287ae213056fe21f3c8 Mon Sep 17 00:00:00 2001 From: "codefather.ua" Date: Wed, 6 Mar 2024 15:54:49 +0200 Subject: [PATCH 3/8] pg impl conflict fix --- task_manager/core/storage.py | 6 ++-- task_manager/storage/asyncpg_storage.py | 41 ++++++++++++++----------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/task_manager/core/storage.py b/task_manager/core/storage.py index a25b73b..f3a83c1 100644 --- a/task_manager/core/storage.py +++ b/task_manager/core/storage.py @@ -74,12 +74,10 @@ async def get_data(self) -> ConsumedTask: return ConsumedTask(self.task.idn, self.task.topic, self.task.payload) async def commit(self): - # FIXME pg impl conflicts - # self.lock.release() + self.lock.release() ... async def rollback(self): self.task.status = NEW - # FIXME pg impl conflicts - # self.lock.release() + self.lock.release() ... diff --git a/task_manager/storage/asyncpg_storage.py b/task_manager/storage/asyncpg_storage.py index 8252591..c64b1ff 100644 --- a/task_manager/storage/asyncpg_storage.py +++ b/task_manager/storage/asyncpg_storage.py @@ -5,7 +5,7 @@ from typing import Optional import asyncpg -from asyncpg import Connection +from asyncpg import Connection, Pool from asyncpg.transaction import Transaction from task_manager.core.storage import StorageInterface, OnTaskCallback, TransactionalResult, ConsumedTask, NEW, \ @@ -14,7 +14,7 @@ class AsyncPGStorage(StorageInterface): def __init__(self): - self.conn: Optional[Connection] = None + self.pool: Optional[Pool] = None self.lock = asyncio.Lock() self.tasks = [] # todo postgres impl @@ -23,17 +23,18 @@ def __init__(self): self.on_task_callbacks = [] async def create_connection(self): - if not self.conn or self.conn.is_closed(): - self.conn = await asyncpg.connect( + if not self.pool or self.pool._closed: + self.pool = await asyncpg.create_pool( user=os.environ.get("POSTGRES_USER", "postgres"), password=os.environ.get("POSTGRES_PASS", "task_manager_password"), database=os.environ.get("POSTGRES_NAME", "task_manager"), host=os.environ.get("POSTGRES_HOST", "127.0.0.1"), - port=os.environ.get("POSTGRES_PORT", "5432") + port=os.environ.get("POSTGRES_PORT", "5432"), + max_size=10 ) async def close_connection(self): - await self.conn.close() + await self.pool.close() async def create_task(self, queue, payload) -> str: """ @@ -52,7 +53,9 @@ async def create_task(self, queue, payload) -> str: f"'{task.idn}', '{queue}', '{json.dumps(payload)}', {NEW}" \ ")" - await self.conn.fetch(q) + async with self.pool.acquire() as connection: + async with connection.transaction(): + await connection.fetch(q) self.tasks.append(task) @@ -72,14 +75,15 @@ async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: description VARCHAR (255) NULL """ - # FIXME pg impl conflicts - # await self.lock.acquire() + await self.lock.acquire() q = ("UPDATE tasks " f"SET status = {IN_PROGRESS} " f"WHERE (idn='{idn}' and status={NEW}) RETURNING * ;") - res: list | asyncpg.Record = await self.conn.fetch(q) + async with self.pool.acquire() as connection: + async with connection.transaction(): + res: list | asyncpg.Record = await connection.fetch(q) if res: res: asyncpg.Record = res[0] @@ -89,13 +93,11 @@ async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: self.lock ) - # FIXME pg impl conflicts - # self.lock.release() + self.lock.release() return None async def take_first_pending(self, topics: list[str]) -> TransactionalResult[ConsumedTask] | None: - # FIXME pg impl conflicts - # await self.lock.acquire() + await self.lock.acquire() """ idn UUID PRIMARY KEY, topic VARCHAR (50) NOT NULL, @@ -114,7 +116,9 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con f"RETURNING * ;" ) - res: list | asyncpg.Record = await self.conn.fetch(q) + async with self.pool.acquire() as connection: + async with connection.transaction(): + res: list | asyncpg.Record = await connection.fetch(q) if res: res: asyncpg.Record = res[0] @@ -124,8 +128,7 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con self.lock ) - # FIXME pg impl conflicts - # self.lock.release() + self.lock.release() return None async def finish_task(self, idn: str, error: None | str = None, message: str = ''): @@ -138,7 +141,9 @@ async def finish_task(self, idn: str, error: None | str = None, message: str = ' f"WHERE idn = '{idn}'" f"RETURNING *;" ) - res: list | asyncpg.Record = await self.conn.fetch(q) + async with self.pool.acquire() as connection: + async with connection.transaction(): + res: list | asyncpg.Record = await connection.fetch(q) if not res: # todo: add exceptions inheritance level to interface and raise appropriate one From 19ebc6d2963b5f5aad6171d0c49fc711188dd98e Mon Sep 17 00:00:00 2001 From: "codefather.ua" Date: Wed, 6 Mar 2024 16:28:39 +0200 Subject: [PATCH 4/8] easy refactoring --- task_manager/storage/asyncpg_storage.py | 60 +++++++++++++++++-------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/task_manager/storage/asyncpg_storage.py b/task_manager/storage/asyncpg_storage.py index c64b1ff..e9cd7ff 100644 --- a/task_manager/storage/asyncpg_storage.py +++ b/task_manager/storage/asyncpg_storage.py @@ -15,6 +15,14 @@ class AsyncPGStorage(StorageInterface): def __init__(self): self.pool: Optional[Pool] = None + self.subscribe_conn: Optional[Connection] = None + self.connection_data = { + "user": os.environ.get("POSTGRES_USER", "postgres"), + "password": os.environ.get("POSTGRES_PASS", "task_manager_password"), + "database": os.environ.get("POSTGRES_NAME", "task_manager"), + "host": os.environ.get("POSTGRES_HOST", "127.0.0.1"), + "port": os.environ.get("POSTGRES_PORT", "5432"), + } self.lock = asyncio.Lock() self.tasks = [] # todo postgres impl @@ -23,18 +31,19 @@ def __init__(self): self.on_task_callbacks = [] async def create_connection(self): - if not self.pool or self.pool._closed: + if not self.pool or self.pool.is_closing(): self.pool = await asyncpg.create_pool( - user=os.environ.get("POSTGRES_USER", "postgres"), - password=os.environ.get("POSTGRES_PASS", "task_manager_password"), - database=os.environ.get("POSTGRES_NAME", "task_manager"), - host=os.environ.get("POSTGRES_HOST", "127.0.0.1"), - port=os.environ.get("POSTGRES_PORT", "5432"), + **self.connection_data, max_size=10 ) + if not self.subscribe_conn or self.subscribe_conn.is_closed(): + self.subscribe_conn = await asyncpg.connect( + **self.connection_data + ) async def close_connection(self): - await self.pool.close() + await self.pool.close() if self.pool else None + await self.subscribe_conn.close() if self.subscribe_conn else None async def create_task(self, queue, payload) -> str: """ @@ -54,10 +63,12 @@ async def create_task(self, queue, payload) -> str: ")" async with self.pool.acquire() as connection: + connection: Connection async with connection.transaction(): await connection.fetch(q) - self.tasks.append(task) + # FIXME what is this ? + # self.tasks.append(task) # todo postgres impl for callback in self.on_task_callbacks: @@ -82,17 +93,19 @@ async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: f"WHERE (idn='{idn}' and status={NEW}) RETURNING * ;") async with self.pool.acquire() as connection: + connection: Connection + async with connection.transaction(): - res: list | asyncpg.Record = await connection.fetch(q) + rec: asyncpg.Record = await connection.fetchrow(q) - if res: - res: asyncpg.Record = res[0] - task = Task.from_dict(dict(res)) + if rec: + task = Task.from_dict(dict(rec)) return ConsumedTaskResult( task, self.lock ) + # TODO is this legal? (release only without rec flow) self.lock.release() return None @@ -117,17 +130,19 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con ) async with self.pool.acquire() as connection: + connection: Connection + async with connection.transaction(): - res: list | asyncpg.Record = await connection.fetch(q) + rec: asyncpg.Record = await connection.fetchrow(q) - if res: - res: asyncpg.Record = res[0] - task = Task.from_dict(dict(res)) + if rec: + task = Task.from_dict(dict(rec)) return ConsumedTaskResult( task, self.lock ) + # TODO is this legal? (release only without rec flow) self.lock.release() return None @@ -142,13 +157,20 @@ async def finish_task(self, idn: str, error: None | str = None, message: str = ' f"RETURNING *;" ) async with self.pool.acquire() as connection: + connection: Connection + async with connection.transaction(): - res: list | asyncpg.Record = await connection.fetch(q) + rec: asyncpg.Record = await connection.fetchrow(q) - if not res: + if not rec: # todo: add exceptions inheritance level to interface and raise appropriate one raise Exception(f"Can't finish task. Task '{idn}' not found in storage") - def add_on_task_callback(self, callback: OnTaskCallback): + def add_on_task_callback(self, callback: OnTaskCallback, channel: str = 'tasks'): # todo postgres impl self.on_task_callbacks.append(callback) + + # self.subscribe_conn.add_listener( + # channel=channel, + # callback=callback + # ) From d6b83f380e00ad7324eaa7da5f2499c687f37631 Mon Sep 17 00:00:00 2001 From: "codefather.ua" Date: Wed, 6 Mar 2024 17:02:13 +0200 Subject: [PATCH 5/8] create_task refactoring --- task_manager/storage/asyncpg_storage.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/task_manager/storage/asyncpg_storage.py b/task_manager/storage/asyncpg_storage.py index e9cd7ff..cc299e5 100644 --- a/task_manager/storage/asyncpg_storage.py +++ b/task_manager/storage/asyncpg_storage.py @@ -54,12 +54,11 @@ async def create_task(self, queue, payload) -> str: error VARCHAR (255) NULL, description VARCHAR (255) NULL """ - task = Task(str(uuid.uuid4()), queue, payload, NEW, None) - + idn = str(uuid.uuid4()) q = "INSERT INTO tasks (" \ "idn, topic, payload, status" \ ") VALUES (" \ - f"'{task.idn}', '{queue}', '{json.dumps(payload)}', {NEW}" \ + f"'{idn}', '{queue}', '{json.dumps(payload)}', {NEW}" \ ")" async with self.pool.acquire() as connection: @@ -72,9 +71,9 @@ async def create_task(self, queue, payload) -> str: # todo postgres impl for callback in self.on_task_callbacks: - asyncio.create_task(callback(task.idn)) + asyncio.create_task(callback(idn)) - return task.idn + return idn async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: """ From 777e6b9883d60d62920f6a07cea57c8adf1dc171 Mon Sep 17 00:00:00 2001 From: "codefather.ua" Date: Wed, 6 Mar 2024 20:23:58 +0200 Subject: [PATCH 6/8] AsyncPGStorage refactoring + AsyncPGConsumedTaskResult and AsyncPGTask realizations --- docker-compose.yml | 2 +- junk/synthetic_test.py | 43 +++++-- task_manager/core/storage.py | 55 ++------- task_manager/core/tasks.py | 21 ++++ task_manager/storage/asyncpg_storage.py | 145 ++++++++++++++++-------- task_manager/storage/in_memory.py | 35 ++++-- test_postgres_storage.py | 1 - 7 files changed, 193 insertions(+), 109 deletions(-) create mode 100644 task_manager/core/tasks.py diff --git a/docker-compose.yml b/docker-compose.yml index 932c76b..97a27dd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,7 +17,7 @@ services: links: - postgres entrypoint: sh entrypoint.sh - command: pipenv run python3 synthetic_test.py + command: pipenv run python3 junk/synthetic_test.py asyncpg_storage postgres: image: postgres:15-alpine diff --git a/junk/synthetic_test.py b/junk/synthetic_test.py index 1cbda16..7b4c5cc 100644 --- a/junk/synthetic_test.py +++ b/junk/synthetic_test.py @@ -1,13 +1,40 @@ import asyncio +import os +import sys +from pathlib import Path +import argparse + +# resolving import problem +parent_dir = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(str(Path(parent_dir).resolve().parents[0])) from task_manager.core import TaskManager from task_manager.core import SessionClosedException -from task_manager.storage import InMemoryStorage, AsyncPGStorage +from task_manager.core.storage import StorageType, StorageInterface + +parser = argparse.ArgumentParser() + +parser.add_argument( + "storage_type", + default=StorageType.IN_MEMORY_STORAGE, + choices=StorageType.choices(), + help="Storage type for synthetic test", +) -# storage = InMemoryStorage() -storage = AsyncPGStorage() +args = parser.parse_args() + +if args.storage_type == StorageType.IN_MEMORY_STORAGE: + from task_manager.storage import InMemoryStorage as storage +elif args.storage_type == StorageType.ASYNCPG_STORAGE: + from task_manager.storage import AsyncPGStorage as storage +else: + exit('Invalid storage type') + +storage: StorageInterface = storage() engine = TaskManager(storage) +loop = asyncio.get_event_loop() + def make_consumer(name, max_messages): async def consumer(): @@ -23,8 +50,8 @@ async def consumer(): async def main(): - asyncio.create_task(make_consumer("1 message", 1)) - asyncio.create_task(make_consumer("permanent", 100)) + loop.create_task(make_consumer("permanent", 100)) + loop.create_task(make_consumer("1 message", 1)) await asyncio.sleep(0.1) @@ -57,7 +84,7 @@ async def main(): await publisher.publish_task('test', {"id": 5}) await asyncio.sleep(1) - asyncio.create_task(make_consumer('new', 100)) + loop.create_task(make_consumer('new', 100)) await asyncio.sleep(3.5) @@ -69,7 +96,7 @@ async def close_session(): print('clossing session') session.close() - asyncio.create_task(close_session()) + loop.create_task(close_session()) try: print('waiting for new task') task = await session.consume_task(['test']) @@ -79,4 +106,4 @@ async def close_session(): await asyncio.sleep(2) -asyncio.run(main()) +loop.run_until_complete(main()) diff --git a/task_manager/core/storage.py b/task_manager/core/storage.py index f3a83c1..561a656 100644 --- a/task_manager/core/storage.py +++ b/task_manager/core/storage.py @@ -1,21 +1,6 @@ -import asyncio -import json -from dataclasses import dataclass from typing import Callable, Awaitable, Any, TypeVar, Generic -from task_manager.core.utils import UUIDEncoder - -NEW = 0 -IN_PROGRESS = 1 -FINISHED = 2 - - -@dataclass -class ConsumedTask: - idn: str - topic: str - payload: dict - +from task_manager.core.tasks import ConsumedTask OnTaskCallback = Callable[[str], Awaitable[Any]] T = TypeVar('T') @@ -49,35 +34,13 @@ def add_on_task_callback(self, callback: OnTaskCallback): raise NotImplemented() -@dataclass -class Task: - idn: str - topic: str - payload: dict - status: int - error: None | str - description: str = '' +class StorageType: + IN_MEMORY_STORAGE = 'in_memory_storage' + ASYNCPG_STORAGE = 'asyncpg_storage' @staticmethod - def from_dict(data: dict): - if isinstance(data.get('payload'), str): - data['payload'] = json.loads(data['payload']) - return Task(**json.loads(json.dumps(data, cls=UUIDEncoder))) - - -class ConsumedTaskResult(TransactionalResult[ConsumedTask]): - def __init__(self, task: Task, lock: asyncio.Lock): - self.task = task - self.lock = lock - - async def get_data(self) -> ConsumedTask: - return ConsumedTask(self.task.idn, self.task.topic, self.task.payload) - - async def commit(self): - self.lock.release() - ... - - async def rollback(self): - self.task.status = NEW - self.lock.release() - ... + def choices(): + return [ + StorageType.IN_MEMORY_STORAGE, + StorageType.ASYNCPG_STORAGE + ] diff --git a/task_manager/core/tasks.py b/task_manager/core/tasks.py new file mode 100644 index 0000000..fd6b8b5 --- /dev/null +++ b/task_manager/core/tasks.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass + + +class TaskStatus: + NEW = 0 + IN_PROGRESS = 1 + FINISHED = 2 + + +@dataclass +class ConsumedTask: + idn: str + topic: str + payload: dict + + +@dataclass +class Task(ConsumedTask): + status: int + error: None | str + description: str = '' diff --git a/task_manager/storage/asyncpg_storage.py b/task_manager/storage/asyncpg_storage.py index cc299e5..3f3cc2a 100644 --- a/task_manager/storage/asyncpg_storage.py +++ b/task_manager/storage/asyncpg_storage.py @@ -2,14 +2,50 @@ import json import os import uuid -from typing import Optional +from dataclasses import dataclass +from typing import Optional, Tuple import asyncpg from asyncpg import Connection, Pool +from asyncpg.prepared_stmt import PreparedStatement from asyncpg.transaction import Transaction -from task_manager.core.storage import StorageInterface, OnTaskCallback, TransactionalResult, ConsumedTask, NEW, \ - IN_PROGRESS, ConsumedTaskResult, FINISHED, Task +from task_manager.core.tasks import ConsumedTask, TaskStatus +from task_manager.core.storage import ( + StorageInterface, OnTaskCallback, TransactionalResult +) +from task_manager.core.utils import UUIDEncoder +from task_manager.storage.in_memory import Task + + +@dataclass +class AsyncPGTask(Task): + @staticmethod + def from_dict(data: dict): + if isinstance(data.get('payload'), str): + data['payload'] = json.loads(data['payload']) + return AsyncPGTask(**json.loads(json.dumps(data, cls=UUIDEncoder))) + + +class AsyncPGConsumedTaskResult(TransactionalResult[ConsumedTask]): + def __init__( + self, + task: AsyncPGTask, + lock: Transaction + ): + self.task = task + self.lock = lock + + async def get_data(self) -> ConsumedTask: + # TODO pg impl + return ConsumedTask(self.task.idn, self.task.topic, self.task.payload) + # ... + + async def commit(self): + await self.lock.commit() + + async def rollback(self): + await self.lock.rollback() class AsyncPGStorage(StorageInterface): @@ -23,7 +59,10 @@ def __init__(self): "host": os.environ.get("POSTGRES_HOST", "127.0.0.1"), "port": os.environ.get("POSTGRES_PORT", "5432"), } - self.lock = asyncio.Lock() + asyncio.get_event_loop().run_until_complete( + self.create_connection() + ) + self.tasks = [] # todo postgres impl # todo postgres impl @@ -37,13 +76,21 @@ async def create_connection(self): max_size=10 ) if not self.subscribe_conn or self.subscribe_conn.is_closed(): - self.subscribe_conn = await asyncpg.connect( - **self.connection_data - ) + self.subscribe_conn = self.pool.acquire() async def close_connection(self): + if self.subscribe_conn: + await self.pool.release(self.subscribe_conn) + await self.subscribe_conn.close() + await self.pool.close() if self.pool else None - await self.subscribe_conn.close() if self.subscribe_conn else None + + async def __create_transaction(self) -> Tuple[Transaction, Connection]: + conn: Connection = await self.pool.acquire() + transaction: Transaction = conn.transaction() + await transaction.start() + + return transaction, conn async def create_task(self, queue, payload) -> str: """ @@ -58,13 +105,13 @@ async def create_task(self, queue, payload) -> str: q = "INSERT INTO tasks (" \ "idn, topic, payload, status" \ ") VALUES (" \ - f"'{idn}', '{queue}', '{json.dumps(payload)}', {NEW}" \ + f"'{idn}', '{queue}', '{json.dumps(payload)}', {TaskStatus.NEW}" \ ")" - async with self.pool.acquire() as connection: - connection: Connection - async with connection.transaction(): - await connection.fetch(q) + transaction, conn = await self.__create_transaction() + await conn.fetch(q) + await transaction.commit() + await self.pool.release(conn) # FIXME what is this ? # self.tasks.append(task) @@ -85,31 +132,29 @@ async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: description VARCHAR (255) NULL """ - await self.lock.acquire() + transaction, conn = await self.__create_transaction() + q = "SELECT * FROM tasks " \ + f"WHERE (idn='{idn}' and status={TaskStatus.NEW}) " \ + "FOR UPDATE;" + await conn.fetchrow(q) q = ("UPDATE tasks " - f"SET status = {IN_PROGRESS} " - f"WHERE (idn='{idn}' and status={NEW}) RETURNING * ;") - - async with self.pool.acquire() as connection: - connection: Connection - - async with connection.transaction(): - rec: asyncpg.Record = await connection.fetchrow(q) + f"SET status = {TaskStatus.IN_PROGRESS} " + f"WHERE (idn='{idn}' and status={TaskStatus.NEW}) RETURNING * ;") + rec: asyncpg.Record = await conn.fetchrow(q) if rec: - task = Task.from_dict(dict(rec)) - return ConsumedTaskResult( + task = AsyncPGTask.from_dict(dict(rec)) + return AsyncPGConsumedTaskResult( task, - self.lock + transaction ) + await transaction.rollback() + await self.pool.release(conn) - # TODO is this legal? (release only without rec flow) - self.lock.release() return None async def take_first_pending(self, topics: list[str]) -> TransactionalResult[ConsumedTask] | None: - await self.lock.acquire() """ idn UUID PRIMARY KEY, topic VARCHAR (50) NOT NULL, @@ -119,49 +164,59 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con description VARCHAR (255) NULL """ + transaction, conn = await self.__create_transaction() + q = "SELECT * FROM tasks " \ + f"WHERE (" \ + f"status = {TaskStatus.NEW} and " \ + f"topic in ({str([f'{i}' for i in topics])[1:-1]})) " \ + "FOR UPDATE;" + await conn.fetchrow(q) + q = ( "UPDATE tasks " - f"SET status = {IN_PROGRESS} " + f"SET status = {TaskStatus.IN_PROGRESS} " f"WHERE (" - f"status = {NEW} and " + f"status = {TaskStatus.NEW} and " f"topic in ({str([f'{i}' for i in topics])[1:-1]})) " f"RETURNING * ;" ) - async with self.pool.acquire() as connection: - connection: Connection - - async with connection.transaction(): - rec: asyncpg.Record = await connection.fetchrow(q) + rec: asyncpg.Record = await conn.fetchrow(q) if rec: - task = Task.from_dict(dict(rec)) - return ConsumedTaskResult( + task = AsyncPGTask.from_dict(dict(rec)) + return AsyncPGConsumedTaskResult( task, - self.lock + transaction ) - # TODO is this legal? (release only without rec flow) - self.lock.release() + await transaction.rollback() + await self.pool.release(conn) + return None async def finish_task(self, idn: str, error: None | str = None, message: str = ''): + transaction, conn = await self.__create_transaction() + q = "SELECT * FROM tasks " \ + f"WHERE idn = '{idn}'" \ + "FOR UPDATE;" + await conn.fetchrow(q) + q = ( "UPDATE tasks " - f"SET status = {FINISHED}, " + f"SET status = {TaskStatus.FINISHED}, " f"error = {error}, " f"description = {message} " f"WHERE idn = '{idn}'" f"RETURNING *;" ) - async with self.pool.acquire() as connection: - connection: Connection - async with connection.transaction(): - rec: asyncpg.Record = await connection.fetchrow(q) + rec: asyncpg.Record = await conn.fetchrow(q) if not rec: + await transaction.rollback() + await self.pool.release(conn) # todo: add exceptions inheritance level to interface and raise appropriate one raise Exception(f"Can't finish task. Task '{idn}' not found in storage") diff --git a/task_manager/storage/in_memory.py b/task_manager/storage/in_memory.py index 62b3e48..8c437ff 100644 --- a/task_manager/storage/in_memory.py +++ b/task_manager/storage/in_memory.py @@ -1,8 +1,27 @@ import asyncio import uuid -from task_manager.core.storage import StorageInterface, Task, NEW, TransactionalResult, ConsumedTask, IN_PROGRESS, \ - ConsumedTaskResult, FINISHED, OnTaskCallback +from task_manager.core.storage import ( + StorageInterface, TransactionalResult, + ConsumedTask, OnTaskCallback +) +from task_manager.core.tasks import Task, TaskStatus + + +class ConsumedTaskResult(TransactionalResult[ConsumedTask]): + def __init__(self, task: Task, lock: asyncio.Lock): + self.task = task + self.lock = lock + + async def get_data(self) -> ConsumedTask: + return ConsumedTask(self.task.idn, self.task.topic, self.task.payload) + + async def commit(self): + self.lock.release() + + async def rollback(self): + self.task.status = TaskStatus.NEW + self.lock.release() # todo: refactor this horror @@ -13,7 +32,7 @@ def __init__(self): self.on_task_callbacks = [] async def create_task(self, queue, payload) -> str: - task = Task(str(uuid.uuid4()), queue, payload, NEW, None) + task = Task(str(uuid.uuid4()), queue, payload, TaskStatus.NEW, None) self.tasks.append(task) for callback in self.on_task_callbacks: @@ -25,8 +44,8 @@ async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: await self.lock.acquire() for task in self.tasks: - if task.idn == idn and task.status == NEW: - task.status = IN_PROGRESS + if task.idn == idn and task.status == TaskStatus.NEW: + task.status = TaskStatus.IN_PROGRESS return ConsumedTaskResult( task, self.lock @@ -39,8 +58,8 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con await self.lock.acquire() for task in self.tasks: - if task.topic in topics and task.status == NEW: - task.status = IN_PROGRESS + if task.topic in topics and task.status == TaskStatus.NEW: + task.status = TaskStatus.IN_PROGRESS return ConsumedTaskResult( task, self.lock @@ -52,7 +71,7 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con async def finish_task(self, idn: str, error: None | str = None, message: str = ''): for task in self.tasks: if task.idn == idn: - task.status = FINISHED + task.status = TaskStatus.FINISHED task.error = error task.message = message return diff --git a/test_postgres_storage.py b/test_postgres_storage.py index 64d6b4f..7d5879d 100644 --- a/test_postgres_storage.py +++ b/test_postgres_storage.py @@ -26,7 +26,6 @@ async def consumer(): async def main(): - await storage.create_connection() asyncio.create_task(make_consumer("1 message", 1)) asyncio.create_task(make_consumer("permanent", 100)) From 8c8b6fdad3bd0e4584019716c1b8326b42d5ebaa Mon Sep 17 00:00:00 2001 From: "codefather.ua" Date: Thu, 7 Mar 2024 17:07:06 +0200 Subject: [PATCH 7/8] postgres pub/sub, + refactoring --- Dockerfile | 2 +- Pipfile | 6 +- README.md | 22 ++- docker-compose.yml | 2 +- entrypoint.sh | 30 ++-- junk/synthetic_test.py | 1 + task_manager/core/manager.py | 2 +- task_manager/storage/asyncpg_storage.py | 198 +++++++++++++++++------- task_manager/storage/utils.py | 6 + 9 files changed, 189 insertions(+), 80 deletions(-) diff --git a/Dockerfile b/Dockerfile index ba0e405..8a221a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,6 @@ RUN pip install --upgrade pip RUN pip install pipenv RUN pipenv install -RUN chmod +x /usr/src/app/entrypoint.sh +#RUN chmod +x /usr/src/app/entrypoint.sh EXPOSE $APP_PORT diff --git a/Pipfile b/Pipfile index 3da1890..fdedaa2 100644 --- a/Pipfile +++ b/Pipfile @@ -4,9 +4,9 @@ verify_ssl = true name = "pypi" [packages] -pydantic = "~=2.6" -aiohttp = "~=3.9" -asyncpg = "~=0.29" +pydantic = "2.6" +aiohttp = "3.9" +asyncpg = "0.29" [dev-packages] diff --git a/README.md b/README.md index b5086a9..a940ba1 100644 --- a/README.md +++ b/README.md @@ -1 +1,21 @@ -# Task Manager \ No newline at end of file +# Task Manager + +## Quick start +```bash +# build +docker-compose build --no-cache +``` +```bash +# up +docker-compose up +``` +It starts `synthetic_test.py` by default. + + +## Docker env example +- POSTGRES_USER=task_manager +- POSTGRES_PASS=task_manager_password +- POSTGRES_NAME=task_manager +- POSTGRES_HOST=postgres +- POSTGRES_PORT=5432 +- POSTGRES_HOST_AUTH_METHOD=trust \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 97a27dd..8692fde 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,7 @@ services: condition: service_healthy links: - postgres - entrypoint: sh entrypoint.sh +# entrypoint: sh entrypoint.sh command: pipenv run python3 junk/synthetic_test.py asyncpg_storage postgres: diff --git a/entrypoint.sh b/entrypoint.sh index cfc84b4..271f03d 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,15 +1,17 @@ -echo "--- starts entrypoint" -#!/bin/sh +# DEPRECATED -# create db and role if not exists -psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -tc "SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_NAME'" | - grep -q 1 || - psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "CREATE USER $POSTGRES_USER WITH ENCRYPTED PASSWORD '$POSTGRES_PASS';" - psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "CREATE DATABASE $POSTGRES_NAME;" - psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "GRANT ALL PRIVILEGES ON DATABASE $POSTGRES_NAME TO $POSTGRES_USER;" - -# run migrations -echo "--- starts migrations" -psql "user=$POSTGRES_USER host=$POSTGRES_HOST port=$POSTGRES_PORT password=$POSTGRES_PASS dbname=$POSTGRES_NAME" -f ./sqlmigrations/001_create_schema.sql -echo "--- ends entrypoint" -exec "$@" \ No newline at end of file +#echo "--- starts entrypoint" +##!/bin/sh +# +## create db and role if not exists +#psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -tc "SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_NAME'" | +# grep -q 1 || +# psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "CREATE USER $POSTGRES_USER WITH ENCRYPTED PASSWORD '$POSTGRES_PASS';" +# psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "CREATE DATABASE $POSTGRES_NAME;" +# psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "GRANT ALL PRIVILEGES ON DATABASE $POSTGRES_NAME TO $POSTGRES_USER;" +# +## run migrations +#echo "--- starts migrations" +#psql "user=$POSTGRES_USER host=$POSTGRES_HOST port=$POSTGRES_PORT password=$POSTGRES_PASS dbname=$POSTGRES_NAME" -f ./sqlmigrations/001_create_schema.sql +#echo "--- ends entrypoint" +#exec "$@" diff --git a/junk/synthetic_test.py b/junk/synthetic_test.py index 7b4c5cc..8f12517 100644 --- a/junk/synthetic_test.py +++ b/junk/synthetic_test.py @@ -44,6 +44,7 @@ async def consumer(): task = await session.consume_task(['test']) print(f"consumer '{name}' got task '{task.idn}' with payload {task.payload} in '{task.topic}'") await asyncio.sleep(3) + await engine.finish_task(task.idn, None) print(f"consumer '{name}' finished task '{task.idn}' with payload {task.payload} in '{task.topic}'") return consumer() diff --git a/task_manager/core/manager.py b/task_manager/core/manager.py index 6366967..0f3708f 100644 --- a/task_manager/core/manager.py +++ b/task_manager/core/manager.py @@ -22,7 +22,7 @@ def close_session(self, session: Session): async def create_task(self, queue, payload): return await self.storage.create_task(queue, payload) - async def finish_task(self, idn: str, error: None|str, message: str = ''): + async def finish_task(self, idn: str, error: None | str, message: str = ''): await self.storage.finish_task(idn, error, message) async def take_pending_task(self, topics: list[str]): diff --git a/task_manager/storage/asyncpg_storage.py b/task_manager/storage/asyncpg_storage.py index 3f3cc2a..810494d 100644 --- a/task_manager/storage/asyncpg_storage.py +++ b/task_manager/storage/asyncpg_storage.py @@ -1,13 +1,13 @@ import asyncio import json import os +from pathlib import Path import uuid from dataclasses import dataclass -from typing import Optional, Tuple +from typing import Optional, Tuple, List import asyncpg from asyncpg import Connection, Pool -from asyncpg.prepared_stmt import PreparedStatement from asyncpg.transaction import Transaction from task_manager.core.tasks import ConsumedTask, TaskStatus @@ -16,6 +16,7 @@ ) from task_manager.core.utils import UUIDEncoder from task_manager.storage.in_memory import Task +from task_manager.storage.utils import generator @dataclass @@ -36,10 +37,10 @@ def __init__( self.task = task self.lock = lock - async def get_data(self) -> ConsumedTask: - # TODO pg impl - return ConsumedTask(self.task.idn, self.task.topic, self.task.payload) - # ... + async def get_data(self) -> AsyncPGTask: + # TODO another returning dataclass impl ? + # TODO I think current AsyncPGTask impl not bed + return self.task async def commit(self): await self.lock.commit() @@ -49,8 +50,9 @@ async def rollback(self): class AsyncPGStorage(StorageInterface): - def __init__(self): + def __init__(self, subscription_channel: str = 'tasks'): self.pool: Optional[Pool] = None + self.subscription_channel = subscription_channel self.subscribe_conn: Optional[Connection] = None self.connection_data = { "user": os.environ.get("POSTGRES_USER", "postgres"), @@ -59,15 +61,76 @@ def __init__(self): "host": os.environ.get("POSTGRES_HOST", "127.0.0.1"), "port": os.environ.get("POSTGRES_PORT", "5432"), } - asyncio.get_event_loop().run_until_complete( + loop = asyncio.get_event_loop() + loop.run_until_complete( self.create_connection() ) + loop.run_until_complete( + self.__create_schema() + ) + loop.run_until_complete( + self.create_subscriber() + ) + + self.tasks = [] # FIXME what is this ? + self.on_task_callbacks: List[OnTaskCallback] = [] - self.tasks = [] # todo postgres impl + async def __create_schema(self): + project_dir = str(Path( + os.path.dirname(os.path.realpath(__file__)) + ).resolve().parents[1]) + + sql_migrations_fir_path = os.path.join( + project_dir, 'sqlmigrations' + ) + files = [] + + # async loop is noway + for migration_file in os.listdir(sql_migrations_fir_path): + file = open( + os.path.join( + sql_migrations_fir_path, + migration_file + ) + ) + files.append(file.read()) + file.close() - # todo postgres impl - # self.conn.add_listener() ? - self.on_task_callbacks = [] + async with self.pool.acquire() as connection: + connection: Connection + + await connection.execute( + "\r".join(files) + ) + + async def __subscription_callback( + self, + connection: Connection, + pid: int, + channel: str, + payload: str + ): + """ + :param callable callback: + A callable or a coroutine function receiving the following + arguments: + **connection**: a Connection the callback is registered with; + **pid**: PID of the Postgres server that sent the notification; + **channel**: name of the channel the notification was sent to; + **payload**: task idn (str[uuid]). + :return: + """ + + async for callback in generator(self.on_task_callbacks): + print(f"callback from postgres: {payload}") + await callback(payload) + + async def create_subscriber(self): + + await self.subscribe_conn.add_listener( + channel=self.subscription_channel, + callback=self.__subscription_callback + ) async def create_connection(self): if not self.pool or self.pool.is_closing(): @@ -76,7 +139,9 @@ async def create_connection(self): max_size=10 ) if not self.subscribe_conn or self.subscribe_conn.is_closed(): - self.subscribe_conn = self.pool.acquire() + self.subscribe_conn = await asyncpg.connect( + **self.connection_data + ) async def close_connection(self): if self.subscribe_conn: @@ -86,6 +151,10 @@ async def close_connection(self): await self.pool.close() if self.pool else None async def __create_transaction(self) -> Tuple[Transaction, Connection]: + """ + for transfer transaction control flow + to the Session -> AsyncPGConsumedTaskResult -> (commit/rollback) + """ conn: Connection = await self.pool.acquire() transaction: Transaction = conn.transaction() await transaction.start() @@ -108,18 +177,17 @@ async def create_task(self, queue, payload) -> str: f"'{idn}', '{queue}', '{json.dumps(payload)}', {TaskStatus.NEW}" \ ")" - transaction, conn = await self.__create_transaction() - await conn.fetch(q) - await transaction.commit() - await self.pool.release(conn) + async with self.pool.acquire() as connection: + connection: Connection + await connection.execute(q) + + if self.on_task_callbacks: + notify_q = f"NOTIFY {self.subscription_channel}, '{idn}';" + await connection.execute(notify_q) # FIXME what is this ? # self.tasks.append(task) - # todo postgres impl - for callback in self.on_task_callbacks: - asyncio.create_task(callback(idn)) - return idn async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: @@ -133,14 +201,20 @@ async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: """ transaction, conn = await self.__create_transaction() - q = "SELECT * FROM tasks " \ - f"WHERE (idn='{idn}' and status={TaskStatus.NEW}) " \ - "FOR UPDATE;" - await conn.fetchrow(q) - - q = ("UPDATE tasks " - f"SET status = {TaskStatus.IN_PROGRESS} " - f"WHERE (idn='{idn}' and status={TaskStatus.NEW}) RETURNING * ;") + + q = ( + "WITH selected_for_update as (" + "SELECT * FROM tasks " + f"WHERE (idn='{idn}' and status={TaskStatus.NEW}) " + "FOR UPDATE" + ") " + "UPDATE tasks " + f"SET status = {TaskStatus.IN_PROGRESS} " + f"FROM selected_for_update " + f"WHERE (tasks.idn=selected_for_update.idn) " + f"RETURNING * ;" + ) + rec: asyncpg.Record = await conn.fetchrow(q) if rec: @@ -165,19 +239,19 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con """ transaction, conn = await self.__create_transaction() - q = "SELECT * FROM tasks " \ - f"WHERE (" \ - f"status = {TaskStatus.NEW} and " \ - f"topic in ({str([f'{i}' for i in topics])[1:-1]})) " \ - "FOR UPDATE;" - await conn.fetchrow(q) q = ( - "UPDATE tasks " - f"SET status = {TaskStatus.IN_PROGRESS} " - f"WHERE (" + "WITH selected_for_update as (" + "SELECT * FROM tasks " + "WHERE (" f"status = {TaskStatus.NEW} and " f"topic in ({str([f'{i}' for i in topics])[1:-1]})) " + "FOR UPDATE" + ") " + "UPDATE tasks " + f"SET status = {TaskStatus.IN_PROGRESS} " + f"FROM selected_for_update " + f"WHERE tasks.idn = selected_for_update.idn " f"RETURNING * ;" ) @@ -197,34 +271,40 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con async def finish_task(self, idn: str, error: None | str = None, message: str = ''): - transaction, conn = await self.__create_transaction() - q = "SELECT * FROM tasks " \ - f"WHERE idn = '{idn}'" \ - "FOR UPDATE;" - await conn.fetchrow(q) + bool_map = { + True: 'true', + False: 'false', + None: 'null' + } q = ( + "WITH selected_for_update as (" + "SELECT * FROM tasks " + f"WHERE idn = '{idn}' " + f"FOR UPDATE " + ") " "UPDATE tasks " f"SET status = {TaskStatus.FINISHED}, " - f"error = {error}, " - f"description = {message} " - f"WHERE idn = '{idn}'" + f"error = {bool_map[error]}, " + f"description = {message if message else bool_map[None]} " + f"FROM selected_for_update " + f"WHERE tasks.idn = selected_for_update.idn " f"RETURNING *;" ) - rec: asyncpg.Record = await conn.fetchrow(q) + async with self.pool.acquire() as connection: + connection: Connection - if not rec: - await transaction.rollback() - await self.pool.release(conn) - # todo: add exceptions inheritance level to interface and raise appropriate one - raise Exception(f"Can't finish task. Task '{idn}' not found in storage") + async with connection.transaction() as transaction: + transaction: Transaction - def add_on_task_callback(self, callback: OnTaskCallback, channel: str = 'tasks'): - # todo postgres impl - self.on_task_callbacks.append(callback) + rec: asyncpg.Record = await connection.fetchrow(q) - # self.subscribe_conn.add_listener( - # channel=channel, - # callback=callback - # ) + if not rec: + await transaction.rollback() + await self.pool.release(connection) + # todo: add exceptions inheritance level to interface and raise appropriate one + raise Exception(f"Can't finish task. Task '{idn}' not found in storage") + + def add_on_task_callback(self, callback: OnTaskCallback): + self.on_task_callbacks.append(callback) diff --git a/task_manager/storage/utils.py b/task_manager/storage/utils.py index e69de29..0a66a47 100644 --- a/task_manager/storage/utils.py +++ b/task_manager/storage/utils.py @@ -0,0 +1,6 @@ +from typing import Iterable + + +async def generator(iterable: Iterable): + for i in iterable: + yield i From 4fe6132cc538ee9bc1e8fbe1cfc7fabd75216be7 Mon Sep 17 00:00:00 2001 From: "vlad.benkovskyi" Date: Fri, 15 Mar 2024 18:38:22 +0200 Subject: [PATCH 8/8] pull requests updates --- Dockerfile | 1 - Pipfile.lock | 570 ------------------ docker-compose.yml | 1 - entrypoint.sh | 17 - junk/synthetic_test.py | 15 +- task_manager/core/storage.py | 12 - task_manager/core/tasks.py | 13 - task_manager/storage/asyncpg_storage.py | 39 +- task_manager/storage/in_memory.py | 2 +- .../sql_migrations}/001_create_schema.sql | 0 task_manager/storage/tasks.py | 24 + test_postgres_storage.py | 86 --- 12 files changed, 51 insertions(+), 729 deletions(-) delete mode 100644 Pipfile.lock delete mode 100644 entrypoint.sh rename {sqlmigrations => task_manager/storage/resources/sql_migrations}/001_create_schema.sql (100%) create mode 100644 task_manager/storage/tasks.py delete mode 100644 test_postgres_storage.py diff --git a/Dockerfile b/Dockerfile index 8a221a4..85e9afe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,6 +29,5 @@ RUN pip install --upgrade pip RUN pip install pipenv RUN pipenv install -#RUN chmod +x /usr/src/app/entrypoint.sh EXPOSE $APP_PORT diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index b00403d..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,570 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "98d8f0c8970e1948e2c96defecc6f51a7e6078e85ac16bea988b0b40190c8741" - }, - "pipfile-spec": 6, - "requires": { - "python_full_version": "3.11", - "python_version": "3.11" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "aiohttp": { - "hashes": [ - "sha256:017a21b0df49039c8f46ca0971b3a7fdc1f56741ab1240cb90ca408049766168", - "sha256:039df344b45ae0b34ac885ab5b53940b174530d4dd8a14ed8b0e2155b9dddccb", - "sha256:055ce4f74b82551678291473f66dc9fb9048a50d8324278751926ff0ae7715e5", - "sha256:06a9b2c8837d9a94fae16c6223acc14b4dfdff216ab9b7202e07a9a09541168f", - "sha256:07b837ef0d2f252f96009e9b8435ec1fef68ef8b1461933253d318748ec1acdc", - "sha256:0ed621426d961df79aa3b963ac7af0d40392956ffa9be022024cd16297b30c8c", - "sha256:0fa43c32d1643f518491d9d3a730f85f5bbaedcbd7fbcae27435bb8b7a061b29", - "sha256:1f5a71d25cd8106eab05f8704cd9167b6e5187bcdf8f090a66c6d88b634802b4", - "sha256:1f5cd333fcf7590a18334c90f8c9147c837a6ec8a178e88d90a9b96ea03194cc", - "sha256:27468897f628c627230dba07ec65dc8d0db566923c48f29e084ce382119802bc", - "sha256:298abd678033b8571995650ccee753d9458dfa0377be4dba91e4491da3f2be63", - "sha256:2c895a656dd7e061b2fd6bb77d971cc38f2afc277229ce7dd3552de8313a483e", - "sha256:361a1026c9dd4aba0109e4040e2aecf9884f5cfe1b1b1bd3d09419c205e2e53d", - "sha256:363afe77cfcbe3a36353d8ea133e904b108feea505aa4792dad6585a8192c55a", - "sha256:38a19bc3b686ad55804ae931012f78f7a534cce165d089a2059f658f6c91fa60", - "sha256:38f307b41e0bea3294a9a2a87833191e4bcf89bb0365e83a8be3a58b31fb7f38", - "sha256:3e59c23c52765951b69ec45ddbbc9403a8761ee6f57253250c6e1536cacc758b", - "sha256:4b4af9f25b49a7be47c0972139e59ec0e8285c371049df1a63b6ca81fdd216a2", - "sha256:504b6981675ace64c28bf4a05a508af5cde526e36492c98916127f5a02354d53", - "sha256:50fca156d718f8ced687a373f9e140c1bb765ca16e3d6f4fe116e3df7c05b2c5", - "sha256:522a11c934ea660ff8953eda090dcd2154d367dec1ae3c540aff9f8a5c109ab4", - "sha256:52df73f14ed99cee84865b95a3d9e044f226320a87af208f068ecc33e0c35b96", - "sha256:595f105710293e76b9dc09f52e0dd896bd064a79346234b521f6b968ffdd8e58", - "sha256:59c26c95975f26e662ca78fdf543d4eeaef70e533a672b4113dd888bd2423caa", - "sha256:5bce0dc147ca85caa5d33debc4f4d65e8e8b5c97c7f9f660f215fa74fc49a321", - "sha256:5eafe2c065df5401ba06821b9a054d9cb2848867f3c59801b5d07a0be3a380ae", - "sha256:5ed3e046ea7b14938112ccd53d91c1539af3e6679b222f9469981e3dac7ba1ce", - "sha256:5fe9ce6c09668063b8447f85d43b8d1c4e5d3d7e92c63173e6180b2ac5d46dd8", - "sha256:648056db9a9fa565d3fa851880f99f45e3f9a771dd3ff3bb0c048ea83fb28194", - "sha256:69361bfdca5468c0488d7017b9b1e5ce769d40b46a9f4a2eed26b78619e9396c", - "sha256:6b0e029353361f1746bac2e4cc19b32f972ec03f0f943b390c4ab3371840aabf", - "sha256:6b88f9386ff1ad91ace19d2a1c0225896e28815ee09fc6a8932fded8cda97c3d", - "sha256:770d015888c2a598b377bd2f663adfd947d78c0124cfe7b959e1ef39f5b13869", - "sha256:7943c414d3a8d9235f5f15c22ace69787c140c80b718dcd57caaade95f7cd93b", - "sha256:7cf5c9458e1e90e3c390c2639f1017a0379a99a94fdfad3a1fd966a2874bba52", - "sha256:7f46acd6a194287b7e41e87957bfe2ad1ad88318d447caf5b090012f2c5bb528", - "sha256:82e6aa28dd46374f72093eda8bcd142f7771ee1eb9d1e223ff0fa7177a96b4a5", - "sha256:835a55b7ca49468aaaac0b217092dfdff370e6c215c9224c52f30daaa735c1c1", - "sha256:84871a243359bb42c12728f04d181a389718710129b36b6aad0fc4655a7647d4", - "sha256:8aacb477dc26797ee089721536a292a664846489c49d3ef9725f992449eda5a8", - "sha256:8e2c45c208c62e955e8256949eb225bd8b66a4c9b6865729a786f2aa79b72e9d", - "sha256:90842933e5d1ff760fae6caca4b2b3edba53ba8f4b71e95dacf2818a2aca06f7", - "sha256:938a9653e1e0c592053f815f7028e41a3062e902095e5a7dc84617c87267ebd5", - "sha256:939677b61f9d72a4fa2a042a5eee2a99a24001a67c13da113b2e30396567db54", - "sha256:9d3c9b50f19704552f23b4eaea1fc082fdd82c63429a6506446cbd8737823da3", - "sha256:a6fe5571784af92b6bc2fda8d1925cccdf24642d49546d3144948a6a1ed58ca5", - "sha256:a78ed8a53a1221393d9637c01870248a6f4ea5b214a59a92a36f18151739452c", - "sha256:ab40e6251c3873d86ea9b30a1ac6d7478c09277b32e14745d0d3c6e76e3c7e29", - "sha256:abf151955990d23f84205286938796c55ff11bbfb4ccfada8c9c83ae6b3c89a3", - "sha256:acef0899fea7492145d2bbaaaec7b345c87753168589cc7faf0afec9afe9b747", - "sha256:b40670ec7e2156d8e57f70aec34a7216407848dfe6c693ef131ddf6e76feb672", - "sha256:b791a3143681a520c0a17e26ae7465f1b6f99461a28019d1a2f425236e6eedb5", - "sha256:b955ed993491f1a5da7f92e98d5dad3c1e14dc175f74517c4e610b1f2456fb11", - "sha256:ba39e9c8627edc56544c8628cc180d88605df3892beeb2b94c9bc857774848ca", - "sha256:bca77a198bb6e69795ef2f09a5f4c12758487f83f33d63acde5f0d4919815768", - "sha256:c3452ea726c76e92f3b9fae4b34a151981a9ec0a4847a627c43d71a15ac32aa6", - "sha256:c46956ed82961e31557b6857a5ca153c67e5476972e5f7190015018760938da2", - "sha256:c7c8b816c2b5af5c8a436df44ca08258fc1a13b449393a91484225fcb7545533", - "sha256:cd73265a9e5ea618014802ab01babf1940cecb90c9762d8b9e7d2cc1e1969ec6", - "sha256:dad46e6f620574b3b4801c68255492e0159d1712271cc99d8bdf35f2043ec266", - "sha256:dc9b311743a78043b26ffaeeb9715dc360335e5517832f5a8e339f8a43581e4d", - "sha256:df822ee7feaaeffb99c1a9e5e608800bd8eda6e5f18f5cfb0dc7eeb2eaa6bbec", - "sha256:e083c285857b78ee21a96ba1eb1b5339733c3563f72980728ca2b08b53826ca5", - "sha256:e5e46b578c0e9db71d04c4b506a2121c0cb371dd89af17a0586ff6769d4c58c1", - "sha256:e99abf0bba688259a496f966211c49a514e65afa9b3073a1fcee08856e04425b", - "sha256:ee43080e75fc92bf36219926c8e6de497f9b247301bbf88c5c7593d931426679", - "sha256:f033d80bc6283092613882dfe40419c6a6a1527e04fc69350e87a9df02bbc283", - "sha256:f1088fa100bf46e7b398ffd9904f4808a0612e1d966b4aa43baa535d1b6341eb", - "sha256:f56455b0c2c7cc3b0c584815264461d07b177f903a04481dfc33e08a89f0c26b", - "sha256:f59dfe57bb1ec82ac0698ebfcdb7bcd0e99c255bd637ff613760d5f33e7c81b3", - "sha256:f7217af2e14da0856e082e96ff637f14ae45c10a5714b63c77f26d8884cf1051", - "sha256:f734e38fd8666f53da904c52a23ce517f1b07722118d750405af7e4123933511", - "sha256:f95511dd5d0e05fd9728bac4096319f80615aaef4acbecb35a990afebe953b0e", - "sha256:fdd215b7b7fd4a53994f238d0f46b7ba4ac4c0adb12452beee724ddd0743ae5d", - "sha256:feeb18a801aacb098220e2c3eea59a512362eb408d4afd0c242044c33ad6d542", - "sha256:ff30218887e62209942f91ac1be902cc80cddb86bf00fbc6783b7a43b2bea26f" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==3.9.3" - }, - "aiosignal": { - "hashes": [ - "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", - "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17" - ], - "markers": "python_version >= '3.7'", - "version": "==1.3.1" - }, - "annotated-types": { - "hashes": [ - "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", - "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" - ], - "markers": "python_version >= '3.8'", - "version": "==0.6.0" - }, - "async-timeout": { - "hashes": [ - "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f", - "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028" - ], - "markers": "python_full_version < '3.12.0'", - "version": "==4.0.3" - }, - "asyncpg": { - "hashes": [ - "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9", - "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7", - "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548", - "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23", - "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3", - "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675", - "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe", - "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175", - "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83", - "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385", - "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da", - "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106", - "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870", - "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449", - "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc", - "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178", - "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9", - "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b", - "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169", - "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610", - "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772", - "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2", - "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c", - "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb", - "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac", - "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408", - "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22", - "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb", - "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02", - "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59", - "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8", - "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3", - "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e", - "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4", - "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364", - "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f", - "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775", - "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3", - "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090", - "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810", - "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.0'", - "version": "==0.29.0" - }, - "attrs": { - "hashes": [ - "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30", - "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1" - ], - "markers": "python_version >= '3.7'", - "version": "==23.2.0" - }, - "frozenlist": { - "hashes": [ - "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7", - "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98", - "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad", - "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5", - "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae", - "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e", - "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a", - "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701", - "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d", - "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6", - "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6", - "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106", - "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75", - "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868", - "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a", - "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0", - "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1", - "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826", - "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec", - "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6", - "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950", - "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19", - "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0", - "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8", - "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a", - "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09", - "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86", - "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c", - "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5", - "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b", - "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b", - "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d", - "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0", - "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea", - "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776", - "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a", - "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897", - "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7", - "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09", - "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9", - "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe", - "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd", - "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742", - "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09", - "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0", - "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932", - "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1", - "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a", - "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49", - "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d", - "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7", - "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480", - "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89", - "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e", - "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b", - "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82", - "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb", - "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068", - "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8", - "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b", - "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb", - "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2", - "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11", - "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b", - "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc", - "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0", - "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497", - "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17", - "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0", - "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2", - "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439", - "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5", - "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac", - "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825", - "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887", - "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced", - "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74" - ], - "markers": "python_version >= '3.8'", - "version": "==1.4.1" - }, - "idna": { - "hashes": [ - "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", - "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f" - ], - "markers": "python_version >= '3.5'", - "version": "==3.6" - }, - "multidict": { - "hashes": [ - "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", - "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", - "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", - "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", - "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", - "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", - "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", - "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", - "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", - "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", - "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", - "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", - "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", - "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", - "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", - "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", - "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", - "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", - "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", - "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", - "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", - "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", - "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", - "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", - "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", - "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", - "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", - "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", - "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", - "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", - "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", - "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", - "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", - "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", - "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", - "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", - "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", - "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", - "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", - "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", - "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", - "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", - "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", - "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", - "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", - "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", - "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", - "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", - "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", - "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", - "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", - "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", - "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", - "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", - "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", - "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", - "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", - "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", - "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", - "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", - "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", - "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", - "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", - "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", - "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", - "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", - "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", - "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", - "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", - "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", - "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", - "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", - "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", - "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", - "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", - "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", - "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", - "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", - "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", - "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", - "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", - "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", - "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", - "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", - "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", - "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", - "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", - "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", - "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", - "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.5" - }, - "pydantic": { - "hashes": [ - "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a", - "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.6.3" - }, - "pydantic-core": { - "hashes": [ - "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a", - "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed", - "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979", - "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff", - "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5", - "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45", - "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340", - "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad", - "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23", - "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6", - "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7", - "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241", - "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda", - "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187", - "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba", - "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c", - "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2", - "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c", - "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132", - "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf", - "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972", - "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db", - "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade", - "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4", - "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8", - "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f", - "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9", - "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48", - "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec", - "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d", - "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9", - "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb", - "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4", - "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89", - "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c", - "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9", - "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da", - "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac", - "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b", - "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf", - "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e", - "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137", - "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1", - "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b", - "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8", - "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e", - "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053", - "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01", - "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe", - "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd", - "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805", - "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183", - "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8", - "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99", - "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820", - "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074", - "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256", - "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8", - "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975", - "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad", - "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e", - "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca", - "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df", - "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b", - "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a", - "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a", - "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721", - "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a", - "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f", - "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2", - "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97", - "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6", - "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed", - "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc", - "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1", - "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe", - "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120", - "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f", - "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a" - ], - "markers": "python_version >= '3.8'", - "version": "==2.16.3" - }, - "typing-extensions": { - "hashes": [ - "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", - "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" - ], - "markers": "python_version >= '3.8'", - "version": "==4.10.0" - }, - "yarl": { - "hashes": [ - "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", - "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", - "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", - "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", - "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", - "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", - "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", - "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", - "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", - "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", - "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", - "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", - "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", - "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", - "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", - "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", - "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", - "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", - "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", - "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", - "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", - "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", - "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", - "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", - "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", - "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", - "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", - "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", - "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", - "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", - "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", - "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", - "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", - "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", - "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", - "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", - "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", - "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", - "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", - "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", - "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", - "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", - "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", - "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", - "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", - "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", - "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", - "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", - "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", - "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", - "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", - "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", - "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", - "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", - "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", - "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", - "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", - "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", - "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", - "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", - "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", - "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", - "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", - "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", - "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", - "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", - "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", - "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", - "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", - "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", - "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", - "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", - "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", - "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", - "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", - "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", - "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", - "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", - "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", - "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", - "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", - "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", - "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", - "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", - "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", - "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", - "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", - "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", - "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", - "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" - ], - "markers": "python_version >= '3.7'", - "version": "==1.9.4" - } - }, - "develop": {} -} diff --git a/docker-compose.yml b/docker-compose.yml index 8692fde..772c2c9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,7 +16,6 @@ services: condition: service_healthy links: - postgres -# entrypoint: sh entrypoint.sh command: pipenv run python3 junk/synthetic_test.py asyncpg_storage postgres: diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 271f03d..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,17 +0,0 @@ -# DEPRECATED - -#echo "--- starts entrypoint" -##!/bin/sh -# -## create db and role if not exists -#psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -tc "SELECT 1 FROM pg_database WHERE datname = '$POSTGRES_NAME'" | -# grep -q 1 || -# psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "CREATE USER $POSTGRES_USER WITH ENCRYPTED PASSWORD '$POSTGRES_PASS';" -# psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "CREATE DATABASE $POSTGRES_NAME;" -# psql -U $POSTGRES_USER -h $POSTGRES_HOST -p $POSTGRES_PORT -c "GRANT ALL PRIVILEGES ON DATABASE $POSTGRES_NAME TO $POSTGRES_USER;" -# -## run migrations -#echo "--- starts migrations" -#psql "user=$POSTGRES_USER host=$POSTGRES_HOST port=$POSTGRES_PORT password=$POSTGRES_PASS dbname=$POSTGRES_NAME" -f ./sqlmigrations/001_create_schema.sql -#echo "--- ends entrypoint" -#exec "$@" diff --git a/junk/synthetic_test.py b/junk/synthetic_test.py index 8f12517..24d957c 100644 --- a/junk/synthetic_test.py +++ b/junk/synthetic_test.py @@ -10,7 +10,20 @@ from task_manager.core import TaskManager from task_manager.core import SessionClosedException -from task_manager.core.storage import StorageType, StorageInterface +from task_manager.core.storage import StorageInterface + + +class StorageType: + IN_MEMORY_STORAGE = 'in_memory_storage' + ASYNCPG_STORAGE = 'asyncpg_storage' + + @staticmethod + def choices(): + return [ + StorageType.IN_MEMORY_STORAGE, + StorageType.ASYNCPG_STORAGE + ] + parser = argparse.ArgumentParser() diff --git a/task_manager/core/storage.py b/task_manager/core/storage.py index 561a656..748493e 100644 --- a/task_manager/core/storage.py +++ b/task_manager/core/storage.py @@ -32,15 +32,3 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con def add_on_task_callback(self, callback: OnTaskCallback): raise NotImplemented() - - -class StorageType: - IN_MEMORY_STORAGE = 'in_memory_storage' - ASYNCPG_STORAGE = 'asyncpg_storage' - - @staticmethod - def choices(): - return [ - StorageType.IN_MEMORY_STORAGE, - StorageType.ASYNCPG_STORAGE - ] diff --git a/task_manager/core/tasks.py b/task_manager/core/tasks.py index fd6b8b5..50669d4 100644 --- a/task_manager/core/tasks.py +++ b/task_manager/core/tasks.py @@ -1,21 +1,8 @@ from dataclasses import dataclass -class TaskStatus: - NEW = 0 - IN_PROGRESS = 1 - FINISHED = 2 - - @dataclass class ConsumedTask: idn: str topic: str payload: dict - - -@dataclass -class Task(ConsumedTask): - status: int - error: None | str - description: str = '' diff --git a/task_manager/storage/asyncpg_storage.py b/task_manager/storage/asyncpg_storage.py index 810494d..a489e78 100644 --- a/task_manager/storage/asyncpg_storage.py +++ b/task_manager/storage/asyncpg_storage.py @@ -3,50 +3,39 @@ import os from pathlib import Path import uuid -from dataclasses import dataclass from typing import Optional, Tuple, List import asyncpg from asyncpg import Connection, Pool from asyncpg.transaction import Transaction -from task_manager.core.tasks import ConsumedTask, TaskStatus +from task_manager.core.tasks import ConsumedTask from task_manager.core.storage import ( StorageInterface, OnTaskCallback, TransactionalResult ) -from task_manager.core.utils import UUIDEncoder -from task_manager.storage.in_memory import Task +from task_manager.storage.tasks import Task, TaskStatus, create_task_from_dict from task_manager.storage.utils import generator -@dataclass -class AsyncPGTask(Task): - @staticmethod - def from_dict(data: dict): - if isinstance(data.get('payload'), str): - data['payload'] = json.loads(data['payload']) - return AsyncPGTask(**json.loads(json.dumps(data, cls=UUIDEncoder))) - - class AsyncPGConsumedTaskResult(TransactionalResult[ConsumedTask]): def __init__( self, - task: AsyncPGTask, - lock: Transaction + task: Task, + transaction: Transaction ): self.task = task - self.lock = lock + self.transaction = transaction - async def get_data(self) -> AsyncPGTask: + async def get_data(self) -> Task: # TODO another returning dataclass impl ? - # TODO I think current AsyncPGTask impl not bed + # TODO I think current Task impl not bed return self.task async def commit(self): - await self.lock.commit() + await self.transaction.commit() async def rollback(self): - await self.lock.rollback() + await self.transaction.rollback() class AsyncPGStorage(StorageInterface): @@ -72,7 +61,6 @@ def __init__(self, subscription_channel: str = 'tasks'): self.create_subscriber() ) - self.tasks = [] # FIXME what is this ? self.on_task_callbacks: List[OnTaskCallback] = [] async def __create_schema(self): @@ -81,7 +69,7 @@ async def __create_schema(self): ).resolve().parents[1]) sql_migrations_fir_path = os.path.join( - project_dir, 'sqlmigrations' + project_dir, 'task_manager', 'storage', 'resources', 'sql_migrations' ) files = [] @@ -185,9 +173,6 @@ async def create_task(self, queue, payload) -> str: notify_q = f"NOTIFY {self.subscription_channel}, '{idn}';" await connection.execute(notify_q) - # FIXME what is this ? - # self.tasks.append(task) - return idn async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: @@ -218,7 +203,7 @@ async def take_pending(self, idn) -> TransactionalResult[ConsumedTask] | None: rec: asyncpg.Record = await conn.fetchrow(q) if rec: - task = AsyncPGTask.from_dict(dict(rec)) + task = create_task_from_dict(dict(rec)) return AsyncPGConsumedTaskResult( task, transaction @@ -258,7 +243,7 @@ async def take_first_pending(self, topics: list[str]) -> TransactionalResult[Con rec: asyncpg.Record = await conn.fetchrow(q) if rec: - task = AsyncPGTask.from_dict(dict(rec)) + task = create_task_from_dict(dict(rec)) return AsyncPGConsumedTaskResult( task, transaction diff --git a/task_manager/storage/in_memory.py b/task_manager/storage/in_memory.py index 8c437ff..2d361c3 100644 --- a/task_manager/storage/in_memory.py +++ b/task_manager/storage/in_memory.py @@ -5,7 +5,7 @@ StorageInterface, TransactionalResult, ConsumedTask, OnTaskCallback ) -from task_manager.core.tasks import Task, TaskStatus +from task_manager.storage.tasks import Task, TaskStatus class ConsumedTaskResult(TransactionalResult[ConsumedTask]): diff --git a/sqlmigrations/001_create_schema.sql b/task_manager/storage/resources/sql_migrations/001_create_schema.sql similarity index 100% rename from sqlmigrations/001_create_schema.sql rename to task_manager/storage/resources/sql_migrations/001_create_schema.sql diff --git a/task_manager/storage/tasks.py b/task_manager/storage/tasks.py new file mode 100644 index 0000000..631b631 --- /dev/null +++ b/task_manager/storage/tasks.py @@ -0,0 +1,24 @@ +import json +from dataclasses import dataclass + +from task_manager.core import ConsumedTask +from task_manager.core.utils import UUIDEncoder + + +class TaskStatus: + NEW = 0 + IN_PROGRESS = 1 + FINISHED = 2 + + +@dataclass +class Task(ConsumedTask): + status: int + error: None | str + description: str = '' + + +def create_task_from_dict(data: dict): + if isinstance(data.get('payload'), str): + data['payload'] = json.loads(data['payload']) + return Task(**json.loads(json.dumps(data, cls=UUIDEncoder))) diff --git a/test_postgres_storage.py b/test_postgres_storage.py deleted file mode 100644 index 7d5879d..0000000 --- a/test_postgres_storage.py +++ /dev/null @@ -1,86 +0,0 @@ -import asyncio - -from task_manager.core import TaskManager -from task_manager.core import SessionClosedException -from task_manager.storage import InMemoryStorage, AsyncPGStorage - -# storage = InMemoryStorage() -storage = AsyncPGStorage() -engine = TaskManager(storage) - -loop = asyncio.new_event_loop() -asyncio.set_event_loop(loop) - - -def make_consumer(name, max_messages): - async def consumer(): - session = engine.new_session() - - for i in range(max_messages): - task = await session.consume_task(['test']) - print(f"consumer '{name}' got task '{task.idn}' with payload {task.payload} in '{task.topic}'") - await asyncio.sleep(3) - print(f"consumer '{name}' finished task '{task.idn}' with payload {task.payload} in '{task.topic}'") - - return consumer() - - -async def main(): - - asyncio.create_task(make_consumer("1 message", 1)) - asyncio.create_task(make_consumer("permanent", 100)) - - await asyncio.sleep(0.1) - - publisher = engine.new_session() - - # publish when consumers are running - for i in range(3): - print(f'publish {i}') - await publisher.publish_task('test', {"id": i}) - - print(f'publish nonce') - await publisher.publish_task('another', {"nonce": "nonce"}) - - # publish when permanent consumer still in progress - await asyncio.sleep(3) - print(f'publish 3') - await publisher.publish_task('test', {"id": 3}) - - # publish when permanent consumer pending - await asyncio.sleep(7) - - print(f'publish nonce') - await publisher.publish_task('another', {"nonce": "nonce"}) - - print(f'publish 4') - await publisher.publish_task('test', {"id": 4}) - - # publish when permanent consumer in progress and run new consumer after 1 sec passed - print(f'publish 5') - await publisher.publish_task('test', {"id": 5}) - - await asyncio.sleep(1) - asyncio.create_task(make_consumer('new', 100)) - - await asyncio.sleep(3.5) - - # test session closing - session = engine.new_session() - - async def close_session(): - await asyncio.sleep(1) - print('clossing session') - session.close() - - asyncio.create_task(close_session()) - try: - print('waiting for new task') - task = await session.consume_task(['test']) - except SessionClosedException as e: - print('successfully caught exception') - - await asyncio.sleep(2) - - -loop.run_until_complete(main())