diff --git a/.github/workflows/ci.painterapi.yaml b/.github/workflows/ci.api.yaml similarity index 68% rename from .github/workflows/ci.painterapi.yaml rename to .github/workflows/ci.api.yaml index b43d09a..6b3d588 100644 --- a/.github/workflows/ci.painterapi.yaml +++ b/.github/workflows/ci.api.yaml @@ -3,21 +3,18 @@ name: CI Painter API on: push: paths: - - painterapi/** - - .github/workflows/ci.painterapi.yaml + - api/** + - .github/workflows/ci.api.yaml defaults: run: - working-directory: painterapi + working-directory: api jobs: build: runs-on: ubuntu-latest - env: - DISPLAY: :99.0 - steps: - uses: actions/checkout@v2 @@ -27,10 +24,6 @@ jobs: with: python-version: "3.10" - - - run: sudo apt-get install xvfb - - run: Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & - - name: Install pipenv run: python -m pip install --upgrade pipenv diff --git a/api/.env.example b/api/.env.example index 0bac0ab..d40220d 100644 --- a/api/.env.example +++ b/api/.env.example @@ -5,6 +5,9 @@ POSTGRES_DB=artist POSTGRES_USER=local-user POSTGRES_PASSWORD=local-pass +# No display for graphics rendering. +DISPLAY=":99.0" + # Google GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/iam/serice/account.json" GOOGLE_CLOUD_PROJECT="artist-2d" diff --git a/api/.env.test b/api/.env.test index d0fa1f3..3829262 100644 --- a/api/.env.test +++ b/api/.env.test @@ -2,12 +2,16 @@ ENV="test" SECRET_KEY="django-insecure-test-key" PORT=8080 -POSTGRES_HOST=db +# This matches te docker-compose service. +POSTGRES_HOST=db-test POSTGRES_PORT=5432 POSTGRES_DB=artist POSTGRES_USER=postgres POSTGRES_PASSWORD=test-pass +# No display for graphics rendering. +DISPLAY=":99.0" + # Google GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/iam/serice/account.json" GOOGLE_CLOUD_PROJECT="artist-2d" diff --git a/api/Dockerfile b/api/Dockerfile index ed709d8..bad4798 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -20,6 +20,11 @@ RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy FROM base AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends \ + python3-tk \ + ghostscript \ + && rm -rf /var/lib/apt/lists/* + # Copy virtual env from python-deps stage COPY --from=python-deps /.venv /.venv ENV PATH="/.venv/bin:$PATH" diff --git a/api/Makefile b/api/Makefile index edcdb32..e3ce509 100644 --- a/api/Makefile +++ b/api/Makefile @@ -1,12 +1,15 @@ -.PHONY: setup run run-api test testcase deploy clean +.PHONY: setup setup-virtual-display run run-api db-migrate db-makemigrations db-shell test clean setup: pip install --upgrade --user pipenv pipenv install --dev +setup-virtual-display: + pipenv run Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + run: run-api -run-api: +run-api: setup-virtual-display pipenv run docker-compose up -d --build db-migrate: @@ -18,8 +21,8 @@ db-makemigrations: db-shell: pipenv run docker-compose exec db psql artist local-user -test: - pipenv run docker-compose -f docker-compose-test.yml up --build --abort-on-container-exit +test: setup-virtual-display + pipenv run docker-compose -f docker-compose-test.yml up --build --abort-on-container-exit --force-recreate --remove-orphans clean: find . -name '*~' -delete diff --git a/api/Pipfile b/api/Pipfile index 1d69d63..d1f5086 100644 --- a/api/Pipfile +++ b/api/Pipfile @@ -9,6 +9,7 @@ psycopg2-binary = "*" google-cloud-storage = "*" django-cors-headers = "*" django-allauth = {extras = ["socialaccount"], version = "*"} +pillow = "*" [dev-packages] diff --git a/api/Pipfile.lock b/api/Pipfile.lock index 0859d08..5be3b48 100644 --- a/api/Pipfile.lock +++ b/api/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5091a015380693ee93b053a09b500a7e4f06a4ebfaf50f199f1c2835306291d3" + "sha256": "fd0ace03fd174e2dfeeb658f2c0ca37fca09fed66993650b130d748841cd2b8b" }, "pipfile-spec": 6, "requires": { @@ -26,77 +26,92 @@ }, "cachetools": { "hashes": [ - "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945", - "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105" + "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292", + "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a" ], "markers": "python_version >= '3.7'", - "version": "==5.3.3" + "version": "==5.5.0" }, "certifi": { "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.7.4" + "version": "==2024.8.30" }, "cffi": { "hashes": [ - "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc", - "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a", - "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417", - "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab", - "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520", - "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36", - "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743", - "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8", - "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed", - "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684", - "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56", - "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324", - "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d", - "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235", - "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e", - "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088", - "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000", - "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7", - "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e", - "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673", - "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c", - "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe", - "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2", - "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098", - "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8", - "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a", - "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0", - "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b", - "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896", - "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e", - "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9", - "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2", - "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b", - "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6", - "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404", - "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f", - "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0", - "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4", - "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc", - "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936", - "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba", - "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872", - "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb", - "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614", - "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1", - "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d", - "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969", - "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b", - "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4", - "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627", - "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956", - "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357" + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==1.16.0" + "version": "==1.17.1" }, "charset-normalizer": { "hashes": [ @@ -196,60 +211,55 @@ }, "cryptography": { "hashes": [ - "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad", - "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583", - "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b", - "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c", - "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1", - "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648", - "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949", - "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba", - "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c", - "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9", - "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d", - "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c", - "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e", - "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2", - "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d", - "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7", - "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70", - "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2", - "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7", - "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14", - "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe", - "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e", - "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71", - "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961", - "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7", - "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c", - "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28", - "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842", - "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902", - "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801", - "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a", - "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e" + "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", + "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", + "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", + "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", + "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", + "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", + "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", + "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", + "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84", + "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", + "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", + "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", + "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2", + "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", + "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", + "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365", + "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96", + "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", + "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", + "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d", + "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", + "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", + "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", + "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172", + "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034", + "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", + "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" ], "markers": "python_version >= '3.7'", - "version": "==42.0.8" + "version": "==43.0.1" }, "django": { "hashes": [ - "sha256:8363ac062bb4ef7c3f12d078f6fa5d154031d129a15170a1066412af49d30905", - "sha256:ff1b61005004e476e0aeea47c7f79b85864c70124030e95146315396f1e7951f" + "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2", + "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==5.0.6" + "version": "==5.1.1" }, "django-allauth": { "extras": [ "socialaccount" ], "hashes": [ - "sha256:2374164c468a309e6badf70bc3405136df6036f24a20a13387f2a063066bdaa9" + "sha256:54bf0af8dc5c334254dd56f9069447c19b9b623110a095b2a0dcb82a414e1055" ], - "markers": "python_version >= '3.7'", - "version": "==0.63.3" + "markers": "python_version >= '3.8'", + "version": "==64.2.1" }, "django-cors-headers": { "hashes": [ @@ -262,19 +272,19 @@ }, "google-api-core": { "hashes": [ - "sha256:f12a9b8309b5e21d92483bbd47ce2c445861ec7d269ef6784ecc0ea8c1fa6125", - "sha256:f4695f1e3650b316a795108a76a1c416e6afb036199d1c1f1f110916df479ffd" + "sha256:53ec0258f2837dd53bbd3d3df50f5359281b3cc13f800c941dd15a9b5a415af4", + "sha256:ca07de7e8aa1c98a8bfca9321890ad2340ef7f2eb136e558cee68f24b94b0a8f" ], "markers": "python_version >= '3.7'", - "version": "==2.19.1" + "version": "==2.19.2" }, "google-auth": { "hashes": [ - "sha256:042c4702efa9f7d3c48d3a69341c209381b125faa6dbf3ebe56bc7e40ae05c23", - "sha256:87805c36970047247c8afe614d4e3af8eceafc1ebba0c679fe75ddd1d575e871" + "sha256:72fd4733b80b6d777dcde515628a9eb4a577339437012874ea286bca7261ee65", + "sha256:8eb87396435c19b20d32abd2f984e31c191a15284af72eb922f10e5bde9c04cc" ], "markers": "python_version >= '3.7'", - "version": "==2.31.0" + "version": "==2.34.0" }, "google-cloud-core": { "hashes": [ @@ -286,110 +296,69 @@ }, "google-cloud-storage": { "hashes": [ - "sha256:49378abff54ef656b52dca5ef0f2eba9aa83dc2b2c72c78714b03a1a95fe9388", - "sha256:5b393bc766b7a3bc6f5407b9e665b2450d36282614b7945e570b3480a456d1e1" + "sha256:97a4d45c368b7d401ed48c4fdfe86e1e1cb96401c9e199e419d289e2c0370166", + "sha256:aaf7acd70cdad9f274d29332673fcab98708d0e1f4dceb5a5356aaef06af4d99" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==2.17.0" + "version": "==2.18.2" }, "google-crc32c": { "hashes": [ - "sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a", - "sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876", - "sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c", - "sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289", - "sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298", - "sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02", - "sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f", - "sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2", - "sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a", - "sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb", - "sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210", - "sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5", - "sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee", - "sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c", - "sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a", - "sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314", - "sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd", - "sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65", - "sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37", - "sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4", - "sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13", - "sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894", - "sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31", - "sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e", - "sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709", - "sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740", - "sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc", - "sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d", - "sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c", - "sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c", - "sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d", - "sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906", - "sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61", - "sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57", - "sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c", - "sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a", - "sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438", - "sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946", - "sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7", - "sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96", - "sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091", - "sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae", - "sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d", - "sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88", - "sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2", - "sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd", - "sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541", - "sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728", - "sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178", - "sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968", - "sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346", - "sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8", - "sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93", - "sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7", - "sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273", - "sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462", - "sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94", - "sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd", - "sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e", - "sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57", - "sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b", - "sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9", - "sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a", - "sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100", - "sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325", - "sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183", - "sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556", - "sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4" - ], - "markers": "python_version >= '3.7'", - "version": "==1.5.0" + "sha256:05e2d8c9a2f853ff116db9706b4a27350587f341eda835f46db3c0a8c8ce2f24", + "sha256:18e311c64008f1f1379158158bb3f0c8d72635b9eb4f9545f8cf990c5668e59d", + "sha256:236c87a46cdf06384f614e9092b82c05f81bd34b80248021f729396a78e55d7e", + "sha256:35834855408429cecf495cac67ccbab802de269e948e27478b1e47dfb6465e57", + "sha256:386122eeaaa76951a8196310432c5b0ef3b53590ef4c317ec7588ec554fec5d2", + "sha256:40b05ab32a5067525670880eb5d169529089a26fe35dce8891127aeddc1950e8", + "sha256:48abd62ca76a2cbe034542ed1b6aee851b6f28aaca4e6551b5599b6f3ef175cc", + "sha256:50cf2a96da226dcbff8671233ecf37bf6e95de98b2a2ebadbfdf455e6d05df42", + "sha256:51c4f54dd8c6dfeb58d1df5e4f7f97df8abf17a36626a217f169893d1d7f3e9f", + "sha256:5bcc90b34df28a4b38653c36bb5ada35671ad105c99cfe915fb5bed7ad6924aa", + "sha256:62f6d4a29fea082ac4a3c9be5e415218255cf11684ac6ef5488eea0c9132689b", + "sha256:6eceb6ad197656a1ff49ebfbbfa870678c75be4344feb35ac1edf694309413dc", + "sha256:7aec8e88a3583515f9e0957fe4f5f6d8d4997e36d0f61624e70469771584c760", + "sha256:91ca8145b060679ec9176e6de4f89b07363d6805bd4760631ef254905503598d", + "sha256:a184243544811e4a50d345838a883733461e67578959ac59964e43cca2c791e7", + "sha256:a9e4b426c3702f3cd23b933436487eb34e01e00327fac20c9aebb68ccf34117d", + "sha256:bb0966e1c50d0ef5bc743312cc730b533491d60585a9a08f897274e57c3f70e0", + "sha256:bb8b3c75bd157010459b15222c3fd30577042a7060e29d42dabce449c087f2b3", + "sha256:bd5e7d2445d1a958c266bfa5d04c39932dc54093fa391736dbfdb0f1929c1fb3", + "sha256:c87d98c7c4a69066fd31701c4e10d178a648c2cac3452e62c6b24dc51f9fcc00", + "sha256:d2952396dc604544ea7476b33fe87faedc24d666fb0c2d5ac971a2b9576ab871", + "sha256:d8797406499f28b5ef791f339594b0b5fdedf54e203b5066675c406ba69d705c", + "sha256:d9e9913f7bd69e093b81da4535ce27af842e7bf371cde42d1ae9e9bd382dc0e9", + "sha256:e2806553238cd076f0a55bddab37a532b53580e699ed8e5606d0de1f856b5205", + "sha256:ebab974b1687509e5c973b5c4b8b146683e101e102e17a86bd196ecaa4d099fc", + "sha256:ed767bf4ba90104c1216b68111613f0d5926fb3780660ea1198fc469af410e9d", + "sha256:f7a1fc29803712f80879b0806cb83ab24ce62fc8daf0569f2204a0cfd7f68ed4" + ], + "markers": "python_version >= '3.9'", + "version": "==1.6.0" }, "google-resumable-media": { "hashes": [ - "sha256:103ebc4ba331ab1bfdac0250f8033627a2cd7cde09e7ccff9181e31ba4315b2c", - "sha256:eae451a7b2e2cdbaaa0fd2eb00cc8a1ee5e95e16b55597359cbc3d27d7d90e33" + "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", + "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0" ], "markers": "python_version >= '3.7'", - "version": "==2.7.1" + "version": "==2.7.2" }, "googleapis-common-protos": { "hashes": [ - "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945", - "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87" + "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63", + "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0" ], "markers": "python_version >= '3.7'", - "version": "==1.63.2" + "version": "==1.65.0" }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:69297d5da0cc9281c77efffb4e730254dd45943f45bbfb461de5991713989b1e", + "sha256:e5c5dafde284f26e9e0f28f6ea2d6400abd5ca099864a67f576f3981c6476124" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "markers": "python_version >= '3.6'", + "version": "==3.9" }, "oauthlib": { "hashes": [ @@ -399,6 +368,93 @@ "markers": "python_version >= '3.6'", "version": "==3.2.2" }, + "pillow": { + "hashes": [ + "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", + "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", + "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", + "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", + "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", + "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", + "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", + "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", + "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", + "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", + "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", + "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", + "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", + "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", + "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", + "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", + "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", + "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", + "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", + "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", + "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", + "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", + "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", + "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", + "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", + "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", + "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", + "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", + "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", + "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", + "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", + "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", + "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", + "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", + "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", + "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", + "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", + "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", + "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", + "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", + "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", + "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", + "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", + "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", + "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", + "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", + "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", + "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", + "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", + "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", + "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", + "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", + "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", + "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", + "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", + "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", + "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", + "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", + "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", + "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", + "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", + "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", + "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", + "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", + "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", + "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", + "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", + "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", + "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", + "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", + "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", + "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", + "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", + "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", + "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", + "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", + "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", + "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", + "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", + "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==10.4.0" + }, "proto-plus": { "hashes": [ "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445", @@ -409,20 +465,20 @@ }, "protobuf": { "hashes": [ - "sha256:0e341109c609749d501986b835f667c6e1e24531096cff9d34ae411595e26505", - "sha256:176c12b1f1c880bf7a76d9f7c75822b6a2bc3db2d28baa4d300e8ce4cde7409b", - "sha256:354d84fac2b0d76062e9b3221f4abbbacdfd2a4d8af36bab0474f3a0bb30ab38", - "sha256:4fadd8d83e1992eed0248bc50a4a6361dc31bcccc84388c54c86e530b7f58863", - "sha256:54330f07e4949d09614707c48b06d1a22f8ffb5763c159efd5c0928326a91470", - "sha256:610e700f02469c4a997e58e328cac6f305f649826853813177e6290416e846c6", - "sha256:7fc3add9e6003e026da5fc9e59b131b8f22b428b991ccd53e2af8071687b4fce", - "sha256:9e8f199bf7f97bd7ecebffcae45ebf9527603549b2b562df0fbc6d4d688f14ca", - "sha256:a109916aaac42bff84702fb5187f3edadbc7c97fc2c99c5ff81dd15dcce0d1e5", - "sha256:b848dbe1d57ed7c191dfc4ea64b8b004a3f9ece4bf4d0d80a367b76df20bf36e", - "sha256:f3ecdef226b9af856075f28227ff2c90ce3a594d092c39bee5513573f25e2714" + "sha256:0dfd86d2b5edf03d91ec2a7c15b4e950258150f14f9af5f51c17fa224ee1931f", + "sha256:1b04bde117a10ff9d906841a89ec326686c48ececeb65690f15b8cabe7149495", + "sha256:42597e938f83bb7f3e4b35f03aa45208d49ae8d5bcb4bc10b9fc825e0ab5e423", + "sha256:4304e4fceb823d91699e924a1fdf95cde0e066f3b1c28edb665bda762ecde10f", + "sha256:4b4b9a0562a35773ff47a3df823177ab71a1f5eb1ff56d8f842b7432ecfd7fd2", + "sha256:4c7f5cb38c640919791c9f74ea80c5b82314c69a8409ea36f2599617d03989af", + "sha256:51f09caab818707ab91cf09cc5c156026599cf05a4520779ccbf53c1b352fb25", + "sha256:c529535e5c0effcf417682563719e5d8ac8d2b93de07a56108b4c2d436d7a29a", + "sha256:cabfe43044ee319ad6832b2fda332646f9ef1636b0130186a3ae0a52fc264bb4", + "sha256:f24e5d70e6af8ee9672ff605d5503491635f63d5db2fffb6472be78ba62efd8f", + "sha256:fc063acaf7a3d9ca13146fefb5b42ac94ab943ec6e978f543cd5637da2d57957" ], "markers": "python_version >= '3.8'", - "version": "==5.27.2" + "version": "==5.28.1" }, "psycopg2-binary": { "hashes": [ @@ -505,19 +561,19 @@ }, "pyasn1": { "hashes": [ - "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", - "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473" + "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", + "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034" ], "markers": "python_version >= '3.8'", - "version": "==0.6.0" + "version": "==0.6.1" }, "pyasn1-modules": { "hashes": [ - "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6", - "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b" + "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd", + "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c" ], "markers": "python_version >= '3.8'", - "version": "==0.4.0" + "version": "==0.4.1" }, "pycparser": { "hashes": [ @@ -532,10 +588,10 @@ "crypto" ], "hashes": [ - "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", - "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" + "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", + "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c" ], - "version": "==2.8.0" + "version": "==2.9.0" }, "requests": { "hashes": [ @@ -562,11 +618,11 @@ }, "sqlparse": { "hashes": [ - "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93", - "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663" + "sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", + "sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" ], "markers": "python_version >= '3.8'", - "version": "==0.5.0" + "version": "==0.5.1" }, "typing-extensions": { "hashes": [ @@ -578,11 +634,11 @@ }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" ], "markers": "python_version >= '3.8'", - "version": "==2.2.2" + "version": "==2.2.3" } }, "develop": {} diff --git a/api/README.md b/api/README.md index 7621fee..f387cfe 100644 --- a/api/README.md +++ b/api/README.md @@ -23,8 +23,26 @@ Set up your local development environment for the API. - You can start by copying the example and updating the values. `cp .env.example .env` - Or you can copy a `.env` file from another api in this project. 4. `make setup` will handle pipenv fun for you. + - Run this again if new packages were added with `pipenv install ...` 5. Do something to improve this getting started guide! +#### Local Django Admin + +The app uses oauth and the Django admin can be accessed on a locally running server at [http://localhost:8000/admin/]. + +The first time your run, and any time you destroy your `docker-compose` volumes, you'll need to set yourself up as a super user. + +1. [Sign in with OAuth](http://localhost:8000/accounts/google/login/?process=login). This creates an account that you will give staff and superuser access. +1. Jump onto the running psql container. + 1. Find the container ID by running `docker ps` and copy the `Container ID` for the `postgres` image. + 1. Run `docker exec it containerIdThatYouCopied bash` to connect to the running postgres container. + 1. Run `psql artist -Upostgres` to access the `artist` database. + 1. Run `select * from auth_user;` to see your user table. You should see exactly 1 user and it should be you. + 1. Give yourself super admin permissions by running `update auth_user set is_staff = 't', is_superuser = 't' where id = 1;`. This assumes your user's row ID is 1. It should be. +1. Access the admin console at [http://localhost:8000/admin/]. + + + ## Run ``` diff --git a/api/api/art_storage.py b/api/api/art_storage.py index 7a4b7a8..53ce1a8 100644 --- a/api/api/art_storage.py +++ b/api/api/art_storage.py @@ -11,6 +11,12 @@ class ArtStorage: + def __new__(cls): + """Create or return the singleton instance of the ArtStorage.""" + if not hasattr(cls, 'instance'): + cls.instance = super(ArtStorage, cls).__new__(cls) + return cls.instance + def __init__(self): self.gs = storage.Client() self.bucket = self.gs.bucket(BUCKET_NAME) @@ -26,3 +32,12 @@ def get_art(self, generation): 'artist_id': m['artist_id'], }) return {'art': art} + + def new_image_file(self, generation: int, artist_id: int): + blob = self.bucket.blob(f'gen-{generation}/{artist_id}.jpg') + #blob.acl.all().grant_read() + return blob + + def open(self, blob): + """Gets the context manager for a filepointer to write the art to.""" + return blob.open(mode='wb', ignore_flush=True) diff --git a/api/api/migrations/0006_alter_artist_public_link.py b/api/api/migrations/0006_alter_artist_public_link.py new file mode 100644 index 0000000..20ceb0b --- /dev/null +++ b/api/api/migrations/0006_alter_artist_public_link.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.1 on 2024-09-15 17:47 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0005_remove_newvote_artist_remove_newvote_user_artist_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='artist', + name='public_link', + field=models.URLField(null=True), + ), + ] diff --git a/api/api/models.py b/api/api/models.py index aa50b8b..c9bc54b 100644 --- a/api/api/models.py +++ b/api/api/models.py @@ -3,11 +3,11 @@ def get_current_generation(): - # Get the max id of where `active_date` is not `Null`. + # Get the max id of where `active_date` is not `Null` or `None`. gen = (Generation.objects .filter(active_date__isnull=False) .order_by("-id") - .values_list("id", flat=True)[0]) + .values_list("id", flat=True).first()) return gen @@ -38,13 +38,13 @@ class Generation(Datetime): inaction_date = models.DateTimeField(null=True) def __str__(self): - return f'{self.id}: {self.active_date} - {self.inaction_date}' + return f'Generation: {self.id}: {self.active_date} - {self.inaction_date}' class Artist(Datetime): id = models.AutoField(primary_key=True) dna = models.TextField() - public_link = models.URLField(max_length=200) + public_link = models.URLField(max_length=200, null=True) generation = models.ForeignKey(Generation, on_delete=models.CASCADE) diff --git a/api/app/urls.py b/api/app/urls.py index 5e7f87f..afa0bab 100644 --- a/api/app/urls.py +++ b/api/app/urls.py @@ -20,6 +20,7 @@ urlpatterns = [ # Local endpoints. path("api/", include("api.urls")), + path("painter/", include("painter.urls")), # Django built-ins path("admin/", admin.site.urls), diff --git a/api/docker-compose-test.yml b/api/docker-compose-test.yml index 4b2282a..f098180 100644 --- a/api/docker-compose-test.yml +++ b/api/docker-compose-test.yml @@ -1,7 +1,7 @@ version: '3.9' services: - db: + db-test: image: postgres:15 restart: always env_file: @@ -14,10 +14,12 @@ services: entrypoint: python manage.py test volumes: - ./:/home/appuser/ + # x11 forwarding, to connect to the host x11 port. + - /tmp/.X11-unix:/tmp/.X11-unix env_file: - ./.env.test depends_on: - - db + - db-test volumes: postgres_data: diff --git a/api/docker-compose.yml b/api/docker-compose.yml index d96900e..ababd8d 100644 --- a/api/docker-compose.yml +++ b/api/docker-compose.yml @@ -18,6 +18,8 @@ services: volumes: - ./:/home/appuser/ - ${GOOGLE_APPLICATION_CREDENTIALS}:${GOOGLE_APPLICATION_CREDENTIALS} + # x11 forwarding, to connect to the host x11 port. + - /tmp/.X11-unix:/tmp/.X11-unix ports: - 8000:8000 env_file: diff --git a/painterapi/painter/__init__.py b/api/painter/__init__.py similarity index 100% rename from painterapi/painter/__init__.py rename to api/painter/__init__.py diff --git a/painterapi/painter/dna_parser.py b/api/painter/dna_parser.py similarity index 100% rename from painterapi/painter/dna_parser.py rename to api/painter/dna_parser.py diff --git a/api/painter/generation.py b/api/painter/generation.py new file mode 100644 index 0000000..35bbc3d --- /dev/null +++ b/api/painter/generation.py @@ -0,0 +1,56 @@ +import random + +from api import art_storage +from api import models +from painter.graphics import engine +from painter import painter as _painter + + +_GRAPHICS_ENGINE = None + +def graphics_engine(): + global _GRAPHICS_ENGINE + if not _GRAPHICS_ENGINE: + _GRAPHICS_ENGINE = engine.TurtleEngine() + return _GRAPHICS_ENGINE + + +# Hardcode a few special values because they're just used once. +_NUM_ARTISTS = 64 +_NUM_CHROMOSOMES = 32 +_MIN_CHROMOSOME_LENGTH = 256 +_MAX_CHROMOSOME_LENGTH = 512 + + +def bootstrap(): + """Create a first generation of artists. + """ + gen = models.Generation.objects.create() + + for i in range(_NUM_ARTISTS): + # Build the DNA string of chromosomes. + chromosomes = [] + for _ in range(_NUM_CHROMOSOMES): + chromo_len = random.randint(_MIN_CHROMOSOME_LENGTH, _MAX_CHROMOSOME_LENGTH-1) + chromo_str = ''.join(random.choices('ATCG', k=chromo_len)) + chromosomes.append(chromo_str) + + dna = '\n'.join(chromosomes) + artist = models.Artist.objects.create( + dna=dna, + generation=gen, + ) + paint(artist, gen) + + +def paint(artist, gen): + """Paints and saves this artist's masterpiece to the artist model. + """ + graphics_engine().reset() + p = _painter.Painter(artist.dna, graphics_engine()) + while p.still_growing(): + p.paint() + p.age_up() + public_url = graphics_engine().save_image(art_storage.ArtStorage(), gen.id, artist.id) + artist.public_url = public_url + artist.save() diff --git a/painterapi/painter/graphics/actions.py b/api/painter/graphics/actions.py similarity index 100% rename from painterapi/painter/graphics/actions.py rename to api/painter/graphics/actions.py diff --git a/api/painter/graphics/engine.py b/api/painter/graphics/engine.py new file mode 100644 index 0000000..43b1748 --- /dev/null +++ b/api/painter/graphics/engine.py @@ -0,0 +1,61 @@ +import abc +import io +import tempfile +import turtle + +from PIL import Image + +from painter.graphics import actions + + +class EngineInterface(metaclass=abc.ABCMeta): + + def __init__(self, action_class): + self.action_class = action_class + + def save_image(self, output_filename: str): + pass + + def get_action(self, index): + return self.action_class(index) + + def get_action_count(self): + return len(self.action_class.__members__) + + +class TurtleEngine(EngineInterface): + + def __new__(cls): + """Create or return the singleton instance of the ArtStorage.""" + if not hasattr(cls, 'instance'): + cls.instance = super(TurtleEngine, cls).__new__(cls) + return cls.instance + + def __init__(self): + EngineInterface.__init__(self, actions.TurtleAction) + self.reset() + + def save_image(self, storage, generation: int, artist_id: int): + """Save a JPEG image to the art_storage. + + Args: + - storage: The object responsible for storaging images. + - generation: The generation number, used by `storage` to know where to save the image. + - artist_id: The artist's numeric ID, used by `storage` to know where to save the image. + + Returns: + - str: The public URL to the newly saved image file. + """ + canvas = turtle.getscreen().getcanvas() + ps = canvas.postscript() + image_file = storage.new_image_file(generation, artist_id) + with Image.open(io.BytesIO(ps.encode('utf-8'))) as img: + with storage.open(image_file) as fp: + img.save(fp, format='JPEG') + return image_file.public_url + + def reset(self): + turtle.clearscreen() + turtle.speed(10) + turtle.hideturtle() + turtle.getscreen().colormode(255) diff --git a/painterapi/painter/paint.py b/api/painter/paint.py similarity index 100% rename from painterapi/painter/paint.py rename to api/painter/paint.py diff --git a/painterapi/painter/painter.py b/api/painter/painter.py similarity index 100% rename from painterapi/painter/painter.py rename to api/painter/painter.py diff --git a/painterapi/painter/storage/art_storage.py b/api/painter/storage/art_storage.py similarity index 50% rename from painterapi/painter/storage/art_storage.py rename to api/painter/storage/art_storage.py index 00e3d24..8d745f9 100644 --- a/painterapi/painter/storage/art_storage.py +++ b/api/painter/storage/art_storage.py @@ -1,3 +1,5 @@ +""" DEPRECCATED !!! DELETE ME DELETE ME""" + import os from google.cloud import storage @@ -11,7 +13,3 @@ def __init__(self): self.gs = storage.Client() self.bucket = self.gs.bucket(BUCKET_NAME) - def open(self, generation, artist_id): - """Gets the context manager for a filepointer to write the art to.""" - blob = self.bucket.blob(f'gen-{generation}/{artist_id}.jpg') - return blob.open(mode='wb', ignore_flush=True) diff --git a/painterapi/painter/stroke.py b/api/painter/stroke.py similarity index 100% rename from painterapi/painter/stroke.py rename to api/painter/stroke.py diff --git a/painterapi/tests/graphics/__init__.py b/api/painter/tests/__init__.py similarity index 100% rename from painterapi/tests/graphics/__init__.py rename to api/painter/tests/__init__.py diff --git a/api/painter/tests/graphics/__init__.py b/api/painter/tests/graphics/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/painterapi/tests/graphics/test_actions.py b/api/painter/tests/graphics/test_actions.py similarity index 97% rename from painterapi/tests/graphics/test_actions.py rename to api/painter/tests/graphics/test_actions.py index 20b20f4..454d84b 100644 --- a/painterapi/tests/graphics/test_actions.py +++ b/api/painter/tests/graphics/test_actions.py @@ -6,7 +6,7 @@ from painter.graphics import engine -engine = engine.TurtleEngine(None) +engine = engine.TurtleEngine() class TestTurtleAction(unittest.TestCase): diff --git a/painterapi/tests/test_dna_parser.py b/api/painter/tests/test_dna_parser.py similarity index 89% rename from painterapi/tests/test_dna_parser.py rename to api/painter/tests/test_dna_parser.py index 959e34b..e572872 100644 --- a/painterapi/tests/test_dna_parser.py +++ b/api/painter/tests/test_dna_parser.py @@ -7,7 +7,7 @@ class TestChromosome(unittest.TestCase): def test_parse_valid(self): - c = dna_parser.Chromosome('GAAACGCTTTC', engine.TurtleEngine(None)) + c = dna_parser.Chromosome('GAAACGCTTTC', engine.TurtleEngine()) self.assertTrue(c.is_valid()) self.assertEqual('G', c.pre_junk) self.assertEqual('C', c.post_junk) @@ -17,7 +17,7 @@ def test_parse_valid(self): class TestGeneSequencer(unittest.TestCase): def test_valid(self): - c = dna_parser.Chromosome('', engine.TurtleEngine(None)) + c = dna_parser.Chromosome('', engine.TurtleEngine()) c.pre_junk = 'A' # 65 % 6 = 5 c.genes = 'AAACCCTTTAAACCC' diff --git a/api/painter/tests/test_views.py b/api/painter/tests/test_views.py new file mode 100644 index 0000000..2bbb297 --- /dev/null +++ b/api/painter/tests/test_views.py @@ -0,0 +1,53 @@ +from unittest import mock + +from django.test import TestCase +from django.test.client import Client + +from api import models +from painter import generation + + +class TestViews(TestCase): + + def setUp(self): + self.client = Client() + + graphics_engine_patch = mock.patch('painter.graphics.engine.TurtleEngine') + self.engine = graphics_engine_patch.start().return_value + self.addCleanup(graphics_engine_patch.stop) + + # Make sure we don't talk to the real storage system. + art_storage_patch = mock.patch('painter.generation.art_storage') + self.art_storage = art_storage_patch.start().ArtStorage.return_value + self.addCleanup(art_storage_patch.stop) + + painter_patch = mock.patch('painter.painter.Painter') + self.painter = painter_patch.start().return_value + # Make sure it doesn't grow indefinitely. :) + self.painter.still_growing.return_value = False + self.addCleanup(painter_patch.stop) + + def test_health(self): + response = self.client.get('/painter/health') + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'OK') + + def test_crontime_bootstrap(self): + # Ensure we have no generations (which triggers the bootstrap). + models.Generation.objects.all().delete() + self.assertEqual(models.Generation.objects.count(), 0) + + response = self.client.get('/painter/crontime') + + # Check the HTTP call succeeded. + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content, b'OK') + + # The first generation should have been created. + self.assertEqual(models.Generation.objects.count(), 1) + + # The engine should have been reset once per artist. + self.assertEqual(self.engine.reset.call_count, generation._NUM_ARTISTS) + # And that we saved one image per artists. + self.assertEqual(self.engine.save_image.call_count, generation._NUM_ARTISTS) diff --git a/api/painter/urls.py b/api/painter/urls.py new file mode 100644 index 0000000..ac906b4 --- /dev/null +++ b/api/painter/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views + +urlpatterns = [ + path("crontime", views.crontime, name="crontime"), + path("health", views.health, name="health"), +] diff --git a/api/painter/views.py b/api/painter/views.py new file mode 100644 index 0000000..f95d25f --- /dev/null +++ b/api/painter/views.py @@ -0,0 +1,19 @@ +from django.http import HttpResponse +from django.utils import timezone + +from api import models +from painter import generation + + +def health(request): + return HttpResponse("OK") + + +def crontime(request): + # TODO(kmd): async + max_gen = models.Generation.objects.order_by('-id').first() + if not max_gen: + generation.bootstrap() + # TODO(kmd): Check if we have enough votes to start a new generation. + # If so, create the new generation. + return HttpResponse("OK") diff --git a/painterapi/painter/__main__.py b/painterapi/painter/__main__.py deleted file mode 100644 index aca3c77..0000000 --- a/painterapi/painter/__main__.py +++ /dev/null @@ -1,25 +0,0 @@ -import sys - -from painter import painter -from painter import datastore -from painter.graphics import engine -from painter.storage import art_storage - -def main() -> int: - graphics_engine = engine.TurtleEngine(art_storage.ArtStorage()) - DS = datastore.Client() - - current_gen = 0 - for artist_id, dna_str in DS.read_dna(current_gen): - print(f'Artist {artist_id} starting to paint...') - graphics_engine.reset() - p = painter.Painter(dna_str, graphics_engine) - while p.still_growing(): - p.paint() - p.age_up() - graphics_engine.save_image(current_gen, artist_id) - return 0 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/painterapi/painter/api.py b/painterapi/painter/api.py deleted file mode 100644 index 5b9fff9..0000000 --- a/painterapi/painter/api.py +++ /dev/null @@ -1,38 +0,0 @@ -import os - -from flask import Flask, request - -from painter import painter -from painter import datastore -from painter.graphics import engine -from painter.storage import art_storage - - -app = Flask(__name__) - - -@app.route("/health") -def health_check(): - return 'OK' - - -@app.route("/paint") -def paint(): - gen = request.args.get('gen', -1) - - # TODO(kmd): This should be processed async. - graphics_engine = engine.TurtleEngine(art_storage.ArtStorage()) - DS = datastore.Client() - for artist_id, dna_str in DS.read_dna(gen): - app.logger.info(f'Artist {artist_id} starting to paint...') - graphics_engine.reset() - p = painter.Painter(dna_str, graphics_engine) - while p.still_growing(): - p.paint() - p.age_up() - graphics_engine.save_image(gen, artist_id) - return 'OK' - - -if __name__ == "__main__": - app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080))) diff --git a/painterapi/painter/datastore.py b/painterapi/painter/datastore.py deleted file mode 100644 index b61abcf..0000000 --- a/painterapi/painter/datastore.py +++ /dev/null @@ -1,39 +0,0 @@ -import zlib - -from google.cloud import datastore - - -class Client: - - def __init__(self): - self.client = datastore.Client(namespace='artist2d') - self.key_dna_generations = self.client.key('entity', 'dna', 'entity', 'generation') - - def read_dna(self, generation): - """Generator for all the DNA records for single generation.""" - key_gen = self._get_dna_generation_key(generation) - #dna_array = datastore.Entity(key_gen) - dna_array = self.client.get(key_gen) - for artist_id, artist_dna in dna_array.items(): - dna_blob = artist_dna['encoded_dna'] - raw_dna = zlib.decompress(dna_blob) - yield artist_id, raw_dna.decode('utf-8') - - def write_new_dna(self, generation, artist_id, raw_dna): - if hasattr(raw_dna, 'encode'): - # If this is a string, convert to bytes. - raw_dna = raw_dna.encode() - - gen_key = self._get_dna_generation_key(generation) - artists_dna = self.client.get(gen_key) - if not artists_dna: - print(f'Staring new generation {generation} in firestore') - artists_dna = datastore.Entity(gen_key) - - encoded_dna = zlib.compress(raw_dna) - artists_dna.update({str(artist_id): {'encoded_dna': encoded_dna}}) - self.client.put(artists_dna) - - def _get_dna_generation_key(self, generation): - return self.client.key( - 'entity', str(generation), 'entity', 'artists', parent=self.key_dna_generations) diff --git a/painterapi/painter/graphics/engine.py b/painterapi/painter/graphics/engine.py deleted file mode 100644 index a42b3bb..0000000 --- a/painterapi/painter/graphics/engine.py +++ /dev/null @@ -1,44 +0,0 @@ -import abc -import io -import tempfile -import turtle - -from PIL import Image - -from painter.graphics import actions - - -class EngineInterface(metaclass=abc.ABCMeta): - - def __init__(self, action_class): - self.action_class = action_class - - def save_image(self, output_filename: str): - pass - - def get_action(self, index): - return self.action_class(index) - - def get_action_count(self): - return len(self.action_class.__members__) - - -class TurtleEngine(EngineInterface): - - def __init__(self, art_storage): - EngineInterface.__init__(self, actions.TurtleAction) - self.art_storage = art_storage - self.reset() - - def save_image(self, generation, artist_id): - canvas = turtle.getscreen().getcanvas() - ps = canvas.postscript() - with Image.open(io.BytesIO(ps.encode('utf-8'))) as img: - with self.art_storage.open(generation, artist_id) as blob_fp: - img.save(blob_fp, format='JPEG') - - def reset(self): - turtle.clearscreen() - turtle.speed(10) - turtle.hideturtle() - turtle.getscreen().colormode(255)