From a6fd000d45de33b3cceff2c9d11243f84e2a9ff5 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 8 Dec 2022 19:59:27 -0800 Subject: [PATCH 01/34] Removed unused code --- sdk/python/flet/page.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index 7e83f1cdb..5ae4132ab 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -269,21 +269,15 @@ def insert(self, at, *controls): ctrl.did_mount() def remove(self, *controls): - added_controls = [] with self._lock: for control in controls: self._controls.remove(control) - added_controls = self.__update(self) - for ctrl in added_controls: - ctrl.did_mount() + self.__update(self) def remove_at(self, index): - added_controls = [] with self._lock: self._controls.pop(index) - added_controls = self.__update(self) - for ctrl in added_controls: - ctrl.did_mount() + self.__update(self) def clean(self): with self._lock: From 8658b4dcbce0677e4e771a7ac2d17abaa1929e6f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 9 Dec 2022 11:53:20 -0800 Subject: [PATCH 02/34] Added websockets dependency --- sdk/python/pdm.lock | 79 ++++++++++++++++++++++++++++++++++++++- sdk/python/pyproject.toml | 4 +- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/sdk/python/pdm.lock b/sdk/python/pdm.lock index 1602ada5d..902b43174 100644 --- a/sdk/python/pdm.lock +++ b/sdk/python/pdm.lock @@ -240,6 +240,12 @@ version = "1.4.2" requires_python = ">=3.7" summary = "WebSocket client for Python with low level API options" +[[package]] +name = "websockets" +version = "10.4" +requires_python = ">=3.7" +summary = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" + [[package]] name = "zipp" version = "3.10.0" @@ -248,7 +254,7 @@ summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "4.0" -content_hash = "sha256:9bd1e399fac3187f9ca477a6d26e125417df67a1172333ed5232019feb419eef" +content_hash = "sha256:9d9f6efa10cb304a58a2c308465e75a9984ec6d5f18a24de1cc6e5adac42a2b9" [metadata.files] "attrs 22.1.0" = [ @@ -447,6 +453,77 @@ content_hash = "sha256:9bd1e399fac3187f9ca477a6d26e125417df67a1172333ed5232019fe {url = "https://files.pythonhosted.org/packages/75/af/1d13b93e7a21aca7f8ab8645fcfcfad21fc39716dc9dce5dc2a97f73ff78/websocket-client-1.4.2.tar.gz", hash = "sha256:d6e8f90ca8e2dd4e8027c4561adeb9456b54044312dba655e7cae652ceb9ae59"}, {url = "https://files.pythonhosted.org/packages/78/d5/2b5719b738791cd798e8f097eba4bb093ff5efca5cef2f3d37a72daa111f/websocket_client-1.4.2-py3-none-any.whl", hash = "sha256:d6b06432f184438d99ac1f456eaf22fe1ade524c3dd16e661142dc54e9cba574"}, ] +"websockets 10.4" = [ + {url = "https://files.pythonhosted.org/packages/00/15/611ddaca66937f77aa5021e97c9bec61e6a30668b75db3707713b69b3b88/websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"}, + {url = "https://files.pythonhosted.org/packages/03/e2/7784912651a299a5e060656e6368946ae4c1da63f01236f7d650e8070cf8/websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"}, + {url = "https://files.pythonhosted.org/packages/09/35/2b8ed52dc995507476ebbb7a91a0c5ed80fd80fa0a840f422ac25c722dbf/websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"}, + {url = "https://files.pythonhosted.org/packages/0c/56/b2d373ed19b4e7b6c5c7630d598ba10473fa6131e67e69590214ab18bc09/websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"}, + {url = "https://files.pythonhosted.org/packages/0c/f0/195097822f8edc4ffa355f6463a1890928577517382c0baededc760f9397/websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"}, + {url = "https://files.pythonhosted.org/packages/14/88/81c08fb3418c5aedf3776333f29443599729509a4f673d6598dd769d3d6b/websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"}, + {url = "https://files.pythonhosted.org/packages/17/e4/3bdc2ea97d7da70d9f184051dcd40f27c849ded517ea9bab70df677a6b23/websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"}, + {url = "https://files.pythonhosted.org/packages/19/a3/02ce75ffca3ef147cc0f44647c67acb3171b5a09910b5b9f083b5ca395a6/websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"}, + {url = "https://files.pythonhosted.org/packages/1c/4b/cab8fed34c3a29d4594ff77234f6e6b45feb35331f1c12fccf92ca5486dd/websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"}, + {url = "https://files.pythonhosted.org/packages/1d/06/5ecd0434cf35f92ca9ce80e38a3ac9bf5422ace9488693c3900e2f1c7fa0/websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"}, + {url = "https://files.pythonhosted.org/packages/1e/76/163a18626001465a309bf74b6aeb301d7092e304637fe00f89d7efc75c44/websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"}, + {url = "https://files.pythonhosted.org/packages/20/7a/bd0ce7ac1cfafc76c84d6e8051bcbd0f7def8e45207230833bd6ff77a41d/websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"}, + {url = "https://files.pythonhosted.org/packages/25/a7/4e32f8edfc26339d8d170fe539e0b83a329c42d974dacfe07a0566390aef/websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"}, + {url = "https://files.pythonhosted.org/packages/27/bb/6327e8c7d4dd7d5b450b409a461be278968ce05c54da13da581ac87661db/websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"}, + {url = "https://files.pythonhosted.org/packages/29/33/dd88aefeabc9dddb4f48c9e15c6c2554dfb6b4cf8d8f1b4de4d12ba997de/websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"}, + {url = "https://files.pythonhosted.org/packages/2b/cb/d394efe7b0ee6cdeffac28a1cb054e42f9f95974885ca3bcd6fceb0acde1/websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"}, + {url = "https://files.pythonhosted.org/packages/2e/dd/521f0574bed6d08ce5e0acd5893ae418c0a81ef55eb4c960aedac9cbd929/websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"}, + {url = "https://files.pythonhosted.org/packages/33/3a/72c9d733d676447da2c89a35c694f779a9a360cff51ee0f90bb562d80cd4/websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"}, + {url = "https://files.pythonhosted.org/packages/36/8f/6dd75723ea67d54dec3a597ad781642c0febe8d51f233b95347981c0e549/websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"}, + {url = "https://files.pythonhosted.org/packages/37/02/ef21ca4698c2fd950250e5ac397fd07b0c9f16bbd073d0ea64c25baef9c1/websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"}, + {url = "https://files.pythonhosted.org/packages/3e/a5/e4535867a96bb07000c54172e1be82cd0b3a95339244cac1d400f8ba9b64/websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"}, + {url = "https://files.pythonhosted.org/packages/47/4d/f2e28f112302d3bc794b74ae64656255161d8223f4d47bd17d40cbb3629e/websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"}, + {url = "https://files.pythonhosted.org/packages/47/58/69435f1479acb56b3678905b5f2be57908a201c28465d4368d91f52cad76/websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"}, + {url = "https://files.pythonhosted.org/packages/4a/39/3b6b64f775f1f4f5de6eb909d72f3f794f453730b5b3176fa5021ff334ba/websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"}, + {url = "https://files.pythonhosted.org/packages/4d/6f/2388f9304cdaa0215b6388f837c6dbfe6d63ac1bba8c196e3b14eea1831e/websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"}, + {url = "https://files.pythonhosted.org/packages/4e/8b/854b3625cc5130e4af8a10a7502c2f6c16d1bd107ff009394127a2f8abb3/websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"}, + {url = "https://files.pythonhosted.org/packages/57/d7/df17197565e8874f0a77f8211304169ad4f39ffa3e8c008a7b0bf187a238/websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"}, + {url = "https://files.pythonhosted.org/packages/5a/87/dea889793d2d0958be254fc86dac528d97de9354d16fcdbcbad259750014/websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"}, + {url = "https://files.pythonhosted.org/packages/5d/3c/fc1725524e48f624df77f5998b1c7070fdddec3ae67a2ffbc99ffd116269/websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"}, + {url = "https://files.pythonhosted.org/packages/60/3a/6dccbe2725d13c398b90cbebeea684cda7792e6d874f96417db900556ad0/websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"}, + {url = "https://files.pythonhosted.org/packages/62/76/c2411e634979cc6e812ef2a96aa295545cfcbc9566b298db09f3f4639d62/websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"}, + {url = "https://files.pythonhosted.org/packages/63/f2/ec4c59b4f91936eb2a5ddcf2f7e57184acbce5122d5d83911c5a47f25144/websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"}, + {url = "https://files.pythonhosted.org/packages/68/bd/c8bd8354fc629863a2db39c9182d40297f47dfb2ed3e178bc83041ce044b/websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"}, + {url = "https://files.pythonhosted.org/packages/68/ec/3267f8bbe8a4a5e181ab3fc67cc137f0966ab9e9a4da14ffc603f320b9e6/websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"}, + {url = "https://files.pythonhosted.org/packages/71/93/5a4f408177e43d84274e1c08cbea3e50ad80db654dc25a0bba79dbdc00b4/websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"}, + {url = "https://files.pythonhosted.org/packages/75/18/155c3582fd69b60d9c490fb0e64e37269c55d5873cbcb37f83e2d3feb078/websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"}, + {url = "https://files.pythonhosted.org/packages/77/65/d7c73e62cf19f068850ddab548837329dab9c023567f5834747f61cdc747/websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"}, + {url = "https://files.pythonhosted.org/packages/85/dc/549a807a53c13fd4a8dac286f117a7a71260defea9ec0c05d6027f2ae273/websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, + {url = "https://files.pythonhosted.org/packages/86/8e/390e0e3db702c55d31ca3999c622bb3b8b480c306c1bdee6a2da44b13b1b/websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"}, + {url = "https://files.pythonhosted.org/packages/88/00/9776e2626a30e3455a830665e50cf40f5d34a4134272b3138a637afa38a7/websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"}, + {url = "https://files.pythonhosted.org/packages/88/97/d70e2d528b9ffe759134e5db6b1424b61cd61fd1c4471b178c76e01f41af/websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"}, + {url = "https://files.pythonhosted.org/packages/8a/1e/8f34d7ee924dc7a624c1e14f43209484cb5eccb58e892285d45551729a95/websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"}, + {url = "https://files.pythonhosted.org/packages/90/e1/22e43e9a1fbc9ddf4a0317b231e2e28eddfbe8804b7ca4a9f7fba7033b17/websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"}, + {url = "https://files.pythonhosted.org/packages/93/7b/72134e4c75002e311c072f0665fe45f7321d614c5c65181888faddd616e9/websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"}, + {url = "https://files.pythonhosted.org/packages/a0/92/aa8d1ba3a7e3e6cf6d5d1c929530a40138667ea60454bf5c0fff3b93cae2/websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"}, + {url = "https://files.pythonhosted.org/packages/a1/6f/60e5f6e114b6077683d74da5df0d4af647a9e6d2a18b4698f577b2cb7c14/websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"}, + {url = "https://files.pythonhosted.org/packages/a1/f6/83da14582fbb0496c47a4c039bd6e802886a0c300e9795c0f839fd1498e3/websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"}, + {url = "https://files.pythonhosted.org/packages/ab/41/ed2fecb228c1f25cea03fce4a22a86f7771a10875d5762e777e943bb7d68/websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"}, + {url = "https://files.pythonhosted.org/packages/b0/fc/a818cddc63589e12d5eff9b51a59aad82e2adf35279493248a3742c41f85/websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"}, + {url = "https://files.pythonhosted.org/packages/b1/8f/dbffb63e7da0ada24e9ef8802c439169e0ed9a7ef8f6049874e6cbfc7919/websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"}, + {url = "https://files.pythonhosted.org/packages/b4/91/c460f5164af303b31f58362935f7b8ed1750e3b8fbcb900da4b0661532a8/websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"}, + {url = "https://files.pythonhosted.org/packages/bb/5c/7dc1f604688f43168ef17313055e048c755a29afde821f7e0b19bd3a180f/websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"}, + {url = "https://files.pythonhosted.org/packages/c5/01/145d2883dfeffedf541a7c95bb26f8d8b5ddca84a7c8f671ec3b878ae7cd/websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"}, + {url = "https://files.pythonhosted.org/packages/c6/41/07f39745017af5381aeb6c1d8c6509aa1861193c948648d4aaf4d0637915/websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"}, + {url = "https://files.pythonhosted.org/packages/cc/19/2f003f9f81c0fab2eabb81d8fc2fce5fb5b5714f1b4abfe897cb209e031d/websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"}, + {url = "https://files.pythonhosted.org/packages/d1/60/0a6cb94e25b981e428c1cdcc2b0a406ac6e1dfc78d8a81c8a4ee7510e853/websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"}, + {url = "https://files.pythonhosted.org/packages/d1/c6/9489869aa591e6a8941b0af2302f8383e199e90477559a510713d41bfa45/websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"}, + {url = "https://files.pythonhosted.org/packages/d4/1a/2e4afd95abd33bd6ad77042270f8eee3697e07cdd749c068bff08bba2022/websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"}, + {url = "https://files.pythonhosted.org/packages/d5/5d/d0b039f0db0bb1fea93437721cf3cd8a244ad02a86960c38a3853d5e1fab/websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"}, + {url = "https://files.pythonhosted.org/packages/d6/7c/79ea4e7f56dfe7f703213000bbbd29b70cef2666698d98b66ce1af43caee/websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"}, + {url = "https://files.pythonhosted.org/packages/d7/f9/f64ec37da654351b212e5534b0e31703ed80d2a6acb6b8c1b1373fafa876/websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"}, + {url = "https://files.pythonhosted.org/packages/da/0b/a501ed176c69b51ca83f4186bad80bba9b59ab354fd8954d7d36cb2ec47f/websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"}, + {url = "https://files.pythonhosted.org/packages/e0/8d/7bffabd3f10a88cd68080669b33f407471283becf7e5cb4f0143b117211d/websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"}, + {url = "https://files.pythonhosted.org/packages/e6/94/cb97e5a9d019e473a37317a740852850ef09e14c02621dd86a898ec90f7a/websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"}, + {url = "https://files.pythonhosted.org/packages/e9/48/a0751eafbeab06866fc70a66f7dfa08422cb96113af9138e526e7b106f14/websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"}, + {url = "https://files.pythonhosted.org/packages/ec/ba/74b4b92cc41ffc4cfa791fb9f8e8ab7c4d9bf84e54a5bef12ab23eb54880/websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"}, + {url = "https://files.pythonhosted.org/packages/f8/f0/437187175182beed10246f53ef9793a5f6e087ce71ee25b64fdb12e396e0/websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"}, + {url = "https://files.pythonhosted.org/packages/f9/15/ab0e9155700d3037ffe4a146a719f3e68ee025c9d45d6a39b027e928db52/websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"}, + {url = "https://files.pythonhosted.org/packages/fd/42/07f31d9f9e142b38cde8d3ea0c8ea1bacf9bc366f2f573eca57086e9f2a6/websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"}, +] "zipp 3.10.0" = [ {url = "https://files.pythonhosted.org/packages/40/8a/d63273ed0fa4a3d06f77e7b043f6577d8894e95515b0c187c52e2c0efabb/zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, {url = "https://files.pythonhosted.org/packages/8d/d7/1bd1e0a5bc95a27a6f5c4ee8066ddfc5b69a9ec8d39ab11a41a804ec8f0d/zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index c6f8adef6..5c39f6b03 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -13,7 +13,9 @@ dependencies = [ "repath>=0.9.0", "watchdog>=2.1.9", "requests>=2.28.1", - "oauthlib>=3.2.0"] + "oauthlib>=3.2.0", + "websockets>=10.4", +] requires-python = ">=3.7" license = { text = "MIT" } classifiers = [ From 232141d28b6e752e776a1fa213a1f93daf543fe7 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 12 Dec 2022 10:26:55 -0800 Subject: [PATCH 03/34] SyncConnection added --- sdk/python/flet/connection.py | 117 ++------------------------ sdk/python/flet/flet.py | 10 +-- sdk/python/flet/sync_connection.py | 129 +++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 119 deletions(-) create mode 100644 sdk/python/flet/sync_connection.py diff --git a/sdk/python/flet/connection.py b/sdk/python/flet/connection.py index 20f3444c5..840e536cb 100644 --- a/sdk/python/flet/connection.py +++ b/sdk/python/flet/connection.py @@ -1,126 +1,21 @@ -import logging -import threading -import uuid - -from flet.protocol import * +from typing import List, Optional +from flet.protocol import Command from flet.pubsub import PubSubHub -from flet.reconnecting_websocket import ReconnectingWebSocket class Connection: - def __init__(self, ws: ReconnectingWebSocket): - self._ws = ws - self._ws.on_message = self._on_message - self._ws_callbacks = {} - self._on_event = None - self._on_session_created = None + def __init__(self): self.host_client_id: Optional[str] = None self.page_name: Optional[str] = None self.page_url: Optional[str] = None self.sessions = {} self.pubsubhub = PubSubHub() - @property - def on_event(self): - return self._on_event - - @on_event.setter - def on_event(self, handler): - self._on_event = handler - - @property - def on_session_created(self): - return self._on_session_created - - @on_session_created.setter - def on_session_created(self, handler): - self._on_session_created = handler - - def _on_message(self, data): - logging.debug(f"_on_message: {data}") - msg_dict = json.loads(data) - msg = Message(**msg_dict) - if msg.id: - # callback - evt = self._ws_callbacks[msg.id][0] - self._ws_callbacks[msg.id] = (None, msg.payload) - evt.set() - elif msg.action == Actions.PAGE_EVENT_TO_HOST: - if self._on_event is not None: - th = threading.Thread( - target=self._on_event, - args=( - self, - PageEventPayload(**msg.payload), - ), - daemon=True, - ) - th.start() - # self._on_event(self, PageEventPayload(**msg.payload)) - elif msg.action == Actions.SESSION_CREATED: - if self._on_session_created is not None: - th = threading.Thread( - target=self._on_session_created, - args=( - self, - PageSessionCreatedPayload(**msg.payload), - ), - daemon=True, - ) - th.start() - else: - # it's something else - print(msg.payload) - - def register_host_client( - self, - host_client_id: Optional[str], - page_name: str, - is_app: bool, - update: bool, - auth_token: Optional[str], - permissions: Optional[str], - ): - payload = RegisterHostClientRequestPayload( - host_client_id, page_name, is_app, update, auth_token, permissions - ) - response = self._send_message_with_result(Actions.REGISTER_HOST_CLIENT, payload) - return RegisterHostClientResponsePayload(**response) - def send_command(self, session_id: str, command: Command): - assert self.page_name is not None - payload = PageCommandRequestPayload(self.page_name, session_id, command) - response = self._send_message_with_result( - Actions.PAGE_COMMAND_FROM_HOST, payload - ) - result = PageCommandResponsePayload(**response) - if result.error: - raise Exception(result.error) - return result + raise NotImplementedError() def send_commands(self, session_id: str, commands: List[Command]): - assert self.page_name is not None - payload = PageCommandsBatchRequestPayload(self.page_name, session_id, commands) - response = self._send_message_with_result( - Actions.PAGE_COMMANDS_BATCH_FROM_HOST, payload - ) - result = PageCommandsBatchResponsePayload(**response) - if result.error: - raise Exception(result.error) - return result - - def _send_message_with_result(self, action_name, payload): - msg_id = uuid.uuid4().hex - msg = Message(msg_id, action_name, payload) - j = json.dumps(msg, cls=CommandEncoder, separators=(",", ":")) - logging.debug(f"_send_message_with_result: {j}") - evt = threading.Event() - self._ws_callbacks[msg_id] = (evt, None) - self._ws.send(j) - evt.wait() - return self._ws_callbacks.pop(msg_id)[1] + raise NotImplementedError() def close(self): - logging.debug("Closing connection...") - if self._ws is not None: - self._ws.close() + raise NotImplementedError() diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index 65f93e64f..b341affc3 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -14,7 +14,7 @@ from time import sleep from flet import constants, version -from flet.connection import Connection +from flet.sync_connection import SyncConnection from flet.event import Event from flet.page import Page from flet.reconnecting_websocket import ReconnectingWebSocket @@ -227,7 +227,7 @@ def on_session_created(conn, session_data): ws_url = _get_ws_url(server) ws = ReconnectingWebSocket(ws_url) - conn = Connection(ws) + conn = SyncConnection(ws) conn.on_event = on_event if session_handler is not None: @@ -248,13 +248,7 @@ def _on_ws_connect(): conn.page_url += f"/{conn.page_name}" connected.set() - def _on_ws_failed_connect(): - logging.info(f"Failed to connect: {ws_url}") - # if is_localhost_url(ws_url): - # _start_flet_server() - ws.on_connect = _on_ws_connect - ws.on_failed_connect = _on_ws_failed_connect ws.connect() for n in range(0, constants.CONNECT_TIMEOUT_SECONDS): if not connected.is_set(): diff --git a/sdk/python/flet/sync_connection.py b/sdk/python/flet/sync_connection.py new file mode 100644 index 000000000..6cc073352 --- /dev/null +++ b/sdk/python/flet/sync_connection.py @@ -0,0 +1,129 @@ +import json +import logging +import threading +from typing import List, Optional +import uuid +from flet.connection import Connection + +from flet.protocol import * +from flet.pubsub import PubSubHub +from flet.reconnecting_websocket import ReconnectingWebSocket + + +class SyncConnection(Connection): + def __init__(self, ws: ReconnectingWebSocket): + self._ws = ws + self._ws.on_message = self._on_message + self._ws_callbacks = {} + self._on_event = None + self._on_session_created = None + self.host_client_id: Optional[str] = None + self.page_name: Optional[str] = None + self.page_url: Optional[str] = None + self.sessions = {} + self.pubsubhub = PubSubHub() + + @property + def on_event(self): + return self._on_event + + @on_event.setter + def on_event(self, handler): + self._on_event = handler + + @property + def on_session_created(self): + return self._on_session_created + + @on_session_created.setter + def on_session_created(self, handler): + self._on_session_created = handler + + def _on_message(self, data): + logging.debug(f"_on_message: {data}") + msg_dict = json.loads(data) + msg = Message(**msg_dict) + if msg.id: + # callback + evt = self._ws_callbacks[msg.id][0] + self._ws_callbacks[msg.id] = (None, msg.payload) + evt.set() + elif msg.action == Actions.PAGE_EVENT_TO_HOST: + if self._on_event is not None: + th = threading.Thread( + target=self._on_event, + args=( + self, + PageEventPayload(**msg.payload), + ), + daemon=True, + ) + th.start() + # self._on_event(self, PageEventPayload(**msg.payload)) + elif msg.action == Actions.SESSION_CREATED: + if self._on_session_created is not None: + th = threading.Thread( + target=self._on_session_created, + args=( + self, + PageSessionCreatedPayload(**msg.payload), + ), + daemon=True, + ) + th.start() + else: + # it's something else + print(msg.payload) + + def register_host_client( + self, + host_client_id: Optional[str], + page_name: str, + is_app: bool, + update: bool, + auth_token: Optional[str], + permissions: Optional[str], + ): + payload = RegisterHostClientRequestPayload( + host_client_id, page_name, is_app, update, auth_token, permissions + ) + response = self._send_message_with_result(Actions.REGISTER_HOST_CLIENT, payload) + return RegisterHostClientResponsePayload(**response) + + def send_command(self, session_id: str, command: Command): + assert self.page_name is not None + payload = PageCommandRequestPayload(self.page_name, session_id, command) + response = self._send_message_with_result( + Actions.PAGE_COMMAND_FROM_HOST, payload + ) + result = PageCommandResponsePayload(**response) + if result.error: + raise Exception(result.error) + return result + + def send_commands(self, session_id: str, commands: List[Command]): + assert self.page_name is not None + payload = PageCommandsBatchRequestPayload(self.page_name, session_id, commands) + response = self._send_message_with_result( + Actions.PAGE_COMMANDS_BATCH_FROM_HOST, payload + ) + result = PageCommandsBatchResponsePayload(**response) + if result.error: + raise Exception(result.error) + return result + + def _send_message_with_result(self, action_name, payload): + msg_id = uuid.uuid4().hex + msg = Message(msg_id, action_name, payload) + j = json.dumps(msg, cls=CommandEncoder, separators=(",", ":")) + logging.debug(f"_send_message_with_result: {j}") + evt = threading.Event() + self._ws_callbacks[msg_id] = (evt, None) + self._ws.send(j) + evt.wait() + return self._ws_callbacks.pop(msg_id)[1] + + def close(self): + logging.debug("Closing connection...") + if self._ws is not None: + self._ws.close() From e742f4443c356e784dadcbd4d2091cfca4c264f7 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 12 Dec 2022 11:12:38 -0800 Subject: [PATCH 04/34] Refactored Connection --- sdk/python/flet/connection.py | 19 ++++- sdk/python/flet/flet.py | 83 ++++----------------- sdk/python/flet/sync_connection.py | 116 ++++++++++++++++------------- 3 files changed, 94 insertions(+), 124 deletions(-) diff --git a/sdk/python/flet/connection.py b/sdk/python/flet/connection.py index 840e536cb..70b6e971c 100644 --- a/sdk/python/flet/connection.py +++ b/sdk/python/flet/connection.py @@ -5,8 +5,7 @@ class Connection: def __init__(self): - self.host_client_id: Optional[str] = None - self.page_name: Optional[str] = None + self.page_name: str = "" self.page_url: Optional[str] = None self.sessions = {} self.pubsubhub = PubSubHub() @@ -14,8 +13,24 @@ def __init__(self): def send_command(self, session_id: str, command: Command): raise NotImplementedError() + async def send_command_async(self, session_id: str, command: Command): + raise NotImplementedError() + def send_commands(self, session_id: str, commands: List[Command]): raise NotImplementedError() + async def send_commands_async(self, session_id: str, commands: List[Command]): + raise NotImplementedError() + def close(self): raise NotImplementedError() + + def _get_ws_url(self, server: str): + url = server.rstrip("/") + if server.startswith("https://"): + url = url.replace("https://", "wss://") + elif server.startswith("http://"): + url = url.replace("http://", "ws://") + else: + url = "ws://" + url + return url + "/ws" diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index b341affc3..8edde9edb 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -51,23 +51,23 @@ def page( name="", host=None, port=0, - permissions=None, view: AppViewer = WEB_BROWSER, assets_dir=None, upload_dir=None, web_renderer="canvaskit", route_url_strategy="hash", + token=None, ): conn = _connect_internal( page_name=name, host=host, port=port, is_app=False, - permissions=permissions, assets_dir=assets_dir, upload_dir=upload_dir, web_renderer=web_renderer, route_url_strategy=route_url_strategy, + token=token, ) url_prefix = os.getenv("FLET_DISPLAY_URL_PREFIX") if url_prefix is not None: @@ -89,12 +89,12 @@ def app( host=None, port=0, target=None, - permissions=None, view: AppViewer = FLET_APP, assets_dir=None, upload_dir=None, web_renderer="canvaskit", route_url_strategy="hash", + token=None, ): if target is None: raise Exception("target argument is not specified") @@ -104,7 +104,7 @@ def app( host=host, port=port, is_app=True, - permissions=permissions, + token=token, session_handler=target, assets_dir=assets_dir, upload_dir=upload_dir, @@ -162,15 +162,13 @@ def exit_gracefully(signum, frame): def _connect_internal( - page_name=None, + page_name, host=None, port=0, is_app=False, - update=False, share=False, server=None, token=None, - permissions=None, session_handler=None, assets_dir=None, upload_dir=None, @@ -200,8 +198,6 @@ def _connect_internal( ) server = f"http://{server_ip}:{port}" - connected = threading.Event() - def on_event(conn, e): if e.sessionID in conn.sessions: conn.sessions[e.sessionID].on_event( @@ -225,40 +221,15 @@ def on_session_created(conn, session_data): ) page.error(f"There was an error while processing your request: {e}") - ws_url = _get_ws_url(server) - ws = ReconnectingWebSocket(ws_url) - conn = SyncConnection(ws) - conn.on_event = on_event - - if session_handler is not None: - conn.on_session_created = on_session_created - - def _on_ws_connect(): - if conn.page_name is None: - conn.page_name = page_name - assert conn.page_name is not None - result = conn.register_host_client( - conn.host_client_id, conn.page_name, is_app, update, token, permissions - ) - conn.host_client_id = result.hostClientID - conn.page_name = result.pageName - conn.page_url = server.rstrip("/") - if conn.page_name != constants.INDEX_PAGE: - assert conn.page_url is not None - conn.page_url += f"/{conn.page_name}" - connected.set() - - ws.on_connect = _on_ws_connect - ws.connect() - for n in range(0, constants.CONNECT_TIMEOUT_SECONDS): - if not connected.is_set(): - sleep(1) - if not connected.is_set(): - ws.close() - raise Exception( - f"Could not connected to Flet server in {constants.CONNECT_TIMEOUT_SECONDS} seconds." - ) - + conn = SyncConnection( + server_address=server, + page_name=page_name, + is_app=is_app, + token=token, + on_event=on_event, + on_session_created=on_session_created, + ) + conn.connect() return conn @@ -449,17 +420,6 @@ def open_flet_view(page_url, hidden): return subprocess.Popen(args, env=flet_env) -def _get_ws_url(server: str): - url = server.rstrip("/") - if server.startswith("https://"): - url = url.replace("https://", "wss://") - elif server.startswith("http://"): - url = url.replace("http://", "ws://") - else: - url = "ws://" + url - return url + "/ws" - - def _download_fletd(): ver = version.version flet_exe = "fletd.exe" if is_windows() else "fletd" @@ -503,21 +463,6 @@ def _download_flet_client(file_name): return str(temp_arch) -# not currently used, but maybe useful in the future -def _get_latest_flet_release(): - releases = json.loads( - urllib.request.urlopen( - f"https://api.github.com/repos/flet-dev/flet/releases?per_page=5" - ) - .read() - .decode() - ) - if len(releases) > 0: - return releases[0]["tag_name"].lstrip("v") - else: - return None - - # Fix: https://bugs.python.org/issue35935 # if _is_windows(): # signal.signal(signal.SIGINT, signal.SIG_DFL) diff --git a/sdk/python/flet/sync_connection.py b/sdk/python/flet/sync_connection.py index 6cc073352..37cce249d 100644 --- a/sdk/python/flet/sync_connection.py +++ b/sdk/python/flet/sync_connection.py @@ -1,57 +1,82 @@ import json import logging import threading +from time import sleep from typing import List, Optional import uuid +from flet import constants from flet.connection import Connection from flet.protocol import * -from flet.pubsub import PubSubHub from flet.reconnecting_websocket import ReconnectingWebSocket class SyncConnection(Connection): - def __init__(self, ws: ReconnectingWebSocket): - self._ws = ws - self._ws.on_message = self._on_message - self._ws_callbacks = {} - self._on_event = None - self._on_session_created = None - self.host_client_id: Optional[str] = None - self.page_name: Optional[str] = None - self.page_url: Optional[str] = None - self.sessions = {} - self.pubsubhub = PubSubHub() - - @property - def on_event(self): - return self._on_event - - @on_event.setter - def on_event(self, handler): - self._on_event = handler + def __init__( + self, + server_address: str, + page_name: str, + is_app: bool, + token: Optional[str], + on_event=None, + on_session_created=None, + ): + super().__init__() + self.page_name = page_name + self.__host_client_id: Optional[str] = None + self.__is_app = is_app + self.__token = token + self.__server_address = server_address + self.__ws = ReconnectingWebSocket(self._get_ws_url(server_address)) + self.__ws.on_connect = self.__on_ws_connect + self.__ws.on_message = self.__on_ws_message + self.__ws_callbacks = {} + self.__on_event = on_event + self.__on_session_created = on_session_created - @property - def on_session_created(self): - return self._on_session_created + def connect(self): + self.__connected = threading.Event() + self.__ws.connect() + for n in range(0, constants.CONNECT_TIMEOUT_SECONDS): + if not self.__connected.is_set(): + sleep(1) + if not self.__connected.is_set(): + self.__ws.close() + raise Exception( + f"Could not connected to Flet server in {constants.CONNECT_TIMEOUT_SECONDS} seconds." + ) - @on_session_created.setter - def on_session_created(self, handler): - self._on_session_created = handler + def __on_ws_connect(self): + payload = RegisterHostClientRequestPayload( + self.__host_client_id, + self.page_name, + self.__is_app, + update=False, + authToken=self.__token, + permissions=None, + ) + response = self._send_message_with_result(Actions.REGISTER_HOST_CLIENT, payload) + register_result = RegisterHostClientResponsePayload(**response) + self.__host_client_id = register_result.hostClientID + self.page_name = register_result.pageName + self.page_url = self.__server_address.rstrip("/") + if self.page_name != constants.INDEX_PAGE: + self.page_url += f"/{self.page_name}" + self.__connected.set() - def _on_message(self, data): + def __on_ws_message(self, data): logging.debug(f"_on_message: {data}") msg_dict = json.loads(data) msg = Message(**msg_dict) if msg.id: # callback - evt = self._ws_callbacks[msg.id][0] - self._ws_callbacks[msg.id] = (None, msg.payload) + evt = self.__ws_callbacks[msg.id][0] + self.__ws_callbacks[msg.id] = (None, msg.payload) evt.set() elif msg.action == Actions.PAGE_EVENT_TO_HOST: - if self._on_event is not None: + if self.__on_event is not None: th = threading.Thread( - target=self._on_event, + target=self.__on_event, args=( self, PageEventPayload(**msg.payload), @@ -61,9 +86,9 @@ def _on_message(self, data): th.start() # self._on_event(self, PageEventPayload(**msg.payload)) elif msg.action == Actions.SESSION_CREATED: - if self._on_session_created is not None: + if self.__on_session_created is not None: th = threading.Thread( - target=self._on_session_created, + target=self.__on_session_created, args=( self, PageSessionCreatedPayload(**msg.payload), @@ -75,21 +100,6 @@ def _on_message(self, data): # it's something else print(msg.payload) - def register_host_client( - self, - host_client_id: Optional[str], - page_name: str, - is_app: bool, - update: bool, - auth_token: Optional[str], - permissions: Optional[str], - ): - payload = RegisterHostClientRequestPayload( - host_client_id, page_name, is_app, update, auth_token, permissions - ) - response = self._send_message_with_result(Actions.REGISTER_HOST_CLIENT, payload) - return RegisterHostClientResponsePayload(**response) - def send_command(self, session_id: str, command: Command): assert self.page_name is not None payload = PageCommandRequestPayload(self.page_name, session_id, command) @@ -118,12 +128,12 @@ def _send_message_with_result(self, action_name, payload): j = json.dumps(msg, cls=CommandEncoder, separators=(",", ":")) logging.debug(f"_send_message_with_result: {j}") evt = threading.Event() - self._ws_callbacks[msg_id] = (evt, None) - self._ws.send(j) + self.__ws_callbacks[msg_id] = (evt, None) + self.__ws.send(j) evt.wait() - return self._ws_callbacks.pop(msg_id)[1] + return self.__ws_callbacks.pop(msg_id)[1] def close(self): logging.debug("Closing connection...") - if self._ws is not None: - self._ws.close() + if self.__ws is not None: + self.__ws.close() From f081c4037d96ad74dd7da2c13a2f6e4aa1c951cb Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 12 Dec 2022 11:53:30 -0800 Subject: [PATCH 05/34] Simplify event handlers --- sdk/python/flet/flet.py | 55 +---------------------- sdk/python/flet/reconnecting_websocket.py | 34 +++----------- sdk/python/flet/sync_connection.py | 16 +++---- 3 files changed, 16 insertions(+), 89 deletions(-) diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index 8edde9edb..ed19011c2 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -47,43 +47,6 @@ WebRenderer = Literal[None, "auto", "html", "canvaskit"] -def page( - name="", - host=None, - port=0, - view: AppViewer = WEB_BROWSER, - assets_dir=None, - upload_dir=None, - web_renderer="canvaskit", - route_url_strategy="hash", - token=None, -): - conn = _connect_internal( - page_name=name, - host=host, - port=port, - is_app=False, - assets_dir=assets_dir, - upload_dir=upload_dir, - web_renderer=web_renderer, - route_url_strategy=route_url_strategy, - token=token, - ) - url_prefix = os.getenv("FLET_DISPLAY_URL_PREFIX") - if url_prefix is not None: - print(url_prefix, conn.page_url) - else: - logging.info(f"Page URL: {conn.page_url}") - - page = Page(conn, constants.ZERO_SESSION) - conn.sessions[constants.ZERO_SESSION] = page - - if view == WEB_BROWSER: - open_in_browser(conn.page_url) - - return page - - def app( name="", host=None, @@ -103,7 +66,6 @@ def app( page_name=name, host=host, port=port, - is_app=True, token=token, session_handler=target, assets_dir=assets_dir, @@ -165,7 +127,6 @@ def _connect_internal( page_name, host=None, port=0, - is_app=False, share=False, server=None, token=None, @@ -183,14 +144,10 @@ def _connect_internal( if env_port is not None and env_port: port = env_port - # page with a custom port starts detached process - attached = False if not is_app and port != 0 else True - server_ip = host if host not in [None, "", "*"] else "127.0.0.1" port = _start_flet_server( host, port, - attached, assets_dir, upload_dir, web_renderer, @@ -224,7 +181,6 @@ def on_session_created(conn, session_data): conn = SyncConnection( server_address=server, page_name=page_name, - is_app=is_app, token=token, on_event=on_event, on_session_created=on_session_created, @@ -234,13 +190,12 @@ def on_session_created(conn, session_data): def _start_flet_server( - host, port, attached, assets_dir, upload_dir, web_renderer, route_url_strategy + host, port, assets_dir, upload_dir, web_renderer, route_url_strategy ): if port == 0: port = get_free_tcp_port() logging.info(f"Starting local Flet Server on port {port}...") - logging.info(f"Attached process: {attached}") fletd_exe = "fletd.exe" if is_windows() else "fletd" @@ -302,13 +257,7 @@ def _start_flet_server( creationflags = 0 start_new_session = False - if attached: - args.append("--attached") - else: - if is_windows(): - creationflags = subprocess.CREATE_NEW_PROCESS_GROUP - else: - start_new_session = True + args.append("--attached") log_level = logging.getLogger().getEffectiveLevel() if log_level == logging.CRITICAL: diff --git a/sdk/python/flet/reconnecting_websocket.py b/sdk/python/flet/reconnecting_websocket.py index e60b92925..5d865b015 100644 --- a/sdk/python/flet/reconnecting_websocket.py +++ b/sdk/python/flet/reconnecting_websocket.py @@ -11,11 +11,13 @@ class ReconnectingWebSocket: - def __init__(self, url) -> None: + def __init__( + self, url, on_connect=None, on_failed_connect=None, on_message=None + ) -> None: self._url = url - self._on_connect_handler = None - self._on_failed_connect_handler = None - self._on_message_handler = None + self._on_connect_handler = on_connect + self._on_failed_connect_handler = on_failed_connect + self._on_message_handler = on_message self.connected = threading.Event() self.exit = threading.Event() self.retry = 0 @@ -24,30 +26,6 @@ def __init__(self, url) -> None: ws_logger = logging.getLogger("websocket") ws_logger.setLevel(logging.FATAL) - @property - def on_connect(self, handler): - return self._on_connect_handler - - @on_connect.setter - def on_connect(self, handler): - self._on_connect_handler = handler - - @property - def on_failed_connect(self, handler): - return self._on_failed_connect_handler - - @on_failed_connect.setter - def on_failed_connect(self, handler): - self._on_failed_connect_handler = handler - - @property - def on_message(self, handler): - return self._on_message_handler - - @on_message.setter - def on_message(self, handler): - self._on_message_handler = handler - def _on_open(self, wsapp) -> None: logging.info(f"Successfully connected to {self._url}") websocket.setdefaulttimeout(self.default_timeout) diff --git a/sdk/python/flet/sync_connection.py b/sdk/python/flet/sync_connection.py index 37cce249d..76dcfbd1b 100644 --- a/sdk/python/flet/sync_connection.py +++ b/sdk/python/flet/sync_connection.py @@ -16,7 +16,6 @@ def __init__( self, server_address: str, page_name: str, - is_app: bool, token: Optional[str], on_event=None, on_session_created=None, @@ -24,12 +23,13 @@ def __init__( super().__init__() self.page_name = page_name self.__host_client_id: Optional[str] = None - self.__is_app = is_app self.__token = token self.__server_address = server_address - self.__ws = ReconnectingWebSocket(self._get_ws_url(server_address)) - self.__ws.on_connect = self.__on_ws_connect - self.__ws.on_message = self.__on_ws_message + self.__ws = ReconnectingWebSocket( + self._get_ws_url(server_address), + on_connect=self.__on_ws_connect, + on_message=self.__on_ws_message, + ) self.__ws_callbacks = {} self.__on_event = on_event self.__on_session_created = on_session_created @@ -48,9 +48,9 @@ def connect(self): def __on_ws_connect(self): payload = RegisterHostClientRequestPayload( - self.__host_client_id, - self.page_name, - self.__is_app, + hostClientID=self.__host_client_id, + pageName=self.page_name, + isApp=True, update=False, authToken=self.__token, permissions=None, From b83640d05d339721a12b2e22d4135bd04056412c Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Mon, 12 Dec 2022 17:11:31 -0800 Subject: [PATCH 06/34] macOS client is closed by a real PID --- client/lib/main.dart | 4 ++ client/macos/Runner.xcodeproj/project.pbxproj | 7 +-- client/macos/Runner/DebugProfile.entitlements | 2 +- client/macos/Runner/Release.entitlements | 2 +- sdk/python/flet/flet.py | 56 ++++++++++++------- sdk/python/flet/utils.py | 6 ++ 6 files changed, 51 insertions(+), 26 deletions(-) diff --git a/client/lib/main.dart b/client/lib/main.dart index b56654779..ec2423e68 100644 --- a/client/lib/main.dart +++ b/client/lib/main.dart @@ -37,6 +37,10 @@ void main([List? args]) async { throw Exception('Page URL must be provided as a first argument.'); } pageUrl = args[0]; + if (args.length > 1) { + var pidFile = await File(args[1]).create(); + await pidFile.writeAsString("$pid"); + } } debugPrint("Page URL: $pageUrl"); diff --git a/client/macos/Runner.xcodeproj/project.pbxproj b/client/macos/Runner.xcodeproj/project.pbxproj index c31c95112..363fe4b4a 100644 --- a/client/macos/Runner.xcodeproj/project.pbxproj +++ b/client/macos/Runner.xcodeproj/project.pbxproj @@ -55,7 +55,7 @@ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* flet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flet.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* Flet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Flet.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -112,7 +112,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* flet.app */, + 33CC10ED2044A3C60003C045 /* Flet.app */, ); name = Products; sourceTree = ""; @@ -159,7 +159,6 @@ EDAD244E5F1A9F15957004F8 /* Pods-Runner.release.xcconfig */, 3BFA430DEADD5855D5604A50 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -193,7 +192,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* flet.app */; + productReference = 33CC10ED2044A3C60003C045 /* Flet.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ diff --git a/client/macos/Runner/DebugProfile.entitlements b/client/macos/Runner/DebugProfile.entitlements index 08c3ab17c..78c36cf44 100644 --- a/client/macos/Runner/DebugProfile.entitlements +++ b/client/macos/Runner/DebugProfile.entitlements @@ -3,7 +3,7 @@ com.apple.security.app-sandbox - + com.apple.security.cs.allow-jit com.apple.security.network.server diff --git a/client/macos/Runner/Release.entitlements b/client/macos/Runner/Release.entitlements index ee95ab7e5..08ba3a3fa 100644 --- a/client/macos/Runner/Release.entitlements +++ b/client/macos/Runner/Release.entitlements @@ -3,7 +3,7 @@ com.apple.security.app-sandbox - + com.apple.security.network.client diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index ed19011c2..31213c3f3 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -28,6 +28,7 @@ is_macos, is_windows, open_in_browser, + random_string, safe_tar_extractall, which, ) @@ -92,13 +93,14 @@ def exit_gracefully(signum, frame): logging.info("Connected to Flet app and handling user sessions...") fvp = None + pid_file = None if ( (view == FLET_APP or view == FLET_APP_HIDDEN) and not is_linux_server() and url_prefix is None ): - fvp = open_flet_view(conn.page_url, view == FLET_APP_HIDDEN) + fvp, pid_file = open_flet_view(conn.page_url, view == FLET_APP_HIDDEN) try: fvp.wait() except (Exception) as e: @@ -115,12 +117,17 @@ def exit_gracefully(signum, frame): conn.close() - if fvp is not None and not is_windows(): + # close Flet.app started with "open" + if fvp is not None and pid_file is not None and os.path.exists(pid_file): try: - logging.debug(f"Flet View process {fvp.pid}") - os.kill(fvp.pid + 1, signal.SIGKILL) + with open(pid_file) as f: + fvp_pid = int(f.read()) + logging.debug(f"Flet View process {fvp_pid}") + os.kill(fvp_pid, signal.SIGKILL) except: pass + finally: + os.remove(pid_file) def _connect_internal( @@ -289,6 +296,7 @@ def open_flet_view(page_url, hidden): logging.info(f"Starting Flet View app...") args = [] + pid_file = None if is_windows(): flet_exe = "flet.exe" @@ -321,23 +329,31 @@ def open_flet_view(page_url, hidden): # build version-specific path to Flet.app temp_flet_dir = Path.home().joinpath(".flet", "bin", f"flet-{version.version}") - # check if flet_view.app exists in a temp directory - if not temp_flet_dir.exists(): - # check if flet.tar.gz exists - gz_filename = "flet-macos-amd64.tar.gz" - tar_file = Path(__file__).parent.joinpath("bin", gz_filename) - if not tar_file.exists(): - tar_file = _download_flet_client(gz_filename) - - logging.info(f"Extracting Flet.app from archive to {temp_flet_dir}") - temp_flet_dir.mkdir(parents=True, exist_ok=True) - with tarfile.open(str(tar_file), "r:gz") as tar_arch: - safe_tar_extractall(tar_arch, str(temp_flet_dir)) + # check if flet.exe is in PATH (flet developer mode) + flet_path = which("flet", sys.argv[0]) + if flet_path and "/Contents/MacOS/" in flet_path: + logging.info(f"Flet.app found in PATH: {flet_path}") + app_path = str(Path(flet_path).parent.parent.parent) else: - logging.info(f"Flet View found in: {temp_flet_dir}") + # check if flet_view.app exists in a temp directory + if not temp_flet_dir.exists(): + # check if flet.tar.gz exists + gz_filename = "flet-macos-amd64.tar.gz" + tar_file = Path(__file__).parent.joinpath("bin", gz_filename) + if not tar_file.exists(): + tar_file = _download_flet_client(gz_filename) + + logging.info(f"Extracting Flet.app from archive to {temp_flet_dir}") + temp_flet_dir.mkdir(parents=True, exist_ok=True) + with tarfile.open(str(tar_file), "r:gz") as tar_arch: + safe_tar_extractall(tar_arch, str(temp_flet_dir)) + else: + logging.info(f"Flet View found in: {temp_flet_dir}") + app_path = temp_flet_dir.joinpath("Flet.app") - app_path = temp_flet_dir.joinpath("Flet.app") - args = ["open", str(app_path), "-n", "-W", "--args", page_url] + # pid file - Flet client writes its process ID to this file + pid_file = str(Path(tempfile.gettempdir()).joinpath(random_string(20))) + args = ["open", str(app_path), "-n", "-W", "--args", page_url, pid_file] elif is_linux(): # build version-specific path to flet folder temp_flet_dir = Path.home().joinpath(".flet", "bin", f"flet-{version.version}") @@ -366,7 +382,7 @@ def open_flet_view(page_url, hidden): flet_env["FLET_HIDE_WINDOW_ON_START"] = "true" # execute process - return subprocess.Popen(args, env=flet_env) + return subprocess.Popen(args, env=flet_env), pid_file def _download_fletd(): diff --git a/sdk/python/flet/utils.py b/sdk/python/flet/utils.py index 0138f6de9..3d9c97703 100644 --- a/sdk/python/flet/utils.py +++ b/sdk/python/flet/utils.py @@ -1,7 +1,9 @@ import math import os import platform +import random import socket +import string import sys import unicodedata import webbrowser @@ -59,6 +61,10 @@ def open_in_browser(url): webbrowser.open(url) +def random_string(length): + return "".join(random.choice(string.ascii_letters) for i in range(length)) + + # https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python def which(program, exclude_exe=None): import os From 69e7822c8474241214656029d24d2033241afbf4 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 14 Dec 2022 11:21:04 -0800 Subject: [PATCH 07/34] flet.py -> sync/async --- sdk/python/flet/async_connection.py | 50 ++++++ sdk/python/flet/constants.py | 1 - sdk/python/flet/flet.py | 239 ++++++++++++++++++++++++---- sdk/python/flet/utils.py | 15 ++ 4 files changed, 271 insertions(+), 34 deletions(-) create mode 100644 sdk/python/flet/async_connection.py diff --git a/sdk/python/flet/async_connection.py b/sdk/python/flet/async_connection.py new file mode 100644 index 000000000..41bc28bc6 --- /dev/null +++ b/sdk/python/flet/async_connection.py @@ -0,0 +1,50 @@ +from asyncio import sleep +import asyncio +import json +import logging +from typing import List, Optional +import uuid +from flet import constants +from flet.connection import Connection + +from flet.protocol import * + + +class AsyncConnection(Connection): + def __init__( + self, + server_address: str, + page_name: str, + token: Optional[str], + on_event=None, + on_session_created=None, + ): + super().__init__() + self.page_name = page_name + self.__host_client_id: Optional[str] = None + self.__token = token + self.__server_address = server_address + self.__on_event = on_event + self.__on_session_created = on_session_created + + async def connect(self): + print("Connecting async...") + self.page_url = "http://localhost:8550" + self.task = asyncio.get_event_loop().create_task(self.__connect_async()) + + async def __connect_async(self): + i = 1 + while True: + print(i) + await sleep(1) + i += 1 + + async def send_command_async(self, session_id: str, command: Command): + pass + + async def send_commands_async(self, session_id: str, commands: List[Command]): + pass + + def close(self): + logging.debug("Closing connection...") + self.task.cancel() diff --git a/sdk/python/flet/constants.py b/sdk/python/flet/constants.py index df119b68a..8e082df76 100644 --- a/sdk/python/flet/constants.py +++ b/sdk/python/flet/constants.py @@ -1,4 +1,3 @@ INDEX_PAGE = "p/index" -HOSTED_SERVICE_URL = "https://app.flet.dev" CONNECT_TIMEOUT_SECONDS = 30 ZERO_SESSION = "0" diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index 31213c3f3..c9267eeff 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -1,4 +1,5 @@ -import json +import asyncio +import inspect import logging import os import signal @@ -14,6 +15,7 @@ from time import sleep from flet import constants, version +from flet.async_connection import AsyncConnection from flet.sync_connection import SyncConnection from flet.event import Event from flet.page import Page @@ -23,6 +25,7 @@ get_current_script_dir, get_free_tcp_port, get_platform, + is_async_method, is_linux, is_linux_server, is_macos, @@ -49,25 +52,65 @@ def app( + target, name="", host=None, port=0, - target=None, view: AppViewer = FLET_APP, assets_dir=None, upload_dir=None, web_renderer="canvaskit", route_url_strategy="hash", - token=None, + auth_token=None, ): - if target is None: - raise Exception("target argument is not specified") - conn = _connect_internal( + if inspect.iscoroutinefunction(target): + asyncio.run( + app_async( + target=target, + name=name, + host=host, + port=port, + view=view, + assets_dir=assets_dir, + upload_dir=upload_dir, + web_renderer=web_renderer, + route_url_strategy=route_url_strategy, + auth_token=auth_token, + ) + ) + else: + __app_sync( + target=target, + name=name, + host=host, + port=port, + view=view, + assets_dir=assets_dir, + upload_dir=upload_dir, + web_renderer=web_renderer, + route_url_strategy=route_url_strategy, + auth_token=auth_token, + ) + + +def __app_sync( + target, + name="", + host=None, + port=0, + view: AppViewer = FLET_APP, + assets_dir=None, + upload_dir=None, + web_renderer="canvaskit", + route_url_strategy="hash", + auth_token=None, +): + conn = __connect_internal( page_name=name, host=host, port=port, - token=token, + auth_token=auth_token, session_handler=target, assets_dir=assets_dir, upload_dir=upload_dir, @@ -117,7 +160,7 @@ def exit_gracefully(signum, frame): conn.close() - # close Flet.app started with "open" + # close Flet.app started with "open" on macOS if fvp is not None and pid_file is not None and os.path.exists(pid_file): try: with open(pid_file) as f: @@ -130,29 +173,100 @@ def exit_gracefully(signum, frame): os.remove(pid_file) -def _connect_internal( +async def app_async( + target, + name="", + host=None, + port=0, + view: AppViewer = FLET_APP, + assets_dir=None, + upload_dir=None, + web_renderer="canvaskit", + route_url_strategy="hash", + auth_token=None, +): + + conn = await __connect_internal_async( + page_name=name, + host=host, + port=port, + auth_token=auth_token, + session_handler=target, + assets_dir=assets_dir, + upload_dir=upload_dir, + web_renderer=web_renderer, + route_url_strategy=route_url_strategy, + ) + + url_prefix = os.getenv("FLET_DISPLAY_URL_PREFIX") + if url_prefix is not None: + print(url_prefix, conn.page_url) + else: + logging.info(f"App URL: {conn.page_url}") + + terminate = asyncio.Event() + + def exit_gracefully(signum, frame): + logging.debug("Gracefully terminating Flet app...") + terminate.set() + + signal.signal(signal.SIGINT, exit_gracefully) + signal.signal(signal.SIGTERM, exit_gracefully) + + logging.info("Connected to Flet app and handling user sessions...") + + fvp = None + pid_file = None + + if ( + (view == FLET_APP or view == FLET_APP_HIDDEN) + and not is_linux_server() + and url_prefix is None + ): + fvp, pid_file = await open_flet_view_async( + conn.page_url, view == FLET_APP_HIDDEN + ) + try: + await fvp.wait() + except (Exception) as e: + pass + else: + if view == WEB_BROWSER and url_prefix is None: + open_in_browser(conn.page_url) + try: + await terminate.wait() + except KeyboardInterrupt: + pass + + conn.close() + + # close Flet.app started with "open" on macOS + if fvp is not None and pid_file is not None and os.path.exists(pid_file): + try: + with open(pid_file) as f: + fvp_pid = int(f.read()) + logging.debug(f"Flet View process {fvp_pid}") + os.kill(fvp_pid, signal.SIGKILL) + except: + pass + finally: + os.remove(pid_file) + + +def __connect_internal( page_name, host=None, port=0, - share=False, server=None, - token=None, + auth_token=None, session_handler=None, assets_dir=None, upload_dir=None, web_renderer=None, route_url_strategy=None, ): - if share and server is None: - server = constants.HOSTED_SERVICE_URL - elif server is None: - # local mode - env_port = os.getenv("FLET_SERVER_PORT") - if env_port is not None and env_port: - port = env_port - - server_ip = host if host not in [None, "", "*"] else "127.0.0.1" - port = _start_flet_server( + if server is None: + server = __start_flet_server( host, port, assets_dir, @@ -160,7 +274,6 @@ def _connect_internal( web_renderer, route_url_strategy, ) - server = f"http://{server_ip}:{port}" def on_event(conn, e): if e.sessionID in conn.sessions: @@ -188,7 +301,7 @@ def on_session_created(conn, session_data): conn = SyncConnection( server_address=server, page_name=page_name, - token=token, + token=auth_token, on_event=on_event, on_session_created=on_session_created, ) @@ -196,9 +309,57 @@ def on_session_created(conn, session_data): return conn -def _start_flet_server( +async def __connect_internal_async( + page_name, + host=None, + port=0, + server=None, + auth_token=None, + session_handler=None, + assets_dir=None, + upload_dir=None, + web_renderer=None, + route_url_strategy=None, +): + if server is None: + server = __start_flet_server( + host, + port, + assets_dir, + upload_dir, + web_renderer, + route_url_strategy, + ) + + async def on_event(conn, e): + # TODO + pass + + async def on_session_created(conn, session_data): + # TODO + pass + + conn = AsyncConnection( + server_address=server, + page_name=page_name, + token=auth_token, + on_event=on_event, + on_session_created=on_session_created, + ) + await conn.connect() + return conn + + +def __start_flet_server( host, port, assets_dir, upload_dir, web_renderer, route_url_strategy ): + # local mode + env_port = os.getenv("FLET_SERVER_PORT") + if env_port is not None and env_port: + port = env_port + + server_ip = host if host not in [None, "", "*"] else "127.0.0.1" + if port == 0: port = get_free_tcp_port() @@ -216,7 +377,7 @@ def _start_flet_server( fletd_path = which(fletd_exe) if not fletd_path: # download flet from GitHub (python module developer mode) - fletd_path = _download_fletd() + fletd_path = __download_fletd() else: logging.info(f"Flet Server found in PATH") @@ -289,10 +450,23 @@ def _start_flet_server( startupinfo=startupinfo, ) - return port + return f"http://{server_ip}:{port}" def open_flet_view(page_url, hidden): + args, flet_env, pid_file = __locate_and_unpack_flet_view(page_url, hidden) + return subprocess.Popen(args, env=flet_env), pid_file + + +async def open_flet_view_async(page_url, hidden): + args, flet_env, pid_file = __locate_and_unpack_flet_view(page_url, hidden) + return ( + await asyncio.create_subprocess_exec(args[0], *args[1:], env=flet_env), + pid_file, + ) + + +def __locate_and_unpack_flet_view(page_url, hidden): logging.info(f"Starting Flet View app...") args = [] @@ -317,7 +491,7 @@ def open_flet_view(page_url, hidden): logging.info(f"Flet View found in PATH: {flet_path}") else: if not temp_flet_dir.exists(): - zip_file = _download_flet_client("flet-windows.zip") + zip_file = __download_flet_client("flet-windows.zip") logging.info(f"Extracting flet.exe from archive to {temp_flet_dir}") temp_flet_dir.mkdir(parents=True, exist_ok=True) @@ -341,7 +515,7 @@ def open_flet_view(page_url, hidden): gz_filename = "flet-macos-amd64.tar.gz" tar_file = Path(__file__).parent.joinpath("bin", gz_filename) if not tar_file.exists(): - tar_file = _download_flet_client(gz_filename) + tar_file = __download_flet_client(gz_filename) logging.info(f"Extracting Flet.app from archive to {temp_flet_dir}") temp_flet_dir.mkdir(parents=True, exist_ok=True) @@ -364,7 +538,7 @@ def open_flet_view(page_url, hidden): gz_filename = f"flet-linux-{get_arch()}.tar.gz" tar_file = Path(__file__).parent.joinpath("bin", gz_filename) if not tar_file.exists(): - tar_file = _download_flet_client(gz_filename) + tar_file = __download_flet_client(gz_filename) logging.info(f"Extracting Flet from archive to {temp_flet_dir}") temp_flet_dir.mkdir(parents=True, exist_ok=True) @@ -381,11 +555,10 @@ def open_flet_view(page_url, hidden): if hidden: flet_env["FLET_HIDE_WINDOW_ON_START"] = "true" - # execute process - return subprocess.Popen(args, env=flet_env), pid_file + return args, flet_env, pid_file -def _download_fletd(): +def __download_fletd(): ver = version.version flet_exe = "fletd.exe" if is_windows() else "fletd" @@ -419,7 +592,7 @@ def _download_fletd(): return str(temp_fletd_dir.joinpath(flet_exe)) -def _download_flet_client(file_name): +def __download_flet_client(file_name): ver = version.version temp_arch = Path(tempfile.gettempdir()).joinpath(file_name) logging.info(f"Downloading Flet v{ver} to {temp_arch}") diff --git a/sdk/python/flet/utils.py b/sdk/python/flet/utils.py index 3d9c97703..cd481fb95 100644 --- a/sdk/python/flet/utils.py +++ b/sdk/python/flet/utils.py @@ -1,3 +1,4 @@ +import inspect import math import os import platform @@ -65,6 +66,20 @@ def random_string(length): return "".join(random.choice(string.ascii_letters) for i in range(length)) +def is_sync_method(obj, method_name) -> bool: + method = getattr(obj, method_name, None) + if method is None: + return False + return callable(method) and not inspect.iscoroutinefunction(method) + + +def is_async_method(obj, method_name) -> bool: + method = getattr(obj, method_name, None) + if method is None: + return False + return callable(method) and inspect.iscoroutinefunction(method) + + # https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python def which(program, exclude_exe=None): import os From 19494e0be466ac1c198d708b008885176da33f88 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 15 Dec 2022 09:24:34 -0800 Subject: [PATCH 08/34] close_flet_view for both sync/async --- sdk/python/flet/flet.py | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index c9267eeff..3e7507263 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -12,20 +12,17 @@ import urllib.request import zipfile from pathlib import Path -from time import sleep -from flet import constants, version +from flet import version from flet.async_connection import AsyncConnection from flet.sync_connection import SyncConnection from flet.event import Event from flet.page import Page -from flet.reconnecting_websocket import ReconnectingWebSocket from flet.utils import ( get_arch, get_current_script_dir, get_free_tcp_port, get_platform, - is_async_method, is_linux, is_linux_server, is_macos, @@ -106,7 +103,7 @@ def __app_sync( route_url_strategy="hash", auth_token=None, ): - conn = __connect_internal( + conn = __connect_internal_sync( page_name=name, host=host, port=port, @@ -159,18 +156,7 @@ def exit_gracefully(signum, frame): pass conn.close() - - # close Flet.app started with "open" on macOS - if fvp is not None and pid_file is not None and os.path.exists(pid_file): - try: - with open(pid_file) as f: - fvp_pid = int(f.read()) - logging.debug(f"Flet View process {fvp_pid}") - os.kill(fvp_pid, signal.SIGKILL) - except: - pass - finally: - os.remove(pid_file) + close_flet_view(pid_file) async def app_async( @@ -239,9 +225,11 @@ def exit_gracefully(signum, frame): pass conn.close() + close_flet_view(pid_file) - # close Flet.app started with "open" on macOS - if fvp is not None and pid_file is not None and os.path.exists(pid_file): + +def close_flet_view(pid_file): + if pid_file is not None and os.path.exists(pid_file): try: with open(pid_file) as f: fvp_pid = int(f.read()) @@ -253,7 +241,7 @@ def exit_gracefully(signum, frame): os.remove(pid_file) -def __connect_internal( +def __connect_internal_sync( page_name, host=None, port=0, @@ -470,7 +458,9 @@ def __locate_and_unpack_flet_view(page_url, hidden): logging.info(f"Starting Flet View app...") args = [] - pid_file = None + + # pid file - Flet client writes its process ID to this file + pid_file = str(Path(tempfile.gettempdir()).joinpath(random_string(20))) if is_windows(): flet_exe = "flet.exe" @@ -498,7 +488,7 @@ def __locate_and_unpack_flet_view(page_url, hidden): with zipfile.ZipFile(zip_file, "r") as zip_arch: zip_arch.extractall(str(temp_flet_dir)) flet_path = str(temp_flet_dir.joinpath("flet", flet_exe)) - args = [flet_path, page_url] + args = [flet_path, page_url, pid_file] elif is_macos(): # build version-specific path to Flet.app temp_flet_dir = Path.home().joinpath(".flet", "bin", f"flet-{version.version}") @@ -525,8 +515,6 @@ def __locate_and_unpack_flet_view(page_url, hidden): logging.info(f"Flet View found in: {temp_flet_dir}") app_path = temp_flet_dir.joinpath("Flet.app") - # pid file - Flet client writes its process ID to this file - pid_file = str(Path(tempfile.gettempdir()).joinpath(random_string(20))) args = ["open", str(app_path), "-n", "-W", "--args", page_url, pid_file] elif is_linux(): # build version-specific path to flet folder @@ -548,7 +536,7 @@ def __locate_and_unpack_flet_view(page_url, hidden): logging.info(f"Flet View found in: {temp_flet_dir}") app_path = temp_flet_dir.joinpath("flet", "flet") - args = [str(app_path), page_url] + args = [str(app_path), page_url, pid_file] flet_env = {**os.environ} From 38050be7c2ef7cec67df10a91e112004b4eeb073 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 15 Dec 2022 11:06:35 -0800 Subject: [PATCH 09/34] a working AsyncConnection --- sdk/python/flet/async_connection.py | 138 +++++++++++++++++++++++++--- sdk/python/flet/connection.py | 3 - sdk/python/flet/flet.py | 12 +-- 3 files changed, 129 insertions(+), 24 deletions(-) diff --git a/sdk/python/flet/async_connection.py b/sdk/python/flet/async_connection.py index 41bc28bc6..9fa0db3eb 100644 --- a/sdk/python/flet/async_connection.py +++ b/sdk/python/flet/async_connection.py @@ -6,8 +6,10 @@ import uuid from flet import constants from flet.connection import Connection - +import websockets.client as ws_client +from websockets.client import WebSocketClientProtocol from flet.protocol import * +from asyncio.queues import Queue class AsyncConnection(Connection): @@ -15,29 +17,119 @@ def __init__( self, server_address: str, page_name: str, - token: Optional[str], + auth_token: Optional[str], on_event=None, on_session_created=None, ): super().__init__() + self.__send_queue = Queue(1) self.page_name = page_name - self.__host_client_id: Optional[str] = None - self.__token = token self.__server_address = server_address + self.__host_client_id: Optional[str] = None + self.__auth_token = auth_token + self.__ws_callbacks = {} self.__on_event = on_event self.__on_session_created = on_session_created async def connect(self): - print("Connecting async...") - self.page_url = "http://localhost:8550" - self.task = asyncio.get_event_loop().create_task(self.__connect_async()) + ws_url = self._get_ws_url(self.__server_address) + print(f"Connecting async to {ws_url}...") + + attempts = 50 + for n in range(0, attempts): + try: + self.__ws: WebSocketClientProtocol = await ws_client.connect(ws_url) + break + except Exception as e: + logging.debug(f"Error connecting to Flet server: {e}") + if n == attempts - 1: + raise Exception( + f"Failed to connect Flet server in {attempts} attempts." + ) + await asyncio.sleep(0.2) + logging.info(f"Connected to Flet server {self.__server_address}") + + # start send/receive loops + asyncio.get_event_loop().create_task(self.__start_loops()) + + await self.__register_host_client() - async def __connect_async(self): - i = 1 + async def __register_host_client(self): + payload = RegisterHostClientRequestPayload( + hostClientID=self.__host_client_id, + pageName=self.page_name, + isApp=True, + update=False, + authToken=self.__auth_token, + permissions=None, + ) + response = await self._send_message_with_result( + Actions.REGISTER_HOST_CLIENT, payload + ) + register_result = RegisterHostClientResponsePayload(**response) + self.__host_client_id = register_result.hostClientID + self.page_name = register_result.pageName + self.page_url = self.__server_address.rstrip("/") + if self.page_name != constants.INDEX_PAGE: + self.page_url += f"/{self.page_name}" + + async def __start_loops(self): + self.__receive_loop_task = asyncio.create_task(self.__receive_looop()) + self.__send_loop_task = asyncio.create_task(self.__send_loop()) + done, pending = await asyncio.wait( + [self.__receive_loop_task, self.__send_loop_task], + return_when=asyncio.FIRST_COMPLETED, + ) + failed = False + for task in done: + name = task.get_name() + exception = task.exception() + if isinstance(exception, Exception): + logging.error(f"{name} threw {exception}") + failed = True + for task in pending: + task.cancel() + + # re-connect if one of tasks failed + if failed: + logging.debug(f"Re-connecting to Flet server in 1 second") + await asyncio.sleep(1) + await self.connect() + + async def __on_ws_message(self, data): + logging.debug(f"_on_message: {data}") + msg_dict = json.loads(data) + msg = Message(**msg_dict) + if msg.id: + # callback + evt = self.__ws_callbacks[msg.id][0] + self.__ws_callbacks[msg.id] = (None, msg.payload) + evt.set() + elif msg.action == Actions.PAGE_EVENT_TO_HOST: + if self.__on_event is not None: + asyncio.create_task(self.__on_event(PageEventPayload(**msg.payload))) + elif msg.action == Actions.SESSION_CREATED: + if self.__on_session_created is not None: + asyncio.create_task( + self.__on_session_created(PageSessionCreatedPayload(**msg.payload)) + ) + else: + # it's something else + print(msg.payload) + + async def __receive_looop(self): + async for message in self.__ws: + await self.__on_ws_message(message) + + async def __send_loop(self): while True: - print(i) - await sleep(1) - i += 1 + message = await self.__send_queue.get() + try: + await self.__ws.send(message) + except: + # re-enqueue the message to repeat it when re-connected + self.__send_queue.put_nowait(message) + raise async def send_command_async(self, session_id: str, command: Command): pass @@ -45,6 +137,22 @@ async def send_command_async(self, session_id: str, command: Command): async def send_commands_async(self, session_id: str, commands: List[Command]): pass - def close(self): - logging.debug("Closing connection...") - self.task.cancel() + async def _send_message_with_result(self, action_name, payload): + msg_id = uuid.uuid4().hex + msg = Message(msg_id, action_name, payload) + j = json.dumps(msg, cls=CommandEncoder, separators=(",", ":")) + logging.debug(f"_send_message_with_result: {j}") + evt = asyncio.Event() + self.__ws_callbacks[msg_id] = (evt, None) + await self.__send_queue.put(j) + await evt.wait() + return self.__ws_callbacks.pop(msg_id)[1] + + async def close(self): + logging.debug("Closing WebSockets connection...") + if self.__receive_loop_task: + self.__receive_loop_task.cancel() + if self.__send_loop_task: + self.__send_loop_task.cancel() + if self.__ws: + await self.__ws.close() diff --git a/sdk/python/flet/connection.py b/sdk/python/flet/connection.py index 70b6e971c..8d379c36c 100644 --- a/sdk/python/flet/connection.py +++ b/sdk/python/flet/connection.py @@ -22,9 +22,6 @@ def send_commands(self, session_id: str, commands: List[Command]): async def send_commands_async(self, session_id: str, commands: List[Command]): raise NotImplementedError() - def close(self): - raise NotImplementedError() - def _get_ws_url(self, server: str): url = server.rstrip("/") if server.startswith("https://"): diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index 3e7507263..bf106b8fa 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -224,7 +224,7 @@ def exit_gracefully(signum, frame): except KeyboardInterrupt: pass - conn.close() + await conn.close() close_flet_view(pid_file) @@ -319,18 +319,18 @@ async def __connect_internal_async( route_url_strategy, ) - async def on_event(conn, e): - # TODO + async def on_event(e): + print("ON EVENT:", e) pass - async def on_session_created(conn, session_data): - # TODO + async def on_session_created(session_data): + print("ON SESSION CREATED:", session_data) pass conn = AsyncConnection( server_address=server, page_name=page_name, - token=auth_token, + auth_token=auth_token, on_event=on_event, on_session_created=on_session_created, ) From bbc0e8091367bb78b0caa046049411d0c7ad710f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 15 Dec 2022 11:29:02 -0800 Subject: [PATCH 10/34] AsyncConnection complete --- sdk/python/flet/async_connection.py | 43 +++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/sdk/python/flet/async_connection.py b/sdk/python/flet/async_connection.py index 9fa0db3eb..342dfb051 100644 --- a/sdk/python/flet/async_connection.py +++ b/sdk/python/flet/async_connection.py @@ -1,4 +1,3 @@ -from asyncio import sleep import asyncio import json import logging @@ -13,6 +12,9 @@ class AsyncConnection(Connection): + __CONNECT_TIMEOUT = 0.2 + __CONNECT_ATTEMPTS = 50 + def __init__( self, server_address: str, @@ -25,6 +27,7 @@ def __init__( self.__send_queue = Queue(1) self.page_name = page_name self.__server_address = server_address + self.__is_reconnecting = False self.__host_client_id: Optional[str] = None self.__auth_token = auth_token self.__ws_callbacks = {} @@ -33,21 +36,23 @@ def __init__( async def connect(self): ws_url = self._get_ws_url(self.__server_address) - print(f"Connecting async to {ws_url}...") + logging.debug(f"Connecting via WebSockets to {ws_url}...") - attempts = 50 - for n in range(0, attempts): + attempt = self.__CONNECT_ATTEMPTS + while True: try: self.__ws: WebSocketClientProtocol = await ws_client.connect(ws_url) break except Exception as e: logging.debug(f"Error connecting to Flet server: {e}") - if n == attempts - 1: + if attempt == 0 and not self.__is_reconnecting: raise Exception( - f"Failed to connect Flet server in {attempts} attempts." + f"Failed to connect Flet server in {self.__CONNECT_ATTEMPTS} attempts." ) - await asyncio.sleep(0.2) - logging.info(f"Connected to Flet server {self.__server_address}") + attempt -= 1 + await asyncio.sleep(self.__CONNECT_TIMEOUT) + logging.debug(f"Connected to Flet server {self.__server_address}") + self.__is_reconnecting = True # start send/receive loops asyncio.get_event_loop().create_task(self.__start_loops()) @@ -93,7 +98,7 @@ async def __start_loops(self): # re-connect if one of tasks failed if failed: logging.debug(f"Re-connecting to Flet server in 1 second") - await asyncio.sleep(1) + await asyncio.sleep(self.__CONNECT_TIMEOUT) await self.connect() async def __on_ws_message(self, data): @@ -132,10 +137,26 @@ async def __send_loop(self): raise async def send_command_async(self, session_id: str, command: Command): - pass + assert self.page_name is not None + payload = PageCommandRequestPayload(self.page_name, session_id, command) + response = await self._send_message_with_result( + Actions.PAGE_COMMAND_FROM_HOST, payload + ) + result = PageCommandResponsePayload(**response) + if result.error: + raise Exception(result.error) + return result async def send_commands_async(self, session_id: str, commands: List[Command]): - pass + assert self.page_name is not None + payload = PageCommandsBatchRequestPayload(self.page_name, session_id, commands) + response = await self._send_message_with_result( + Actions.PAGE_COMMANDS_BATCH_FROM_HOST, payload + ) + result = PageCommandsBatchResponsePayload(**response) + if result.error: + raise Exception(result.error) + return result async def _send_message_with_result(self, action_name, payload): msg_id = uuid.uuid4().hex From ca774189628501126aff285493a9a9ecb8fa2e99 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 15 Dec 2022 11:46:50 -0800 Subject: [PATCH 11/34] Async session started --- sdk/python/flet/async_connection.py | 5 ++- sdk/python/flet/flet.py | 16 ++++++++-- sdk/python/flet/page.py | 49 ++++++++++++++++++----------- 3 files changed, 49 insertions(+), 21 deletions(-) diff --git a/sdk/python/flet/async_connection.py b/sdk/python/flet/async_connection.py index 342dfb051..182391f36 100644 --- a/sdk/python/flet/async_connection.py +++ b/sdk/python/flet/async_connection.py @@ -176,4 +176,7 @@ async def close(self): if self.__send_loop_task: self.__send_loop_task.cancel() if self.__ws: - await self.__ws.close() + try: + await self.__ws.close() + except: + pass # do nothing diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index bf106b8fa..59ca23483 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -274,6 +274,7 @@ def on_event(conn, e): def on_session_created(conn, session_data): page = Page(conn, session_data.sessionID) + page.fetch_page_details() conn.sessions[session_data.sessionID] = page logging.info(f"Session started: {session_data.sessionID}") try: @@ -324,8 +325,19 @@ async def on_event(e): pass async def on_session_created(session_data): - print("ON SESSION CREATED:", session_data) - pass + page = Page(conn, session_data.sessionID) + await page.fetch_page_details_async() + conn.sessions[session_data.sessionID] = page + logging.info(f"Session started: {session_data.sessionID}") + try: + assert session_handler is not None + await session_handler(page) + except Exception as e: + print( + f"Unhandled error processing page session {page.session_id}:", + traceback.format_exc(), + ) + page.error(f"There was an error while processing your request: {e}") conn = AsyncConnection( server_address=server, diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index 5ae4132ab..f43e22732 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -84,7 +84,6 @@ def __init__(self, conn: Connection, session_id): self._index = {self._Control__uid: self} # index with all page controls self._last_event = None self._event_available = threading.Event() - self._fetch_page_details() self.__views = [View()] self.__default_view = self.__views[0] @@ -180,27 +179,41 @@ def _get_children(self): children.append(self.__offstage) return children - def _fetch_page_details(self): + def fetch_page_details(self): assert self.__conn.page_name is not None values = self.__conn.send_commands( self._session_id, - [ - Command(0, "get", ["page", "route"]), - Command( - 0, - "get", - ["page", "pwa"], - ), - Command(0, "get", ["page", "web"]), - Command(0, "get", ["page", "platform"]), - Command(0, "get", ["page", "width"]), - Command(0, "get", ["page", "height"]), - Command(0, "get", ["page", "windowWidth"]), - Command(0, "get", ["page", "windowHeight"]), - Command(0, "get", ["page", "windowTop"]), - Command(0, "get", ["page", "windowLeft"]), - ], + self.__get_page_detail_commands(), ).results + self.__set_page_details(values) + + async def fetch_page_details_async(self): + assert self.__conn.page_name is not None + values = (await self.__conn.send_commands_async( + self._session_id, + self.__get_page_detail_commands(), + )).results + self.__set_page_details(values) + + def __get_page_detail_commands(self): + return [ + Command(0, "get", ["page", "route"]), + Command( + 0, + "get", + ["page", "pwa"], + ), + Command(0, "get", ["page", "web"]), + Command(0, "get", ["page", "platform"]), + Command(0, "get", ["page", "width"]), + Command(0, "get", ["page", "height"]), + Command(0, "get", ["page", "windowWidth"]), + Command(0, "get", ["page", "windowHeight"]), + Command(0, "get", ["page", "windowTop"]), + Command(0, "get", ["page", "windowLeft"]), + ] + + def __set_page_details(self, values): self._set_attr("route", values[0], False) self._set_attr("pwa", values[1], False) self._set_attr("web", values[2], False) From 171ecfcfb71caa115f6f8ddaa1892b0b61ffc0f7 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 15 Dec 2022 11:54:24 -0800 Subject: [PATCH 12/34] Update control.py --- sdk/python/flet/control.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sdk/python/flet/control.py b/sdk/python/flet/control.py index 27e221222..47d98c1b3 100644 --- a/sdk/python/flet/control.py +++ b/sdk/python/flet/control.py @@ -1,6 +1,7 @@ import datetime as dt import json import threading +import asyncio from difflib import SequenceMatcher from typing import TYPE_CHECKING, Any, Union @@ -52,6 +53,7 @@ def __init__( self.data = data self.__event_handlers = {} self._lock = threading.Lock() + self._async_lock = asyncio.Lock() if ref: ref.current = self @@ -67,9 +69,15 @@ def _before_build_command(self): def did_mount(self): pass + async def did_mount_async(self): + pass + def will_unmount(self): pass + async def will_unmount_async(self): + pass + def _get_children(self): return [] From 278d5325a6fa44ce1056794a583f40336065a6f9 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 15 Dec 2022 13:49:15 -0800 Subject: [PATCH 13/34] some clean() methods deprecated --- sdk/python/flet/column.py | 4 ---- sdk/python/flet/grid_view.py | 4 ---- sdk/python/flet/list_view.py | 4 ---- sdk/python/flet/row.py | 4 ---- 4 files changed, 16 deletions(-) diff --git a/sdk/python/flet/column.py b/sdk/python/flet/column.py index e610b6454..334f59ffc 100644 --- a/sdk/python/flet/column.py +++ b/sdk/python/flet/column.py @@ -140,10 +140,6 @@ def _get_control_name(self): def _get_children(self): return self.__controls - def clean(self): - Control.clean(self) - self.__controls.clear() - # tight @property def tight(self) -> Optional[bool]: diff --git a/sdk/python/flet/grid_view.py b/sdk/python/flet/grid_view.py index f1fc665a5..0c2c16db9 100644 --- a/sdk/python/flet/grid_view.py +++ b/sdk/python/flet/grid_view.py @@ -149,10 +149,6 @@ def _before_build_command(self): def _get_children(self): return self.__controls - def clean(self): - Control.clean(self) - self.__controls.clear() - # horizontal @property def horizontal(self) -> Optional[bool]: diff --git a/sdk/python/flet/list_view.py b/sdk/python/flet/list_view.py index 17c8d2ff1..ea2c75b99 100644 --- a/sdk/python/flet/list_view.py +++ b/sdk/python/flet/list_view.py @@ -141,10 +141,6 @@ def _before_build_command(self): def _get_children(self): return self.__controls - def clean(self): - Control.clean(self) - self.__controls.clear() - # horizontal @property def horizontal(self) -> Optional[bool]: diff --git a/sdk/python/flet/row.py b/sdk/python/flet/row.py index 00efa7600..0982bd8d6 100644 --- a/sdk/python/flet/row.py +++ b/sdk/python/flet/row.py @@ -143,10 +143,6 @@ def _get_control_name(self): def _get_children(self): return self.__controls - def clean(self): - Control.clean(self) - self.__controls.clear() - # tight @property def tight(self) -> Optional[bool]: From 85463126646dcf1cf52e8f272c20e413deebf7dc Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 15 Dec 2022 13:49:59 -0800 Subject: [PATCH 14/34] mount/unmount refactored --- sdk/python/flet/control.py | 34 ++--- sdk/python/flet/page.py | 154 +++++++++++------------ sdk/python/flet/responsive_row.py | 4 - sdk/python/tests/test_moving_children.py | 7 +- 4 files changed, 98 insertions(+), 101 deletions(-) diff --git a/sdk/python/flet/control.py b/sdk/python/flet/control.py index 192aa6d77..baba42ce4 100644 --- a/sdk/python/flet/control.py +++ b/sdk/python/flet/control.py @@ -266,16 +266,14 @@ def update(self): raise Exception("Control must be added to the page first.") self.__page.update(self) - def clean(self): - with self._lock: - self._previous_children.clear() - assert self.__page is not None - assert self.uid is not None - for child in self._get_children(): - self._remove_control_recursively(self.__page.index, child) - return self.__page._send_command("clean", [self.uid]) - - def build_update_commands(self, index, added_controls, commands, isolated=False): + # async def update_async(self): + # if not self.__page: + # raise Exception("Control must be added to the page first.") + # await self.__page.update_async(self) + + def build_update_commands( + self, index, commands, added_controls, removed_controls, isolated=False + ): update_cmd = self._build_command(update=True) if len(update_cmd.attrs) > 0: @@ -324,7 +322,9 @@ def build_update_commands(self, index, added_controls, commands, isolated=False) replaced = True break i += 1 - self._remove_control_recursively(index, ctrl) + removed_controls.extend( + self._remove_control_recursively(index, ctrl) + ) if not replaced: ids.append(ctrl.__uid) if len(ids) > 0: @@ -351,7 +351,11 @@ def build_update_commands(self, index, added_controls, commands, isolated=False) for h in previous_ints[a1:a2]: ctrl = hashes[h] ctrl.build_update_commands( - index, added_controls, commands, isolated=ctrl._is_isolated() + index, + commands, + added_controls, + removed_controls, + isolated=ctrl._is_isolated(), ) n += 1 elif tag == "insert": @@ -376,13 +380,15 @@ def build_update_commands(self, index, added_controls, commands, isolated=False) self.__previous_children.extend(current_children) def _remove_control_recursively(self, index, control): + removed_controls = [control] for child in control._get_children(): - self._remove_control_recursively(index, child) + removed_controls.extend(self._remove_control_recursively(index, child)) if control.__uid in index: - control.will_unmount() del index[control.__uid] + return removed_controls + # private methods def _build_add_commands(self, indent=0, index=None, added_controls=None): diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index f43e22732..b2d67dc24 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -4,7 +4,7 @@ import time import uuid from dataclasses import dataclass -from typing import Any, cast +from typing import Any, Tuple, cast from urllib.parse import urlparse from beartype import beartype @@ -82,8 +82,6 @@ def __init__(self, conn: Connection, session_id): self.__query = QueryString(page=self) # Querystring self._session_id = session_id self._index = {self._Control__uid: self} # index with all page controls - self._last_event = None - self._event_available = threading.Event() self.__views = [View()] self.__default_view = self.__views[0] @@ -149,24 +147,13 @@ def convert_keyboard_event(e): self.__on_error = EventHandler() self._add_event_handler("error", self.__on_error.handler) - def __enter__(self): - return self - - def __exit__(self, type, value, traceback): - self.close() - def get_control(self, id): return self._index.get(id) def _before_build_command(self): super()._before_build_command() - # fonts self._set_attr_json("fonts", self.__fonts) - - # light theme self._set_attr_json("theme", self.__theme) - - # dark theme self._set_attr_json("darkTheme", self.__dark_theme) # keyboard event @@ -189,10 +176,12 @@ def fetch_page_details(self): async def fetch_page_details_async(self): assert self.__conn.page_name is not None - values = (await self.__conn.send_commands_async( - self._session_id, - self.__get_page_detail_commands(), - )).results + values = ( + await self.__conn.send_commands_async( + self._session_id, + self.__get_page_detail_commands(), + ) + ).results self.__set_page_details(values) def __get_page_detail_commands(self): @@ -212,7 +201,7 @@ def __get_page_detail_commands(self): Command(0, "get", ["page", "windowTop"]), Command(0, "get", ["page", "windowLeft"]), ] - + def __set_page_details(self, values): self._set_attr("route", values[0], False) self._set_attr("pwa", values[1], False) @@ -226,25 +215,60 @@ def __set_page_details(self, values): self._set_attr("windowLeft", values[9], False) def update(self, *controls): - added_controls = [] with self._lock: if len(controls) == 0: - added_controls = self.__update(self) + r = self.__update(self) else: - added_controls = self.__update(*controls) - for ctrl in added_controls: - ctrl.did_mount() + r = self.__update(*controls) + self.__handle_mount_unmount(*r) + + def add(self, *controls): + with self._lock: + self._controls.extend(controls) + r = self.__update(self) + self.__handle_mount_unmount(*r) - def __update(self, *controls) -> List[Control]: + def insert(self, at, *controls): + with self._lock: + n = at + for control in controls: + self._controls.insert(n, control) + n += 1 + r = self.__update(self) + self.__handle_mount_unmount(*r) + + def remove(self, *controls): + with self._lock: + for control in controls: + self._controls.remove(control) + r = self.__update(self) + self.__handle_mount_unmount(*r) + + def remove_at(self, index): + with self._lock: + self._controls.pop(index) + r = self.__update(self) + self.__handle_mount_unmount(*r) + + def clean(self): + with self._lock: + self._controls.clear() + r = self.__update(self) + self.__handle_mount_unmount(*r) + + def __update(self, *controls) -> Tuple[List[Control], List[Control]]: added_controls = [] + removed_controls = [] commands = [] # build commands for control in controls: - control.build_update_commands(self._index, added_controls, commands) + control.build_update_commands( + self._index, commands, added_controls, removed_controls + ) if len(commands) == 0: - return added_controls + return added_controls, removed_controls # execute commands results = self.__conn.send_commands(self._session_id, commands).results @@ -260,47 +284,14 @@ def __update(self, *controls) -> List[Control]: self._index[id] = added_controls[n] n += 1 - return added_controls + return added_controls, removed_controls - def add(self, *controls): - added_controls = [] - with self._lock: - self._controls.extend(controls) - added_controls = self.__update(self) + def __handle_mount_unmount(self, added_controls, removed_controls): + for ctrl in removed_controls: + ctrl.will_unmount() for ctrl in added_controls: ctrl.did_mount() - def insert(self, at, *controls): - added_controls = [] - with self._lock: - n = at - for control in controls: - self._controls.insert(n, control) - n += 1 - added_controls = self.__update(self) - for ctrl in added_controls: - ctrl.did_mount() - - def remove(self, *controls): - with self._lock: - for control in controls: - self._controls.remove(control) - self.__update(self) - - def remove_at(self, index): - with self._lock: - self._controls.pop(index) - self.__update(self) - - def clean(self): - with self._lock: - self._previous_children.clear() - for child in self._get_children(): - self._remove_control_recursively(self._index, child) - self._controls.clear() - assert self.uid is not None - return self._send_command("clean", [self.uid]) - def error(self, message=""): with self._lock: self._send_command("error", [message]) @@ -320,22 +311,11 @@ def on_event(self, e: Event): ) elif e.target in self._index: - self._last_event = ControlEvent( - e.target, e.name, e.data, self._index[e.target], self - ) + ce = ControlEvent(e.target, e.name, e.data, self._index[e.target], self) handler = self._index[e.target].event_handlers.get(e.name) if handler: - t = threading.Thread( - target=handler, args=(self._last_event,), daemon=True - ) + t = threading.Thread(target=handler, args=(ce,), daemon=True) t.start() - self._event_available.set() - - def wait_event(self) -> ControlEvent: - self._event_available.clear() - self._event_available.wait() - assert self._last_event is not None - return self._last_event def go(self, route, **kwargs): self.route = route if kwargs == {} else route + self.query.post(kwargs) @@ -435,10 +415,6 @@ def logout(self): ControlEvent(target="page", name="logout", data="", control=self, page=self) ) - def close(self): - if self._session_id == constants.ZERO_SESSION: - self.__conn.close() - def _send_command( self, name: str, @@ -455,6 +431,22 @@ def _send_command( ), ) + async def _send_command_async( + self, + name: str, + values: Optional[List[str]] = None, + attrs: Optional[Dict[str, str]] = None, + ): + return await self.__conn.send_command_async( + self._session_id, + Command( + indent=0, + name=name, + values=values if values is not None else [], + attrs=attrs or {}, + ), + ) + @beartype def set_clipboard(self, value: str): self.__offstage.clipboard.set_data(value) diff --git a/sdk/python/flet/responsive_row.py b/sdk/python/flet/responsive_row.py index fc21c2432..ae3bd8c79 100644 --- a/sdk/python/flet/responsive_row.py +++ b/sdk/python/flet/responsive_row.py @@ -134,10 +134,6 @@ def _before_build_command(self): def _get_children(self): return self.__controls - def clean(self): - Control.clean(self) - self.__controls.clear() - # horizontal_alignment @property def alignment(self) -> MainAxisAlignment: diff --git a/sdk/python/tests/test_moving_children.py b/sdk/python/tests/test_moving_children.py index a4be138c6..7132580ef 100644 --- a/sdk/python/tests/test_moving_children.py +++ b/sdk/python/tests/test_moving_children.py @@ -12,8 +12,9 @@ def test_moving_children(): index = [] added_controls = [] + removed_controls = [] commands = [] - c.build_update_commands(index, added_controls, commands, False) + c.build_update_commands(index, commands, added_controls, removed_controls, False) def replace_controls(c): random.shuffle(c.controls) @@ -24,7 +25,9 @@ def replace_controls(c): for ctrl in c.controls: # print(ctrl._Control__uid) r.add(ctrl._Control__uid) - c.build_update_commands(index, added_controls, commands, False) + c.build_update_commands( + index, commands, added_controls, removed_controls, False + ) for cmd in commands: if cmd.name == "add": for sub_cmd in cmd.commands: From 11ab4e56e1c7ea0869a83b83f0bcbd801fa61a20 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 15 Dec 2022 14:06:03 -0800 Subject: [PATCH 15/34] Async Page methods --- sdk/python/flet/flet.py | 4 ++- sdk/python/flet/page.py | 73 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index 59ca23483..6818bf2c6 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -337,7 +337,9 @@ async def on_session_created(session_data): f"Unhandled error processing page session {page.session_id}:", traceback.format_exc(), ) - page.error(f"There was an error while processing your request: {e}") + await page.error_async( + f"There was an error while processing your request: {e}" + ) conn = AsyncConnection( server_address=server, diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index b2d67dc24..6cd85e7bd 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -222,12 +222,26 @@ def update(self, *controls): r = self.__update(*controls) self.__handle_mount_unmount(*r) + async def update_async(self, *controls): + async with self._async_lock: + if len(controls) == 0: + r = await self.__update_async(self) + else: + r = await self.__update_async(*controls) + await self.__handle_mount_unmount_async(*r) + def add(self, *controls): with self._lock: self._controls.extend(controls) r = self.__update(self) self.__handle_mount_unmount(*r) + async def add_async(self, *controls): + async with self._async_lock: + self._controls.extend(controls) + r = await self.__update_async(self) + await self.__handle_mount_unmount_async(*r) + def insert(self, at, *controls): with self._lock: n = at @@ -237,6 +251,15 @@ def insert(self, at, *controls): r = self.__update(self) self.__handle_mount_unmount(*r) + async def insert_async(self, at, *controls): + async with self._async_lock: + n = at + for control in controls: + self._controls.insert(n, control) + n += 1 + r = await self.__update_async(self) + await self.__handle_mount_unmount_async(*r) + def remove(self, *controls): with self._lock: for control in controls: @@ -244,19 +267,52 @@ def remove(self, *controls): r = self.__update(self) self.__handle_mount_unmount(*r) + async def remove_async(self, *controls): + async with self._async_lock: + for control in controls: + self._controls.remove(control) + r = await self.__update_async(self) + await self.__handle_mount_unmount_async(*r) + def remove_at(self, index): with self._lock: self._controls.pop(index) r = self.__update(self) self.__handle_mount_unmount(*r) + async def remove_at_async(self, index): + async with self._async_lock: + self._controls.pop(index) + r = await self.__update_async(self) + await self.__handle_mount_unmount_async(*r) + def clean(self): with self._lock: self._controls.clear() r = self.__update(self) self.__handle_mount_unmount(*r) + async def clean_async(self): + async with self._async_lock: + self._controls.clear() + r = await self.__update_async(self) + await self.__handle_mount_unmount_async(*r) + def __update(self, *controls) -> Tuple[List[Control], List[Control]]: + commands, added_controls, removed_controls = self.__prepare_update(*controls) + results = self.__conn.send_commands(self._session_id, commands).results + self.__update_control_ids(added_controls, results) + return added_controls, removed_controls + + async def __update_async(self, *controls) -> Tuple[List[Control], List[Control]]: + commands, added_controls, removed_controls = self.__prepare_update(*controls) + results = ( + await self.__conn.send_commands_async(self._session_id, commands) + ).results + self.__update_control_ids(added_controls, results) + return added_controls, removed_controls + + def __prepare_update(self, *controls): added_controls = [] removed_controls = [] commands = [] @@ -268,11 +324,11 @@ def __update(self, *controls) -> Tuple[List[Control], List[Control]]: ) if len(commands) == 0: - return added_controls, removed_controls + return commands, added_controls, removed_controls - # execute commands - results = self.__conn.send_commands(self._session_id, commands).results + return commands, added_controls, removed_controls + def __update_control_ids(self, added_controls, results): if len(results) > 0: n = 0 for line in results: @@ -284,7 +340,6 @@ def __update(self, *controls) -> Tuple[List[Control], List[Control]]: self._index[id] = added_controls[n] n += 1 - return added_controls, removed_controls def __handle_mount_unmount(self, added_controls, removed_controls): for ctrl in removed_controls: @@ -292,10 +347,20 @@ def __handle_mount_unmount(self, added_controls, removed_controls): for ctrl in added_controls: ctrl.did_mount() + async def __handle_mount_unmount_async(self, added_controls, removed_controls): + for ctrl in removed_controls: + await ctrl.will_unmount_async() + for ctrl in added_controls: + await ctrl.did_mount_async() + def error(self, message=""): with self._lock: self._send_command("error", [message]) + async def error_async(self, message=""): + async with self._async_lock: + await self._send_command_async("error", [message]) + def on_event(self, e: Event): logging.info(f"page.on_event: {e.target} {e.name} {e.data}") From 6240f847281a73470d538573401a8d3d4abf9c14 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 15 Dec 2022 14:40:41 -0800 Subject: [PATCH 16/34] Run CLI to work with a new flet view method --- sdk/python/flet/cli/commands/run.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/sdk/python/flet/cli/commands/run.py b/sdk/python/flet/cli/commands/run.py index 7a81c43a3..944e2f425 100644 --- a/sdk/python/flet/cli/commands/run.py +++ b/sdk/python/flet/cli/commands/run.py @@ -8,7 +8,7 @@ import threading import time from flet.cli.commands.base import BaseCommand -from flet.flet import open_flet_view +from flet.flet import close_flet_view, open_flet_view from flet.utils import get_free_tcp_port, is_windows, open_in_browser from watchdog.events import FileSystemEventHandler from watchdog.observers import Observer @@ -97,12 +97,7 @@ def handle(self, options: argparse.Namespace) -> None: except KeyboardInterrupt: pass - if my_event_handler.fvp is not None and not is_windows(): - try: - logging.debug(f"Flet View process {my_event_handler.fvp.pid}") - os.kill(my_event_handler.fvp.pid + 1, signal.SIGKILL) - except: - pass + close_flet_view(my_event_handler.pid_file) my_observer.stop() my_observer.join() @@ -118,6 +113,7 @@ def __init__(self, args, script_path, port, web, hidden) -> None: self.last_time = time.time() self.is_running = False self.fvp = None + self.pid_file = None self.page_url_prefix = f"PAGE_URL_{time.time()}" self.page_url = None self.terminate = threading.Event() @@ -165,7 +161,7 @@ def print_output(self, p): print(line) def open_flet_view_and_wait(self): - self.fvp = open_flet_view(self.page_url, self.hidden) + self.fvp, self.pid_file = open_flet_view(self.page_url, self.hidden) self.fvp.wait() self.p.kill() self.terminate.set() From 29248efda6ad48a52e492a7440950ba183f688c9 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 16 Dec 2022 11:35:31 -0800 Subject: [PATCH 17/34] Async control events --- sdk/python/flet/container.py | 2 +- sdk/python/flet/datatable.py | 4 +- sdk/python/flet/drag_target.py | 5 ++- sdk/python/flet/event_handler.py | 25 ++++++++++- sdk/python/flet/file_picker.py | 7 ++-- sdk/python/flet/flet.py | 9 +++- sdk/python/flet/gesture_detector.py | 65 +++++++++++++++++------------ sdk/python/flet/page.py | 58 +++++++++++++++---------- sdk/python/flet/utils.py | 16 +++---- 9 files changed, 121 insertions(+), 70 deletions(-) diff --git a/sdk/python/flet/container.py b/sdk/python/flet/container.py index d3fb7d2db..20f1734ae 100644 --- a/sdk/python/flet/container.py +++ b/sdk/python/flet/container.py @@ -150,7 +150,7 @@ def convert_container_tap_event_data(e): return ContainerTapEvent(**d) self.__on_click = EventHandler(convert_container_tap_event_data) - self._add_event_handler("click", self.__on_click.handler) + self._add_event_handler("click", self.__on_click.get_handler()) self.content = content self.padding = padding diff --git a/sdk/python/flet/datatable.py b/sdk/python/flet/datatable.py index 98648f67f..36d9e113e 100644 --- a/sdk/python/flet/datatable.py +++ b/sdk/python/flet/datatable.py @@ -44,7 +44,7 @@ def __init__( self.__on_sort = EventHandler( lambda e: DataColumnSortEvent(**json.loads(e.data)) ) - self._add_event_handler("sort", self.__on_sort.handler) + self._add_event_handler("sort", self.__on_sort.get_handler()) self.label = label self.numeric = numeric @@ -116,7 +116,7 @@ def __init__( Control.__init__(self, ref=ref) self.__on_tap_down = EventHandler(lambda e: TapEvent(**json.loads(e.data))) - self._add_event_handler("tap_down", self.__on_tap_down.handler) + self._add_event_handler("tap_down", self.__on_tap_down.get_handler()) self.content = content self.on_double_tap = on_double_tap diff --git a/sdk/python/flet/drag_target.py b/sdk/python/flet/drag_target.py index 2dc9abecb..79c18b4e1 100644 --- a/sdk/python/flet/drag_target.py +++ b/sdk/python/flet/drag_target.py @@ -14,7 +14,7 @@ class DragTarget(Control): A control that completes drag operation when a `Draggable` widget is dropped. When a draggable is dragged on top of a drag target, the drag target is asked whether it will accept the data the draggable is carrying. The drag target will accept incoming drag if it belongs to the same group as draggable. If the user does drop the draggable on top of the drag target (and the drag target has indicated that it will accept the draggable's data), then the drag target is asked to accept the draggable's data. - + Example: ``` import flet as ft @@ -102,6 +102,7 @@ def drag_leave(e): Online docs: https://flet.dev/docs/controls/dragtarget """ + def __init__( self, ref: Optional[Ref] = None, @@ -131,7 +132,7 @@ def convert_accept_event_data(e): return DragTargetAcceptEvent(**d) self.__on_accept = EventHandler(convert_accept_event_data) - self._add_event_handler("accept", self.__on_accept.handler) + self._add_event_handler("accept", self.__on_accept.get_handler()) self.__content: Optional[Control] = None diff --git a/sdk/python/flet/event_handler.py b/sdk/python/flet/event_handler.py index 769bb1c2d..1f0c37eb0 100644 --- a/sdk/python/flet/event_handler.py +++ b/sdk/python/flet/event_handler.py @@ -1,9 +1,18 @@ +from flet.utils import is_asyncio + + class EventHandler: def __init__(self, result_converter=None) -> None: self.__handlers = {} self.__result_converter = result_converter - def handler(self, e): + def get_handler(self): + if is_asyncio(): + return self.__async_handler + else: + return self.__sync_handler + + def __sync_handler(self, e): for h in self.__handlers.keys(): if self.__result_converter is not None: r = self.__result_converter(e) @@ -17,6 +26,20 @@ def handler(self, e): else: h(e) + async def __async_handler(self, e): + for h in self.__handlers.keys(): + if self.__result_converter is not None: + r = self.__result_converter(e) + if r is not None: + r.target = e.target + r.name = e.name + r.data = e.data + r.control = e.control + r.page = e.page + await h(r) + else: + await h(e) + def subscribe(self, handler): if handler is not None: self.__handlers[handler] = True diff --git a/sdk/python/flet/file_picker.py b/sdk/python/flet/file_picker.py index 63e903c5d..42c21db5d 100644 --- a/sdk/python/flet/file_picker.py +++ b/sdk/python/flet/file_picker.py @@ -65,7 +65,7 @@ class FilePickerUploadEvent(ControlEvent): class FilePicker(Control): """ A control that allows you to use the native file explorer to pick single or multiple files, with extensions filtering support and upload. - + Example: ``` import flet as ft @@ -104,6 +104,7 @@ def pick_files_result(e: ft.FilePickerResultEvent): Online docs: https://flet.dev/docs/controls/filepicker """ + def __init__( self, ref: Optional[Ref] = None, @@ -131,14 +132,14 @@ def convert_result_event_data(e): return self.__result self.__on_result = EventHandler(convert_result_event_data) - self._add_event_handler("result", self.__on_result.handler) + self._add_event_handler("result", self.__on_result.get_handler()) def convert_upload_event_data(e): d = json.loads(e.data) return FilePickerUploadEvent(**d) self.__on_upload = EventHandler(convert_upload_event_data) - self._add_event_handler("upload", self.__on_upload.handler) + self._add_event_handler("upload", self.__on_upload.get_handler()) self.__result: Optional[FilePickerResultEvent] = None self.__upload: List[FilePickerUploadFile] = [] diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index 6818bf2c6..b305f5e24 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -321,8 +321,13 @@ async def __connect_internal_async( ) async def on_event(e): - print("ON EVENT:", e) - pass + if e.sessionID in conn.sessions: + await conn.sessions[e.sessionID].on_event_async( + Event(e.eventTarget, e.eventName, e.eventData) + ) + if e.eventTarget == "page" and e.eventName == "close": + logging.info(f"Session closed: {e.sessionID}") + del conn.sessions[e.sessionID] async def on_session_created(session_data): page = Page(conn, session_data.sessionID) diff --git a/sdk/python/flet/gesture_detector.py b/sdk/python/flet/gesture_detector.py index 2fec4c8e5..9702c5691 100644 --- a/sdk/python/flet/gesture_detector.py +++ b/sdk/python/flet/gesture_detector.py @@ -64,7 +64,7 @@ class GestureDetector(ConstrainedControl): Attempts to recognize gestures that correspond to its non-null callbacks. If this control has a content, it defers to that child control for its sizing behavior. If it does not have a content, it grows to fit the parent instead. - + Example: ``` import flet as ft @@ -106,6 +106,7 @@ def on_pan_update2(e: ft.DragUpdateEvent): Online docs: https://flet.dev/docs/controls/gesturedetector """ + def __init__( self, content: Optional[Control] = None, @@ -201,55 +202,65 @@ def __init__( ) self.__on_tap_down = EventHandler(lambda e: TapEvent(**json.loads(e.data))) - self._add_event_handler("tap_down", self.__on_tap_down.handler) + self._add_event_handler("tap_down", self.__on_tap_down.get_handler()) self.__on_tap_up = EventHandler(lambda e: TapEvent(**json.loads(e.data))) - self._add_event_handler("tap_up", self.__on_tap_up.handler) + self._add_event_handler("tap_up", self.__on_tap_up.get_handler()) self.__on_multi_tap = EventHandler( lambda e: MultiTapEvent(e.data.lower() == "true") ) - self._add_event_handler("multi_tap", self.__on_multi_tap.handler) + self._add_event_handler("multi_tap", self.__on_multi_tap.get_handler()) self.__on_secondary_tap_down = EventHandler( lambda e: TapEvent(**json.loads(e.data)) ) self._add_event_handler( - "secondary_tap_down", self.__on_secondary_tap_down.handler + "secondary_tap_down", self.__on_secondary_tap_down.get_handler() ) self.__on_secondary_tap_up = EventHandler( lambda e: TapEvent(**json.loads(e.data)) ) - self._add_event_handler("secondary_tap_up", self.__on_secondary_tap_up.handler) + self._add_event_handler( + "secondary_tap_up", self.__on_secondary_tap_up.get_handler() + ) self.__on_long_press_start = EventHandler( lambda e: LongPressStartEvent(**json.loads(e.data)) ) - self._add_event_handler("long_press_start", self.__on_long_press_start.handler) + self._add_event_handler( + "long_press_start", self.__on_long_press_start.get_handler() + ) self.__on_long_press_end = EventHandler( lambda e: LongPressEndEvent(**json.loads(e.data)) ) - self._add_event_handler("long_press_end", self.__on_long_press_end.handler) + self._add_event_handler( + "long_press_end", self.__on_long_press_end.get_handler() + ) self.__on_secondary_long_press_start = EventHandler( lambda e: LongPressStartEvent(**json.loads(e.data)) ) self._add_event_handler( - "secondary_long_press_start", self.__on_secondary_long_press_start.handler + "secondary_long_press_start", + self.__on_secondary_long_press_start.get_handler(), ) self.__on_secondary_long_press_end = EventHandler( lambda e: LongPressEndEvent(**json.loads(e.data)) ) self._add_event_handler( - "secondary_long_press_end", self.__on_secondary_long_press_end.handler + "secondary_long_press_end", + self.__on_secondary_long_press_end.get_handler(), ) self.__on_double_tap_down = EventHandler( lambda e: TapEvent(**json.loads(e.data)) ) - self._add_event_handler("double_tap_down", self.__on_double_tap_down.handler) + self._add_event_handler( + "double_tap_down", self.__on_double_tap_down.get_handler() + ) # on_horizontal_drag @@ -257,19 +268,19 @@ def __init__( lambda e: DragStartEvent(**json.loads(e.data)) ) self._add_event_handler( - "horizontal_drag_start", self.__on_horizontal_drag_start.handler + "horizontal_drag_start", self.__on_horizontal_drag_start.get_handler() ) self.__on_horizontal_drag_update = EventHandler( lambda e: DragUpdateEvent(**json.loads(e.data)) ) self._add_event_handler( - "horizontal_drag_update", self.__on_horizontal_drag_update.handler + "horizontal_drag_update", self.__on_horizontal_drag_update.get_handler() ) self.__on_horizontal_drag_end = EventHandler( lambda e: DragEndEvent(**json.loads(e.data)) ) self._add_event_handler( - "horizontal_drag_end", self.__on_horizontal_drag_end.handler + "horizontal_drag_end", self.__on_horizontal_drag_end.get_handler() ) # on_vertical_drag @@ -278,19 +289,19 @@ def __init__( lambda e: DragStartEvent(**json.loads(e.data)) ) self._add_event_handler( - "vertical_drag_start", self.__on_vertical_drag_start.handler + "vertical_drag_start", self.__on_vertical_drag_start.get_handler() ) self.__on_vertical_drag_update = EventHandler( lambda e: DragUpdateEvent(**json.loads(e.data)) ) self._add_event_handler( - "vertical_drag_update", self.__on_vertical_drag_update.handler + "vertical_drag_update", self.__on_vertical_drag_update.get_handler() ) self.__on_vertical_drag_end = EventHandler( lambda e: DragEndEvent(**json.loads(e.data)) ) self._add_event_handler( - "vertical_drag_end", self.__on_vertical_drag_end.handler + "vertical_drag_end", self.__on_vertical_drag_end.get_handler() ) # on_pan @@ -298,41 +309,41 @@ def __init__( self.__on_pan_start = EventHandler( lambda e: DragStartEvent(**json.loads(e.data)) ) - self._add_event_handler("pan_start", self.__on_pan_start.handler) + self._add_event_handler("pan_start", self.__on_pan_start.get_handler()) self.__on_pan_update = EventHandler( lambda e: DragUpdateEvent(**json.loads(e.data)) ) - self._add_event_handler("pan_update", self.__on_pan_update.handler) + self._add_event_handler("pan_update", self.__on_pan_update.get_handler()) self.__on_pan_end = EventHandler(lambda e: DragEndEvent(**json.loads(e.data))) - self._add_event_handler("pan_end", self.__on_pan_end.handler) + self._add_event_handler("pan_end", self.__on_pan_end.get_handler()) # on_scale self.__on_scale_start = EventHandler( lambda e: ScaleStartEvent(**json.loads(e.data)) ) - self._add_event_handler("scale_start", self.__on_scale_start.handler) + self._add_event_handler("scale_start", self.__on_scale_start.get_handler()) self.__on_scale_update = EventHandler( lambda e: ScaleUpdateEvent(**json.loads(e.data)) ) - self._add_event_handler("scale_update", self.__on_scale_update.handler) + self._add_event_handler("scale_update", self.__on_scale_update.get_handler()) self.__on_scale_end = EventHandler( lambda e: ScaleEndEvent(**json.loads(e.data)) ) - self._add_event_handler("scale_end", self.__on_scale_end.handler) + self._add_event_handler("scale_end", self.__on_scale_end.get_handler()) # on_hover self.__on_hover = EventHandler(lambda e: HoverEvent(**json.loads(e.data))) - self._add_event_handler("hover", self.__on_hover.handler) + self._add_event_handler("hover", self.__on_hover.get_handler()) self.__on_enter = EventHandler(lambda e: HoverEvent(**json.loads(e.data))) - self._add_event_handler("enter", self.__on_enter.handler) + self._add_event_handler("enter", self.__on_enter.get_handler()) self.__on_exit = EventHandler(lambda e: HoverEvent(**json.loads(e.data))) - self._add_event_handler("exit", self.__on_exit.handler) + self._add_event_handler("exit", self.__on_exit.get_handler()) # on_scroll self.__on_scroll = EventHandler(lambda e: ScrollEvent(**json.loads(e.data))) - self._add_event_handler("scroll", self.__on_scroll.handler) + self._add_event_handler("scroll", self.__on_scroll.get_handler()) self.content = content self.mouse_cursor = mouse_cursor diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index 6cd85e7bd..8f6dae529 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -97,9 +97,9 @@ def __init__(self, conn: Connection, session_id): self.__authorization: Optional[Authorization] = None self.__on_close = EventHandler() - self._add_event_handler("close", self.__on_close.handler) + self._add_event_handler("close", self.__on_close.get_handler()) self.__on_resize = EventHandler() - self._add_event_handler("resize", self.__on_resize.handler) + self._add_event_handler("resize", self.__on_resize.get_handler()) self.__last_route = None @@ -117,20 +117,22 @@ def convert_route_change_event(e): return RouteChangeEvent(route=e.data) self.__on_route_change = EventHandler(convert_route_change_event) - self._add_event_handler("route_change", self.__on_route_change.handler) + self._add_event_handler("route_change", self.__on_route_change.get_handler()) def convert_view_pop_event(e): return ViewPopEvent(view=cast(View, self.get_control(e.data))) self.__on_view_pop = EventHandler(convert_view_pop_event) - self._add_event_handler("view_pop", self.__on_view_pop.handler) + self._add_event_handler("view_pop", self.__on_view_pop.get_handler()) def convert_keyboard_event(e): d = json.loads(e.data) return KeyboardEvent(**d) self.__on_keyboard_event = EventHandler(convert_keyboard_event) - self._add_event_handler("keyboard_event", self.__on_keyboard_event.handler) + self._add_event_handler( + "keyboard_event", self.__on_keyboard_event.get_handler() + ) self.__method_calls: Dict[str, threading.Event] = {} self.__method_call_results: Dict[ @@ -139,13 +141,13 @@ def convert_keyboard_event(e): self._add_event_handler("invoke_method_result", self._on_invoke_method_result) self.__on_window_event = EventHandler() - self._add_event_handler("window_event", self.__on_window_event.handler) + self._add_event_handler("window_event", self.__on_window_event.get_handler()) self.__on_connect = EventHandler() - self._add_event_handler("connect", self.__on_connect.handler) + self._add_event_handler("connect", self.__on_connect.get_handler()) self.__on_disconnect = EventHandler() - self._add_event_handler("disconnect", self.__on_disconnect.handler) + self._add_event_handler("disconnect", self.__on_disconnect.get_handler()) self.__on_error = EventHandler() - self._add_event_handler("error", self.__on_error.handler) + self._add_event_handler("error", self.__on_error.get_handler()) def get_control(self, id): return self._index.get(id) @@ -366,14 +368,7 @@ def on_event(self, e: Event): with self._lock: if e.target == "page" and e.name == "change": - for props in json.loads(e.data): - id = props["i"] - if id in self._index: - for name in props: - if name != "i": - self._index[id]._set_attr( - name, props[name], dirty=False - ) + self.__on_page_change_event(e.data) elif e.target in self._index: ce = ControlEvent(e.target, e.name, e.data, self._index[e.target], self) @@ -382,10 +377,31 @@ def on_event(self, e: Event): t = threading.Thread(target=handler, args=(ce,), daemon=True) t.start() + async def on_event_async(self, e: Event): + logging.info(f"page.on_event_async: {e.target} {e.name} {e.data}") + + if e.target == "page" and e.name == "change": + async with self._async_lock: + self.__on_page_change_event(e.data) + + elif e.target in self._index: + ce = ControlEvent(e.target, e.name, e.data, self._index[e.target], self) + handler = self._index[e.target].event_handlers.get(e.name) + if handler: + await handler(ce) + + def __on_page_change_event(self, data): + for props in json.loads(data): + id = props["i"] + if id in self._index: + for name in props: + if name != "i": + self._index[id]._set_attr(name, props[name], dirty=False) + def go(self, route, **kwargs): self.route = route if kwargs == {} else route + self.query.post(kwargs) - self.__on_route_change.handler( + self.__on_route_change.get_handler()( ControlEvent( target="page", name="route_change", @@ -444,7 +460,7 @@ def login( authorization_url, "flet_oauth_signin", web_popup_window=self.web ) else: - self.__on_login.handler(LoginEvent(error="", error_description="")) + self.__on_login.get_handler()(LoginEvent(error="", error_description="")) return self.__authorization def __on_authorize(self, e): @@ -472,11 +488,11 @@ def __on_authorize(self, e): self.__authorization.request_token(code) except Exception as ex: login_evt.error = str(ex) - self.__on_login.handler(login_evt) + self.__on_login.get_handler()(login_evt) def logout(self): self.__authorization = None - self.__on_logout.handler( + self.__on_logout.get_handler()( ControlEvent(target="page", name="logout", data="", control=self, page=self) ) diff --git a/sdk/python/flet/utils.py b/sdk/python/flet/utils.py index cd481fb95..cdb2ed428 100644 --- a/sdk/python/flet/utils.py +++ b/sdk/python/flet/utils.py @@ -1,3 +1,4 @@ +import asyncio import inspect import math import os @@ -66,18 +67,11 @@ def random_string(length): return "".join(random.choice(string.ascii_letters) for i in range(length)) -def is_sync_method(obj, method_name) -> bool: - method = getattr(obj, method_name, None) - if method is None: +def is_asyncio(): + try: + return asyncio.current_task() is not None + except RuntimeError: return False - return callable(method) and not inspect.iscoroutinefunction(method) - - -def is_async_method(obj, method_name) -> bool: - method = getattr(obj, method_name, None) - if method is None: - return False - return callable(method) and inspect.iscoroutinefunction(method) # https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python From 6d5968298daa504a3d4b68e67065a0629bd65b5a Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 16 Dec 2022 12:18:52 -0800 Subject: [PATCH 18/34] httpx added as dependency --- sdk/python/flet/auth/oauth_provider.py | 6 ++ sdk/python/flet/control.py | 8 +-- sdk/python/flet/page.py | 22 +++++++ sdk/python/pdm.lock | 91 +++++++++++++++++++++++++- sdk/python/pyproject.toml | 1 + 5 files changed, 123 insertions(+), 5 deletions(-) diff --git a/sdk/python/flet/auth/oauth_provider.py b/sdk/python/flet/auth/oauth_provider.py index 862f0f49d..8eb029b3e 100644 --- a/sdk/python/flet/auth/oauth_provider.py +++ b/sdk/python/flet/auth/oauth_provider.py @@ -35,5 +35,11 @@ def _name(self): def _fetch_groups(self, access_token: str) -> List[Group]: return [] + async def _fetch_groups_async(self, access_token: str) -> List[Group]: + return [] + def _fetch_user(self, access_token: str) -> Optional[User]: return None + + async def _fetch_user_async(self, access_token: str) -> Optional[User]: + return None diff --git a/sdk/python/flet/control.py b/sdk/python/flet/control.py index baba42ce4..d90b54f98 100644 --- a/sdk/python/flet/control.py +++ b/sdk/python/flet/control.py @@ -266,10 +266,10 @@ def update(self): raise Exception("Control must be added to the page first.") self.__page.update(self) - # async def update_async(self): - # if not self.__page: - # raise Exception("Control must be added to the page first.") - # await self.__page.update_async(self) + async def update_async(self): + if not self.__page: + raise Exception("Control must be added to the page first.") + await self.__page.update_async(self) def build_update_commands( self, index, commands, added_controls, removed_controls, isolated=False diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index 8f6dae529..c93735c92 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -413,13 +413,35 @@ def go(self, route, **kwargs): self.update() self.query() # Update query url (required when using go) + async def go_async(self, route, **kwargs): + self.route = route if kwargs == {} else route + self.query.post(kwargs) + + await self.__on_route_change.get_handler()( + ControlEvent( + target="page", + name="route_change", + data=self.route, + page=self, + control=self, + ) + ) + await self.update_async() + self.query() + def get_upload_url(self, file_name: str, expires: int): r = self._send_command( "getUploadUrl", attrs={"file": file_name, "expires": str(expires)} ) if r.error: raise Exception(r.error) + return r.result + async def get_upload_url_async(self, file_name: str, expires: int): + r = await self._send_command_async( + "getUploadUrl", attrs={"file": file_name, "expires": str(expires)} + ) + if r.error: + raise Exception(r.error) return r.result def login( diff --git a/sdk/python/pdm.lock b/sdk/python/pdm.lock index 902b43174..48252ae6b 100644 --- a/sdk/python/pdm.lock +++ b/sdk/python/pdm.lock @@ -1,3 +1,14 @@ +[[package]] +name = "anyio" +version = "3.6.2" +requires_python = ">=3.6.2" +summary = "High level compatibility layer for multiple asynchronous event loop implementations" +dependencies = [ + "idna>=2.8", + "sniffio>=1.1", + "typing-extensions; python_version < \"3.8\"", +] + [[package]] name = "attrs" version = "22.1.0" @@ -51,6 +62,39 @@ version = "3.8.0" requires_python = ">=3.7" summary = "A platform independent file lock." +[[package]] +name = "h11" +version = "0.14.0" +requires_python = ">=3.7" +summary = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +dependencies = [ + "typing-extensions; python_version < \"3.8\"", +] + +[[package]] +name = "httpcore" +version = "0.16.2" +requires_python = ">=3.7" +summary = "A minimal low-level HTTP client." +dependencies = [ + "anyio<5.0,>=3.0", + "certifi", + "h11<0.15,>=0.13", + "sniffio==1.*", +] + +[[package]] +name = "httpx" +version = "0.23.1" +requires_python = ">=3.7" +summary = "The next generation HTTP client." +dependencies = [ + "certifi", + "httpcore<0.17.0,>=0.15.0", + "rfc3986[idna2008]<2,>=1.3", + "sniffio", +] + [[package]] name = "identify" version = "2.5.8" @@ -180,6 +224,21 @@ dependencies = [ "urllib3<1.27,>=1.21.1", ] +[[package]] +name = "rfc3986" +version = "1.5.0" +summary = "Validating URI References per RFC 3986" + +[[package]] +name = "rfc3986" +version = "1.5.0" +extras = ["idna2008"] +summary = "Validating URI References per RFC 3986" +dependencies = [ + "idna", + "rfc3986==1.5.0", +] + [[package]] name = "setuptools" version = "65.5.0" @@ -192,6 +251,12 @@ version = "1.16.0" requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" summary = "Python 2 and 3 compatibility utilities" +[[package]] +name = "sniffio" +version = "1.3.0" +requires_python = ">=3.7" +summary = "Sniff out which async library your code is running under" + [[package]] name = "toml" version = "0.10.2" @@ -254,9 +319,13 @@ summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "4.0" -content_hash = "sha256:9d9f6efa10cb304a58a2c308465e75a9984ec6d5f18a24de1cc6e5adac42a2b9" +content_hash = "sha256:611c61f47909f140b2089160638961560fc72b1a8e73c1a9b081e5f6a92048c9" [metadata.files] +"anyio 3.6.2" = [ + {url = "https://files.pythonhosted.org/packages/77/2b/b4c0b7a3f3d61adb1a1e0b78f90a94e2b6162a043880704b7437ef297cad/anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {url = "https://files.pythonhosted.org/packages/8b/94/6928d4345f2bc1beecbff03325cad43d320717f51ab74ab5a571324f4f5a/anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] "attrs 22.1.0" = [ {url = "https://files.pythonhosted.org/packages/1a/cb/c4ffeb41e7137b23755a45e1bfec9cbb76ecf51874c6f1d113984ecaa32c/attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, {url = "https://files.pythonhosted.org/packages/f2/bc/d817287d1aa01878af07c19505fafd1165cd6a119e9d0821ca1d1c20312d/attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, @@ -293,6 +362,18 @@ content_hash = "sha256:9d9f6efa10cb304a58a2c308465e75a9984ec6d5f18a24de1cc6e5ada {url = "https://files.pythonhosted.org/packages/94/b3/ff2845971788613e646e667043fdb5f128e2e540aefa09a3c55be8290d6d/filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, {url = "https://files.pythonhosted.org/packages/95/55/b897882bffb8213456363e646bf9e9fa704ffda5a7d140edf935a9e02c7b/filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, ] +"h11 0.14.0" = [ + {url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] +"httpcore 0.16.2" = [ + {url = "https://files.pythonhosted.org/packages/91/52/93f22e5441539256c0d113faf17e45284aee16eebdd95089e3ca6f480b18/httpcore-0.16.2-py3-none-any.whl", hash = "sha256:52c79095197178856724541e845f2db86d5f1527640d9254b5b8f6f6cebfdee6"}, + {url = "https://files.pythonhosted.org/packages/9b/20/26f6cc4fd00391f8f1c57b0020f5c6eec23904723db04b6f7608e222d815/httpcore-0.16.2.tar.gz", hash = "sha256:c35c5176dc82db732acfd90b581a3062c999a72305df30c0fc8fafd8e4aca068"}, +] +"httpx 0.23.1" = [ + {url = "https://files.pythonhosted.org/packages/8a/df/a3e8b91dfb452e645ef110985a30f0915276a1a2144004c7671c07bb203c/httpx-0.23.1.tar.gz", hash = "sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19"}, + {url = "https://files.pythonhosted.org/packages/e1/74/cdce73069e021ad5913451b86c2707b027975cf302016ca557686d87eb41/httpx-0.23.1-py3-none-any.whl", hash = "sha256:0b9b1f0ee18b9978d637b0776bfd7f54e2ca278e063e3586d8f01cda89e042a8"}, +] "identify 2.5.8" = [ {url = "https://files.pythonhosted.org/packages/1b/66/c826b47a47d805f09092b8f5a4dd241aebc50bb3b190b9e2e3d44e3f6f55/identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"}, {url = "https://files.pythonhosted.org/packages/67/e1/869d7b8df41a3ac2a3c74a2a4ba401df468044dccc489b8937aad40d148e/identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"}, @@ -391,6 +472,10 @@ content_hash = "sha256:9d9f6efa10cb304a58a2c308465e75a9984ec6d5f18a24de1cc6e5ada {url = "https://files.pythonhosted.org/packages/a5/61/a867851fd5ab77277495a8709ddda0861b28163c4613b011bc00228cc724/requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, {url = "https://files.pythonhosted.org/packages/ca/91/6d9b8ccacd0412c08820f72cebaa4f0c0441b5cda699c90f618b6f8a1b42/requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, ] +"rfc3986 1.5.0" = [ + {url = "https://files.pythonhosted.org/packages/79/30/5b1b6c28c105629cc12b33bdcbb0b11b5bb1880c6cfbd955f9e792921aa8/rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, + {url = "https://files.pythonhosted.org/packages/c4/e5/63ca2c4edf4e00657584608bee1001302bbf8c5f569340b78304f2f446cb/rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, +] "setuptools 65.5.0" = [ {url = "https://files.pythonhosted.org/packages/41/82/7f54bbfe5c247a8c9f78d8d1d7c051847bcb78843c397b866dba335c1e88/setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, {url = "https://files.pythonhosted.org/packages/c5/41/247814d8b7a044717164c74080725a6c8f3d2b5fc82b34bd825b617df663/setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, @@ -399,6 +484,10 @@ content_hash = "sha256:9d9f6efa10cb304a58a2c308465e75a9984ec6d5f18a24de1cc6e5ada {url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, {url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, ] +"sniffio 1.3.0" = [ + {url = "https://files.pythonhosted.org/packages/c3/a0/5dba8ed157b0136607c7f2151db695885606968d1fae123dc3391e0cfdbf/sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {url = "https://files.pythonhosted.org/packages/cd/50/d49c388cae4ec10e8109b1b833fd265511840706808576df3ada99ecb0ac/sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] "toml 0.10.2" = [ {url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 5c39f6b03..de323a860 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "requests>=2.28.1", "oauthlib>=3.2.0", "websockets>=10.4", + "httpx>=0.23.1", ] requires-python = ">=3.7" license = { text = "MIT" } From 9418f3da172e3650d0ec21c014c4bbabb8544432 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Sun, 18 Dec 2022 08:10:29 -0800 Subject: [PATCH 19/34] Update github_oauth_provider.py --- .../auth/providers/github_oauth_provider.py | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/sdk/python/flet/auth/providers/github_oauth_provider.py b/sdk/python/flet/auth/providers/github_oauth_provider.py index 4a9842293..00d4af98b 100644 --- a/sdk/python/flet/auth/providers/github_oauth_provider.py +++ b/sdk/python/flet/auth/providers/github_oauth_provider.py @@ -1,7 +1,7 @@ import json from typing import List, Optional -import requests +import httpx from flet.auth.group import Group from flet.auth.oauth_provider import OAuthProvider @@ -21,9 +21,24 @@ def __init__(self, client_id: str, client_secret: str, redirect_url: str) -> Non ) def _fetch_groups(self, access_token: str) -> List[Group]: - headers = {"Authorization": "Bearer {}".format(access_token)} + with httpx.Client() as client: + teams_resp = client.send(self.__get_user_teams_request(access_token)) + return self.__complete_fetch_groups(teams_resp) + + async def _fetch_groups_sync(self, access_token: str) -> List[Group]: + async with httpx.AsyncClient() as client: + teams_resp = await client.send(self.__get_user_teams_request(access_token)) + return self.__complete_fetch_groups(teams_resp) + + def __get_user_teams_request(self, access_token): + return httpx.Request( + "GET", + "https://api.github.com/user/teams", + headers={"Authorization": "Bearer {}".format(access_token)}, + ) + + def __complete_fetch_groups(self, teams_resp): groups = [] - teams_resp = requests.get("https://api.github.com/user/teams", headers=headers) tj = json.loads(teams_resp.text) for t in tj: groups.append( @@ -35,13 +50,22 @@ def _fetch_groups(self, access_token: str) -> List[Group]: return groups def _fetch_user(self, access_token: str) -> Optional[User]: - headers = {"Authorization": "Bearer {}".format(access_token)} - user_resp = requests.get("https://api.github.com/user", headers=headers) + user_req, emails_req = self.__get_user_details_requests(access_token) + with httpx.Client() as client: + user_resp = client.send(user_req) + emails_resp = client.send(emails_req) + uj = json.loads(user_resp.text) - email_resp = requests.get("https://api.github.com/user/emails", headers=headers) - ej = json.loads(email_resp.text) + ej = json.loads(emails_resp.text) for e in ej: if e["primary"]: uj["email"] = e["email"] break return User(uj, id=str(uj["id"])) + + def __get_user_details_requests(self, access_token): + headers = {"Authorization": "Bearer {}".format(access_token)} + return ( + httpx.Request("GET", "https://api.github.com/user", headers=headers), + httpx.Request("GET", "https://api.github.com/user/emails", headers=headers), + ) From 473368ffcd7a797f2e00dd2e939029d88799821f Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 4 Jan 2023 11:38:12 -0800 Subject: [PATCH 20/34] Fix flet.py --- sdk/python/flet/flet.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index d81660d0b..ca69b5bee 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -516,7 +516,7 @@ def __locate_and_unpack_flet_view(page_url, hidden): flet_path = which("flet", sys.argv[0]) if flet_path and "/Contents/MacOS/" in flet_path: logging.info(f"Flet.app found in PATH: {flet_path}") - app_path = str(Path(flet_path).parent.parent.parent) + temp_flet_dir = Path(flet_path).parent.parent.parent else: # check if flet_view.app exists in a temp directory if not temp_flet_dir.exists(): @@ -526,12 +526,12 @@ def __locate_and_unpack_flet_view(page_url, hidden): if not tar_file.exists(): tar_file = __download_flet_client(gz_filename) - logging.info(f"Extracting Flet.app from archive to {temp_flet_dir}") - temp_flet_dir.mkdir(parents=True, exist_ok=True) - with tarfile.open(str(tar_file), "r:gz") as tar_arch: - safe_tar_extractall(tar_arch, str(temp_flet_dir)) - else: - logging.info(f"Flet View found in: {temp_flet_dir}") + logging.info(f"Extracting Flet.app from archive to {temp_flet_dir}") + temp_flet_dir.mkdir(parents=True, exist_ok=True) + with tarfile.open(str(tar_file), "r:gz") as tar_arch: + safe_tar_extractall(tar_arch, str(temp_flet_dir)) + else: + logging.info(f"Flet View found in: {temp_flet_dir}") app_name = None for f in os.listdir(temp_flet_dir): From 8922cecc2bfabb109d3aec15e248a4326ee9eb1d Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 4 Jan 2023 11:39:10 -0800 Subject: [PATCH 21/34] Update pdm.lock --- sdk/python/pdm.lock | 89 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 88 insertions(+), 1 deletion(-) diff --git a/sdk/python/pdm.lock b/sdk/python/pdm.lock index 46a414f1b..3fc30643e 100644 --- a/sdk/python/pdm.lock +++ b/sdk/python/pdm.lock @@ -310,7 +310,7 @@ summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "4.0" -content_hash = "sha256:d0f252d1f7668578b6920f4df7c430b3b482f4aeadb3f686703debc8f88ba27e" +content_hash = "sha256:08e34ff1aedd581c2a61fe21543a497e828f3fd2b58ad1513120b79ca96906be" [metadata.files] "anyio 3.6.2" = [ @@ -353,6 +353,18 @@ content_hash = "sha256:d0f252d1f7668578b6920f4df7c430b3b482f4aeadb3f686703debc8f {url = "https://files.pythonhosted.org/packages/7c/37/4fed6f28d8010c791437b808a94f37c4ae58ee3998b653d2c0286a8cc190/filelock-3.8.2-py3-none-any.whl", hash = "sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c"}, {url = "https://files.pythonhosted.org/packages/d8/73/292d9ea2370840a163e6dd2d2816a571244e9335e2f6ad957bf0527c492f/filelock-3.8.2.tar.gz", hash = "sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2"}, ] +"h11 0.14.0" = [ + {url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] +"httpcore 0.16.2" = [ + {url = "https://files.pythonhosted.org/packages/91/52/93f22e5441539256c0d113faf17e45284aee16eebdd95089e3ca6f480b18/httpcore-0.16.2-py3-none-any.whl", hash = "sha256:52c79095197178856724541e845f2db86d5f1527640d9254b5b8f6f6cebfdee6"}, + {url = "https://files.pythonhosted.org/packages/9b/20/26f6cc4fd00391f8f1c57b0020f5c6eec23904723db04b6f7608e222d815/httpcore-0.16.2.tar.gz", hash = "sha256:c35c5176dc82db732acfd90b581a3062c999a72305df30c0fc8fafd8e4aca068"}, +] +"httpx 0.23.1" = [ + {url = "https://files.pythonhosted.org/packages/8a/df/a3e8b91dfb452e645ef110985a30f0915276a1a2144004c7671c07bb203c/httpx-0.23.1.tar.gz", hash = "sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19"}, + {url = "https://files.pythonhosted.org/packages/e1/74/cdce73069e021ad5913451b86c2707b027975cf302016ca557686d87eb41/httpx-0.23.1-py3-none-any.whl", hash = "sha256:0b9b1f0ee18b9978d637b0776bfd7f54e2ca278e063e3586d8f01cda89e042a8"}, +] "identify 2.5.10" = [ {url = "https://files.pythonhosted.org/packages/24/7f/6812dcfa4ff7f8bd425252eb2bd4ef394477eea1424d3e09bbbfdac94e87/identify-2.5.10.tar.gz", hash = "sha256:dce9e31fee7dbc45fea36a9e855c316b8fbf807e65a862f160840bb5a2bf5dfd"}, {url = "https://files.pythonhosted.org/packages/3e/63/88e087dc7fbe81d16225bdff91fd781bf7cb03101ffc1adfa6b909e36274/identify-2.5.10-py2.py3-none-any.whl", hash = "sha256:fb7c2feaeca6976a3ffa31ec3236a6911fbc51aec9acc111de2aed99f244ade2"}, @@ -447,6 +459,10 @@ content_hash = "sha256:d0f252d1f7668578b6920f4df7c430b3b482f4aeadb3f686703debc8f {url = "https://files.pythonhosted.org/packages/a5/61/a867851fd5ab77277495a8709ddda0861b28163c4613b011bc00228cc724/requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, {url = "https://files.pythonhosted.org/packages/ca/91/6d9b8ccacd0412c08820f72cebaa4f0c0441b5cda699c90f618b6f8a1b42/requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, ] +"rfc3986 1.5.0" = [ + {url = "https://files.pythonhosted.org/packages/79/30/5b1b6c28c105629cc12b33bdcbb0b11b5bb1880c6cfbd955f9e792921aa8/rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, + {url = "https://files.pythonhosted.org/packages/c4/e5/63ca2c4edf4e00657584608bee1001302bbf8c5f569340b78304f2f446cb/rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, +] "setuptools 65.6.3" = [ {url = "https://files.pythonhosted.org/packages/b6/21/cb9a8d0b2c8597c83fce8e9c02884bce3d4951e41e807fc35791c6b23d9a/setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, {url = "https://files.pythonhosted.org/packages/ef/e3/29d6e1a07e8d90ace4a522d9689d03e833b67b50d1588e693eec15f26251/setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, @@ -513,6 +529,77 @@ content_hash = "sha256:d0f252d1f7668578b6920f4df7c430b3b482f4aeadb3f686703debc8f {url = "https://files.pythonhosted.org/packages/75/af/1d13b93e7a21aca7f8ab8645fcfcfad21fc39716dc9dce5dc2a97f73ff78/websocket-client-1.4.2.tar.gz", hash = "sha256:d6e8f90ca8e2dd4e8027c4561adeb9456b54044312dba655e7cae652ceb9ae59"}, {url = "https://files.pythonhosted.org/packages/78/d5/2b5719b738791cd798e8f097eba4bb093ff5efca5cef2f3d37a72daa111f/websocket_client-1.4.2-py3-none-any.whl", hash = "sha256:d6b06432f184438d99ac1f456eaf22fe1ade524c3dd16e661142dc54e9cba574"}, ] +"websockets 10.4" = [ + {url = "https://files.pythonhosted.org/packages/00/15/611ddaca66937f77aa5021e97c9bec61e6a30668b75db3707713b69b3b88/websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"}, + {url = "https://files.pythonhosted.org/packages/03/e2/7784912651a299a5e060656e6368946ae4c1da63f01236f7d650e8070cf8/websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"}, + {url = "https://files.pythonhosted.org/packages/09/35/2b8ed52dc995507476ebbb7a91a0c5ed80fd80fa0a840f422ac25c722dbf/websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"}, + {url = "https://files.pythonhosted.org/packages/0c/56/b2d373ed19b4e7b6c5c7630d598ba10473fa6131e67e69590214ab18bc09/websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"}, + {url = "https://files.pythonhosted.org/packages/0c/f0/195097822f8edc4ffa355f6463a1890928577517382c0baededc760f9397/websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"}, + {url = "https://files.pythonhosted.org/packages/14/88/81c08fb3418c5aedf3776333f29443599729509a4f673d6598dd769d3d6b/websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"}, + {url = "https://files.pythonhosted.org/packages/17/e4/3bdc2ea97d7da70d9f184051dcd40f27c849ded517ea9bab70df677a6b23/websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"}, + {url = "https://files.pythonhosted.org/packages/19/a3/02ce75ffca3ef147cc0f44647c67acb3171b5a09910b5b9f083b5ca395a6/websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"}, + {url = "https://files.pythonhosted.org/packages/1c/4b/cab8fed34c3a29d4594ff77234f6e6b45feb35331f1c12fccf92ca5486dd/websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"}, + {url = "https://files.pythonhosted.org/packages/1d/06/5ecd0434cf35f92ca9ce80e38a3ac9bf5422ace9488693c3900e2f1c7fa0/websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"}, + {url = "https://files.pythonhosted.org/packages/1e/76/163a18626001465a309bf74b6aeb301d7092e304637fe00f89d7efc75c44/websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"}, + {url = "https://files.pythonhosted.org/packages/20/7a/bd0ce7ac1cfafc76c84d6e8051bcbd0f7def8e45207230833bd6ff77a41d/websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"}, + {url = "https://files.pythonhosted.org/packages/25/a7/4e32f8edfc26339d8d170fe539e0b83a329c42d974dacfe07a0566390aef/websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"}, + {url = "https://files.pythonhosted.org/packages/27/bb/6327e8c7d4dd7d5b450b409a461be278968ce05c54da13da581ac87661db/websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"}, + {url = "https://files.pythonhosted.org/packages/29/33/dd88aefeabc9dddb4f48c9e15c6c2554dfb6b4cf8d8f1b4de4d12ba997de/websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"}, + {url = "https://files.pythonhosted.org/packages/2b/cb/d394efe7b0ee6cdeffac28a1cb054e42f9f95974885ca3bcd6fceb0acde1/websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"}, + {url = "https://files.pythonhosted.org/packages/2e/dd/521f0574bed6d08ce5e0acd5893ae418c0a81ef55eb4c960aedac9cbd929/websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"}, + {url = "https://files.pythonhosted.org/packages/33/3a/72c9d733d676447da2c89a35c694f779a9a360cff51ee0f90bb562d80cd4/websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"}, + {url = "https://files.pythonhosted.org/packages/36/8f/6dd75723ea67d54dec3a597ad781642c0febe8d51f233b95347981c0e549/websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"}, + {url = "https://files.pythonhosted.org/packages/37/02/ef21ca4698c2fd950250e5ac397fd07b0c9f16bbd073d0ea64c25baef9c1/websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"}, + {url = "https://files.pythonhosted.org/packages/3e/a5/e4535867a96bb07000c54172e1be82cd0b3a95339244cac1d400f8ba9b64/websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"}, + {url = "https://files.pythonhosted.org/packages/47/4d/f2e28f112302d3bc794b74ae64656255161d8223f4d47bd17d40cbb3629e/websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"}, + {url = "https://files.pythonhosted.org/packages/47/58/69435f1479acb56b3678905b5f2be57908a201c28465d4368d91f52cad76/websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"}, + {url = "https://files.pythonhosted.org/packages/4a/39/3b6b64f775f1f4f5de6eb909d72f3f794f453730b5b3176fa5021ff334ba/websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"}, + {url = "https://files.pythonhosted.org/packages/4d/6f/2388f9304cdaa0215b6388f837c6dbfe6d63ac1bba8c196e3b14eea1831e/websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"}, + {url = "https://files.pythonhosted.org/packages/4e/8b/854b3625cc5130e4af8a10a7502c2f6c16d1bd107ff009394127a2f8abb3/websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"}, + {url = "https://files.pythonhosted.org/packages/57/d7/df17197565e8874f0a77f8211304169ad4f39ffa3e8c008a7b0bf187a238/websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"}, + {url = "https://files.pythonhosted.org/packages/5a/87/dea889793d2d0958be254fc86dac528d97de9354d16fcdbcbad259750014/websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"}, + {url = "https://files.pythonhosted.org/packages/5d/3c/fc1725524e48f624df77f5998b1c7070fdddec3ae67a2ffbc99ffd116269/websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"}, + {url = "https://files.pythonhosted.org/packages/60/3a/6dccbe2725d13c398b90cbebeea684cda7792e6d874f96417db900556ad0/websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"}, + {url = "https://files.pythonhosted.org/packages/62/76/c2411e634979cc6e812ef2a96aa295545cfcbc9566b298db09f3f4639d62/websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"}, + {url = "https://files.pythonhosted.org/packages/63/f2/ec4c59b4f91936eb2a5ddcf2f7e57184acbce5122d5d83911c5a47f25144/websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"}, + {url = "https://files.pythonhosted.org/packages/68/bd/c8bd8354fc629863a2db39c9182d40297f47dfb2ed3e178bc83041ce044b/websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"}, + {url = "https://files.pythonhosted.org/packages/68/ec/3267f8bbe8a4a5e181ab3fc67cc137f0966ab9e9a4da14ffc603f320b9e6/websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"}, + {url = "https://files.pythonhosted.org/packages/71/93/5a4f408177e43d84274e1c08cbea3e50ad80db654dc25a0bba79dbdc00b4/websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"}, + {url = "https://files.pythonhosted.org/packages/75/18/155c3582fd69b60d9c490fb0e64e37269c55d5873cbcb37f83e2d3feb078/websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"}, + {url = "https://files.pythonhosted.org/packages/77/65/d7c73e62cf19f068850ddab548837329dab9c023567f5834747f61cdc747/websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"}, + {url = "https://files.pythonhosted.org/packages/85/dc/549a807a53c13fd4a8dac286f117a7a71260defea9ec0c05d6027f2ae273/websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, + {url = "https://files.pythonhosted.org/packages/86/8e/390e0e3db702c55d31ca3999c622bb3b8b480c306c1bdee6a2da44b13b1b/websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"}, + {url = "https://files.pythonhosted.org/packages/88/00/9776e2626a30e3455a830665e50cf40f5d34a4134272b3138a637afa38a7/websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"}, + {url = "https://files.pythonhosted.org/packages/88/97/d70e2d528b9ffe759134e5db6b1424b61cd61fd1c4471b178c76e01f41af/websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"}, + {url = "https://files.pythonhosted.org/packages/8a/1e/8f34d7ee924dc7a624c1e14f43209484cb5eccb58e892285d45551729a95/websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"}, + {url = "https://files.pythonhosted.org/packages/90/e1/22e43e9a1fbc9ddf4a0317b231e2e28eddfbe8804b7ca4a9f7fba7033b17/websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"}, + {url = "https://files.pythonhosted.org/packages/93/7b/72134e4c75002e311c072f0665fe45f7321d614c5c65181888faddd616e9/websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"}, + {url = "https://files.pythonhosted.org/packages/a0/92/aa8d1ba3a7e3e6cf6d5d1c929530a40138667ea60454bf5c0fff3b93cae2/websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"}, + {url = "https://files.pythonhosted.org/packages/a1/6f/60e5f6e114b6077683d74da5df0d4af647a9e6d2a18b4698f577b2cb7c14/websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"}, + {url = "https://files.pythonhosted.org/packages/a1/f6/83da14582fbb0496c47a4c039bd6e802886a0c300e9795c0f839fd1498e3/websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"}, + {url = "https://files.pythonhosted.org/packages/ab/41/ed2fecb228c1f25cea03fce4a22a86f7771a10875d5762e777e943bb7d68/websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"}, + {url = "https://files.pythonhosted.org/packages/b0/fc/a818cddc63589e12d5eff9b51a59aad82e2adf35279493248a3742c41f85/websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"}, + {url = "https://files.pythonhosted.org/packages/b1/8f/dbffb63e7da0ada24e9ef8802c439169e0ed9a7ef8f6049874e6cbfc7919/websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"}, + {url = "https://files.pythonhosted.org/packages/b4/91/c460f5164af303b31f58362935f7b8ed1750e3b8fbcb900da4b0661532a8/websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"}, + {url = "https://files.pythonhosted.org/packages/bb/5c/7dc1f604688f43168ef17313055e048c755a29afde821f7e0b19bd3a180f/websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"}, + {url = "https://files.pythonhosted.org/packages/c5/01/145d2883dfeffedf541a7c95bb26f8d8b5ddca84a7c8f671ec3b878ae7cd/websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"}, + {url = "https://files.pythonhosted.org/packages/c6/41/07f39745017af5381aeb6c1d8c6509aa1861193c948648d4aaf4d0637915/websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"}, + {url = "https://files.pythonhosted.org/packages/cc/19/2f003f9f81c0fab2eabb81d8fc2fce5fb5b5714f1b4abfe897cb209e031d/websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"}, + {url = "https://files.pythonhosted.org/packages/d1/60/0a6cb94e25b981e428c1cdcc2b0a406ac6e1dfc78d8a81c8a4ee7510e853/websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"}, + {url = "https://files.pythonhosted.org/packages/d1/c6/9489869aa591e6a8941b0af2302f8383e199e90477559a510713d41bfa45/websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"}, + {url = "https://files.pythonhosted.org/packages/d4/1a/2e4afd95abd33bd6ad77042270f8eee3697e07cdd749c068bff08bba2022/websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"}, + {url = "https://files.pythonhosted.org/packages/d5/5d/d0b039f0db0bb1fea93437721cf3cd8a244ad02a86960c38a3853d5e1fab/websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"}, + {url = "https://files.pythonhosted.org/packages/d6/7c/79ea4e7f56dfe7f703213000bbbd29b70cef2666698d98b66ce1af43caee/websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"}, + {url = "https://files.pythonhosted.org/packages/d7/f9/f64ec37da654351b212e5534b0e31703ed80d2a6acb6b8c1b1373fafa876/websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"}, + {url = "https://files.pythonhosted.org/packages/da/0b/a501ed176c69b51ca83f4186bad80bba9b59ab354fd8954d7d36cb2ec47f/websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"}, + {url = "https://files.pythonhosted.org/packages/e0/8d/7bffabd3f10a88cd68080669b33f407471283becf7e5cb4f0143b117211d/websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"}, + {url = "https://files.pythonhosted.org/packages/e6/94/cb97e5a9d019e473a37317a740852850ef09e14c02621dd86a898ec90f7a/websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"}, + {url = "https://files.pythonhosted.org/packages/e9/48/a0751eafbeab06866fc70a66f7dfa08422cb96113af9138e526e7b106f14/websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"}, + {url = "https://files.pythonhosted.org/packages/ec/ba/74b4b92cc41ffc4cfa791fb9f8e8ab7c4d9bf84e54a5bef12ab23eb54880/websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"}, + {url = "https://files.pythonhosted.org/packages/f8/f0/437187175182beed10246f53ef9793a5f6e087ce71ee25b64fdb12e396e0/websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"}, + {url = "https://files.pythonhosted.org/packages/f9/15/ab0e9155700d3037ffe4a146a719f3e68ee025c9d45d6a39b027e928db52/websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"}, + {url = "https://files.pythonhosted.org/packages/fd/42/07f31d9f9e142b38cde8d3ea0c8ea1bacf9bc366f2f573eca57086e9f2a6/websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"}, +] "zipp 3.11.0" = [ {url = "https://files.pythonhosted.org/packages/8e/b3/8b16a007184714f71157b1a71bbe632c5d66dd43bc8152b3c799b13881e1/zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, {url = "https://files.pythonhosted.org/packages/d8/20/256eb3f3f437c575fb1a2efdce5e801a5ce3162ea8117da96c43e6ee97d8/zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, From ce23f585f18df646f5455dbe4e74e7463c6c3889 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Wed, 4 Jan 2023 12:01:29 -0800 Subject: [PATCH 22/34] GitHub OAuth provider to async --- .../auth/providers/github_oauth_provider.py | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/sdk/python/flet/auth/providers/github_oauth_provider.py b/sdk/python/flet/auth/providers/github_oauth_provider.py index 00d4af98b..24029745f 100644 --- a/sdk/python/flet/auth/providers/github_oauth_provider.py +++ b/sdk/python/flet/auth/providers/github_oauth_provider.py @@ -25,7 +25,7 @@ def _fetch_groups(self, access_token: str) -> List[Group]: teams_resp = client.send(self.__get_user_teams_request(access_token)) return self.__complete_fetch_groups(teams_resp) - async def _fetch_groups_sync(self, access_token: str) -> List[Group]: + async def _fetch_groups_async(self, access_token: str) -> List[Group]: async with httpx.AsyncClient() as client: teams_resp = await client.send(self.__get_user_teams_request(access_token)) return self.__complete_fetch_groups(teams_resp) @@ -54,14 +54,14 @@ def _fetch_user(self, access_token: str) -> Optional[User]: with httpx.Client() as client: user_resp = client.send(user_req) emails_resp = client.send(emails_req) + return self.__complete_fetch_user_details(user_resp, emails_resp) - uj = json.loads(user_resp.text) - ej = json.loads(emails_resp.text) - for e in ej: - if e["primary"]: - uj["email"] = e["email"] - break - return User(uj, id=str(uj["id"])) + async def _fetch_user_async(self, access_token: str) -> Optional[User]: + user_req, emails_req = self.__get_user_details_requests(access_token) + async with httpx.AsyncClient() as client: + user_resp = await client.send(user_req) + emails_resp = await client.send(emails_req) + return self.__complete_fetch_user_details(user_resp, emails_resp) def __get_user_details_requests(self, access_token): headers = {"Authorization": "Bearer {}".format(access_token)} @@ -69,3 +69,12 @@ def __get_user_details_requests(self, access_token): httpx.Request("GET", "https://api.github.com/user", headers=headers), httpx.Request("GET", "https://api.github.com/user/emails", headers=headers), ) + + def __complete_fetch_user_details(self, user_resp, emails_resp): + uj = json.loads(user_resp.text) + ej = json.loads(emails_resp.text) + for e in ej: + if e["primary"]: + uj["email"] = e["email"] + break + return User(uj, id=str(uj["id"])) From 281c7494655069a6f176577b187b3a7ff7620bf9 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 5 Jan 2023 09:20:04 -0800 Subject: [PATCH 23/34] Authorization async --- sdk/python/flet/auth/authorization.py | 113 ++++++++++++++++++++++---- sdk/python/flet/control.py | 4 - sdk/python/flet/page.py | 41 ++++++---- 3 files changed, 118 insertions(+), 40 deletions(-) diff --git a/sdk/python/flet/auth/authorization.py b/sdk/python/flet/auth/authorization.py index 7f92b6bb6..a10a47a1e 100644 --- a/sdk/python/flet/auth/authorization.py +++ b/sdk/python/flet/auth/authorization.py @@ -1,8 +1,11 @@ +import asyncio import json import secrets import threading import time from typing import List, Optional, Tuple +from flet.utils import is_asyncio +import httpx import requests from oauthlib.oauth2 import WebApplicationClient @@ -20,7 +23,6 @@ def __init__( fetch_user: bool, fetch_groups: bool, scope: Optional[List[str]] = None, - saved_token: Optional[str] = None, ) -> None: self.fetch_user = fetch_user self.fetch_groups = fetch_groups @@ -28,7 +30,8 @@ def __init__( self.provider = provider self.__token: Optional[OAuthToken] = None self.user: Optional[User] = None - self._lock = threading.Lock() + self.__lock = threading.Lock() if not is_asyncio() else None + self.__async_lock = asyncio.Lock() if is_asyncio() else None # fix scopes self.scope.extend(self.provider.scopes) @@ -41,18 +44,30 @@ def __init__( if s not in self.scope: self.scope.append(s) - if saved_token != None: - self.__token = OAuthToken.from_json(saved_token) - self.__refresh_token() - self.__fetch_user_and_groups() + def dehydrate_token(self, saved_token: str): + self.__token = OAuthToken.from_json(saved_token) + self.__refresh_token() + self.__fetch_user_and_groups() + + async def dehydrate_token_async(self, saved_token: str): + self.__token = OAuthToken.from_json(saved_token) + await self.__refresh_token_async() + await self.__fetch_user_and_groups_async() # token @property def token(self) -> Optional[OAuthToken]: - with self._lock: + with self.__lock: self.__refresh_token() return self.__token + # token_async + @property + async def token_async(self) -> Optional[OAuthToken]: + async with self.__async_lock: + await self.__refresh_token_async() + return self.__token + def get_authorization_data(self) -> Tuple[str, str]: self.state = secrets.token_urlsafe(16) client = WebApplicationClient(self.provider.client_id) @@ -65,6 +80,24 @@ def get_authorization_data(self) -> Tuple[str, str]: return (authorization_url, self.state) def request_token(self, code: str): + req = self.__get_request_token_request(code) + with httpx.Client() as client: + resp = client.send(req) + client = WebApplicationClient(self.provider.client_id) + t = client.parse_request_body_response(resp.text) + self.__token = self.__convert_token(t) + self.__fetch_user_and_groups() + + async def request_token_async(self, code: str): + req = self.__get_request_token_request(code) + async with httpx.AsyncClient() as client: + resp = await client.send(req) + client = WebApplicationClient(self.provider.client_id) + t = client.parse_request_body_response(resp.text) + self.__token = self.__convert_token(t) + await self.__fetch_user_and_groups_async() + + def __get_request_token_request(self, code: str): client = WebApplicationClient(self.provider.client_id) data = client.prepare_request_body( code=code, @@ -73,12 +106,9 @@ def request_token(self, code: str): client_secret=self.provider.client_secret, ) headers = {"content-type": "application/x-www-form-urlencoded"} - response = requests.post( - self.provider.token_endpoint, data=data, headers=headers + return httpx.Request( + "POST", self.provider.token_endpoint, content=data, headers=headers ) - t = client.parse_request_body_response(response.text) - self.__token = self.__convert_token(t) - self.__fetch_user_and_groups() def __fetch_user_and_groups(self): assert self.__token is not None @@ -95,6 +125,21 @@ def __fetch_user_and_groups(self): self.__token.access_token ) + async def __fetch_user_and_groups_async(self): + assert self.__token is not None + if self.fetch_user: + self.user = await self.provider._fetch_user_async(self.__token.access_token) + if self.user == None and self.provider.user_endpoint != None: + if self.provider.user_id_fn == None: + raise Exception( + "user_id_fn must be specified too if user_endpoint is not None" + ) + self.user = await self.__get_user_async() + if self.fetch_groups and self.user != None: + self.user.groups = await self.provider._fetch_groups_async( + self.__token.access_token + ) + def __convert_token(self, t: OAuth2Token): return OAuthToken( access_token=t["access_token"], @@ -106,13 +151,27 @@ def __convert_token(self, t: OAuth2Token): ) def __refresh_token(self): + refresh_req = self.__get_refresh_token_request() + if refresh_req: + with httpx.Client() as client: + refresh_resp = client.send(refresh_req) + self.__complete_refresh_token_request(refresh_resp) + + async def __refresh_token_async(self): + refresh_req = self.__get_refresh_token_request() + if refresh_req: + async with httpx.AsyncClient() as client: + refresh_resp = await client.send(refresh_req) + self.__complete_refresh_token_request(refresh_resp) + + def __get_refresh_token_request(self): if ( self.__token is None or self.__token.expires_at is None or self.__token.refresh_token is None or time.time() < self.__token.expires_at ): - return + return None assert self.__token is not None client = WebApplicationClient(self.provider.client_id) @@ -123,19 +182,37 @@ def __refresh_token(self): redirect_uri=self.provider.redirect_url, ) headers = {"content-type": "application/x-www-form-urlencoded"} - response = requests.post( - self.provider.token_endpoint, data=data, headers=headers + return httpx.Request( + "POST", url=self.provider.token_endpoint, content=data, headers=headers ) - t = client.parse_request_body_response(response.text) + + def __complete_refresh_token_request(self, refresh_resp): + assert self.__token is not None + client = WebApplicationClient(self.provider.client_id) + t = client.parse_request_body_response(refresh_resp.text) if t.get("refresh_token") == None: t["refresh_token"] = self.__token.refresh_token self.__token = self.__convert_token(t) def __get_user(self): + user_req = self.__get_user_request() + with httpx.Client() as client: + user_resp = client.send(user_req) + return self.__complete_user_request(user_resp) + + async def __get_user_async(self): + user_req = self.__get_user_request() + async with httpx.AsyncClient() as client: + user_resp = await client.send(user_req) + return self.__complete_user_request(user_resp) + + def __get_user_request(self): assert self.token is not None assert self.provider.user_endpoint is not None - assert self.provider.user_id_fn is not None headers = {"Authorization": "Bearer {}".format(self.token.access_token)} - user_resp = requests.get(self.provider.user_endpoint, headers=headers) + return httpx.Request("GET", self.provider.user_endpoint, headers=headers) + + def __complete_user_request(self, user_resp): + assert self.provider.user_id_fn is not None uj = json.loads(user_resp.text) return User(uj, str(self.provider.user_id_fn(uj))) diff --git a/sdk/python/flet/control.py b/sdk/python/flet/control.py index d90b54f98..b862ac9d1 100644 --- a/sdk/python/flet/control.py +++ b/sdk/python/flet/control.py @@ -1,7 +1,5 @@ import datetime as dt import json -import threading -import asyncio from difflib import SequenceMatcher from typing import TYPE_CHECKING, Any, Union @@ -52,8 +50,6 @@ def __init__( self.__data: Any = None self.data = data self.__event_handlers = {} - self._lock = threading.Lock() - self._async_lock = asyncio.Lock() if ref: ref.current = self diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index ec1774678..46f430546 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -1,3 +1,4 @@ +import asyncio import json import logging import threading @@ -40,6 +41,7 @@ ThemeMode, ThemeModeString, ) +from flet.utils import is_asyncio from flet.view import View try: @@ -83,6 +85,9 @@ def __init__(self, conn: Connection, session_id): self._session_id = session_id self._index = {self._Control__uid: self} # index with all page controls + self.__lock = threading.Lock() if not is_asyncio() else None + self.__async_lock = asyncio.Lock() if is_asyncio() else None + self.__views = [View()] self.__default_view = self.__views[0] self._controls = self.__default_view.controls @@ -217,7 +222,7 @@ def __set_page_details(self, values): self._set_attr("windowLeft", values[9], False) def update(self, *controls): - with self._lock: + with self.__lock: if len(controls) == 0: r = self.__update(self) else: @@ -225,7 +230,7 @@ def update(self, *controls): self.__handle_mount_unmount(*r) async def update_async(self, *controls): - async with self._async_lock: + async with self.__async_lock: if len(controls) == 0: r = await self.__update_async(self) else: @@ -233,19 +238,19 @@ async def update_async(self, *controls): await self.__handle_mount_unmount_async(*r) def add(self, *controls): - with self._lock: + with self.__lock: self._controls.extend(controls) r = self.__update(self) self.__handle_mount_unmount(*r) async def add_async(self, *controls): - async with self._async_lock: + async with self.__async_lock: self._controls.extend(controls) r = await self.__update_async(self) await self.__handle_mount_unmount_async(*r) def insert(self, at, *controls): - with self._lock: + with self.__lock: n = at for control in controls: self._controls.insert(n, control) @@ -254,7 +259,7 @@ def insert(self, at, *controls): self.__handle_mount_unmount(*r) async def insert_async(self, at, *controls): - async with self._async_lock: + async with self.__async_lock: n = at for control in controls: self._controls.insert(n, control) @@ -263,39 +268,39 @@ async def insert_async(self, at, *controls): await self.__handle_mount_unmount_async(*r) def remove(self, *controls): - with self._lock: + with self.__lock: for control in controls: self._controls.remove(control) r = self.__update(self) self.__handle_mount_unmount(*r) async def remove_async(self, *controls): - async with self._async_lock: + async with self.__async_lock: for control in controls: self._controls.remove(control) r = await self.__update_async(self) await self.__handle_mount_unmount_async(*r) def remove_at(self, index): - with self._lock: + with self.__lock: self._controls.pop(index) r = self.__update(self) self.__handle_mount_unmount(*r) async def remove_at_async(self, index): - async with self._async_lock: + async with self.__async_lock: self._controls.pop(index) r = await self.__update_async(self) await self.__handle_mount_unmount_async(*r) def clean(self): - with self._lock: + with self.__lock: self._controls.clear() r = self.__update(self) self.__handle_mount_unmount(*r) async def clean_async(self): - async with self._async_lock: + async with self.__async_lock: self._controls.clear() r = await self.__update_async(self) await self.__handle_mount_unmount_async(*r) @@ -356,17 +361,17 @@ async def __handle_mount_unmount_async(self, added_controls, removed_controls): await ctrl.did_mount_async() def error(self, message=""): - with self._lock: + with self.__lock: self._send_command("error", [message]) async def error_async(self, message=""): - async with self._async_lock: + async with self.__async_lock: await self._send_command_async("error", [message]) def on_event(self, e: Event): logging.info(f"page.on_event: {e.target} {e.name} {e.data}") - with self._lock: + with self.__lock: if e.target == "page" and e.name == "change": self.__on_page_change_event(e.data) @@ -381,7 +386,7 @@ async def on_event_async(self, e: Event): logging.info(f"page.on_event_async: {e.target} {e.name} {e.data}") if e.target == "page" and e.name == "change": - async with self._async_lock: + async with self.__async_lock: self.__on_page_change_event(e.data) elif e.target in self._index: @@ -454,14 +459,13 @@ def login( on_open_authorization_url=None, complete_page_html: Optional[str] = None, redirect_to_page=False, - authorization=Authorization + authorization=Authorization, ): self.__authorization = authorization( provider, fetch_user=fetch_user, fetch_groups=fetch_groups, scope=scope, - saved_token=saved_token, ) if saved_token == None: authorization_url, state = self.__authorization.get_authorization_data() @@ -483,6 +487,7 @@ def login( authorization_url, "flet_oauth_signin", web_popup_window=self.web ) else: + self.__authorization.dehydrate_token(saved_token) self.__on_login.get_handler()(LoginEvent(error="", error_description="")) return self.__authorization From 88ee2e269e201872b1389970a783c9684cba5eae Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 5 Jan 2023 09:23:41 -0800 Subject: [PATCH 24/34] remove "requests" dependency --- sdk/python/flet/auth/authorization.py | 1 - sdk/python/pdm.lock | 38 +-------------------------- sdk/python/pyproject.toml | 1 - 3 files changed, 1 insertion(+), 39 deletions(-) diff --git a/sdk/python/flet/auth/authorization.py b/sdk/python/flet/auth/authorization.py index a10a47a1e..0137ed0c7 100644 --- a/sdk/python/flet/auth/authorization.py +++ b/sdk/python/flet/auth/authorization.py @@ -7,7 +7,6 @@ from flet.utils import is_asyncio import httpx -import requests from oauthlib.oauth2 import WebApplicationClient from oauthlib.oauth2.rfc6749.tokens import OAuth2Token diff --git a/sdk/python/pdm.lock b/sdk/python/pdm.lock index 3fc30643e..5848346cd 100644 --- a/sdk/python/pdm.lock +++ b/sdk/python/pdm.lock @@ -33,12 +33,6 @@ version = "3.3.1" requires_python = ">=3.6.1" summary = "Validate configuration and produce human readable error messages." -[[package]] -name = "charset-normalizer" -version = "2.1.1" -requires_python = ">=3.6.0" -summary = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." - [[package]] name = "colorama" version = "0.4.6" @@ -203,18 +197,6 @@ dependencies = [ "six>=1.9.0", ] -[[package]] -name = "requests" -version = "2.28.1" -requires_python = ">=3.7, <4" -summary = "Python HTTP for Humans." -dependencies = [ - "certifi>=2017.4.17", - "charset-normalizer<3,>=2", - "idna<4,>=2.5", - "urllib3<1.27,>=1.21.1", -] - [[package]] name = "rfc3986" version = "1.5.0" @@ -266,12 +248,6 @@ version = "4.4.0" requires_python = ">=3.7" summary = "Backported and Experimental Type Hints for Python 3.7+" -[[package]] -name = "urllib3" -version = "1.26.13" -requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" -summary = "HTTP library with thread-safe connection pooling, file post, and more." - [[package]] name = "virtualenv" version = "20.17.1" @@ -310,7 +286,7 @@ summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "4.0" -content_hash = "sha256:08e34ff1aedd581c2a61fe21543a497e828f3fd2b58ad1513120b79ca96906be" +content_hash = "sha256:56985dab38f0f277a90f27e9e63677cb8632a75f4499101ecab6e6757a1d43ac" [metadata.files] "anyio 3.6.2" = [ @@ -333,10 +309,6 @@ content_hash = "sha256:08e34ff1aedd581c2a61fe21543a497e828f3fd2b58ad1513120b79ca {url = "https://files.pythonhosted.org/packages/6d/82/0a0ebd35bae9981dea55c06f8e6aaf44a49171ad798795c72c6f64cba4c2/cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {url = "https://files.pythonhosted.org/packages/c4/bf/d0d622b660d414a47dc7f0d303791a627663f554345b21250e39e7acb48b/cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -"charset-normalizer 2.1.1" = [ - {url = "https://files.pythonhosted.org/packages/a1/34/44964211e5410b051e4b8d2869c470ae8a68ae274953b1c7de6d98bbcf94/charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {url = "https://files.pythonhosted.org/packages/db/51/a507c856293ab05cdc1db77ff4bc1268ddd39f29e7dc4919aa497f0adbec/charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] "colorama 0.4.6" = [ {url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -455,10 +427,6 @@ content_hash = "sha256:08e34ff1aedd581c2a61fe21543a497e828f3fd2b58ad1513120b79ca {url = "https://files.pythonhosted.org/packages/65/e1/824989291d0f01886074fdf9504ba54598f5665bc4dd373b589b87e76608/repath-0.9.0.tar.gz", hash = "sha256:8292139bac6a0e43fd9d70605d4e8daeb25d46672e484ed31a24c7ce0aef0fb7"}, {url = "https://files.pythonhosted.org/packages/87/ed/92e9b8a3ffc562f21df14ef2538f54e911df29730e1f0d79130a4edc86e7/repath-0.9.0-py3-none-any.whl", hash = "sha256:ee079d6c91faeb843274d22d8f786094ee01316ecfe293a1eb6546312bb6a318"}, ] -"requests 2.28.1" = [ - {url = "https://files.pythonhosted.org/packages/a5/61/a867851fd5ab77277495a8709ddda0861b28163c4613b011bc00228cc724/requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, - {url = "https://files.pythonhosted.org/packages/ca/91/6d9b8ccacd0412c08820f72cebaa4f0c0441b5cda699c90f618b6f8a1b42/requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, -] "rfc3986 1.5.0" = [ {url = "https://files.pythonhosted.org/packages/79/30/5b1b6c28c105629cc12b33bdcbb0b11b5bb1880c6cfbd955f9e792921aa8/rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, {url = "https://files.pythonhosted.org/packages/c4/e5/63ca2c4edf4e00657584608bee1001302bbf8c5f569340b78304f2f446cb/rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, @@ -487,10 +455,6 @@ content_hash = "sha256:08e34ff1aedd581c2a61fe21543a497e828f3fd2b58ad1513120b79ca {url = "https://files.pythonhosted.org/packages/0b/8e/f1a0a5a76cfef77e1eb6004cb49e5f8d72634da638420b9ea492ce8305e8/typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {url = "https://files.pythonhosted.org/packages/e3/a7/8f4e456ef0adac43f452efc2d0e4b242ab831297f1bac60ac815d37eb9cf/typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] -"urllib3 1.26.13" = [ - {url = "https://files.pythonhosted.org/packages/65/0c/cc6644eaa594585e5875f46f3c83ee8762b647b51fc5b0fb253a242df2dc/urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, - {url = "https://files.pythonhosted.org/packages/c2/51/32da03cf19d17d46cce5c731967bf58de9bd71db3a379932f53b094deda4/urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, -] "virtualenv 20.17.1" = [ {url = "https://files.pythonhosted.org/packages/18/a2/7931d40ecb02b5236a34ac53770f2f6931e3082b7a7dafe915d892d749d6/virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, {url = "https://files.pythonhosted.org/packages/7b/19/65f13cff26c8cc11fdfcb0499cd8f13388dd7b35a79a376755f152b42d86/virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index e67323fd6..6ad32fb9e 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -12,7 +12,6 @@ dependencies = [ 'typing_extensions; python_version < "3.8"', "repath>=0.9.0", "watchdog>=2.1.9", - "requests>=2.28.1", "oauthlib>=3.2.0", "websockets>=10.4", "httpx>=0.23.1", From f017450bb58c1038d7c7e9f1947b777645b449a7 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 5 Jan 2023 10:37:06 -0800 Subject: [PATCH 25/34] Page class - most of the methods async --- sdk/python/flet/flet.py | 4 +- sdk/python/flet/page.py | 209 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 202 insertions(+), 11 deletions(-) diff --git a/sdk/python/flet/flet.py b/sdk/python/flet/flet.py index ca69b5bee..607b2fd44 100644 --- a/sdk/python/flet/flet.py +++ b/sdk/python/flet/flet.py @@ -516,7 +516,7 @@ def __locate_and_unpack_flet_view(page_url, hidden): flet_path = which("flet", sys.argv[0]) if flet_path and "/Contents/MacOS/" in flet_path: logging.info(f"Flet.app found in PATH: {flet_path}") - temp_flet_dir = Path(flet_path).parent.parent.parent + temp_flet_dir = Path(flet_path).parent.parent.parent.parent else: # check if flet_view.app exists in a temp directory if not temp_flet_dir.exists(): @@ -539,7 +539,7 @@ def __locate_and_unpack_flet_view(page_url, hidden): app_name = f assert app_name is not None, f"Application bundle not found in {temp_flet_dir}" app_path = temp_flet_dir.joinpath(app_name) - args = ["open", str(app_path), "-n", "-W", "--args", page_url] + args = ["open", str(app_path), "-n", "-W", "--args", page_url, pid_file] elif is_linux(): # build version-specific path to flet folder temp_flet_dir = Path.home().joinpath(".flet", "bin", f"flet-{version.version}") diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index 46f430546..4c938c53a 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -5,13 +5,12 @@ import time import uuid from dataclasses import dataclass -from typing import Any, Tuple, cast +from typing import Any, Tuple, Union, cast from urllib.parse import urlparse from beartype import beartype from beartype.typing import Dict, List, Optional -from flet import constants from flet.app_bar import AppBar from flet.auth.authorization import Authorization from flet.auth.oauth_provider import OAuthProvider @@ -110,7 +109,10 @@ def __init__(self, conn: Connection, session_id): # authorize/login/logout self.__on_login = EventHandler() - self._add_event_handler("authorize", self.__on_authorize) + self._add_event_handler( + "authorize", + self.__on_authorize if not is_asyncio() else self.__on_authorize_async, + ) self.__on_logout = EventHandler() # route_change @@ -139,11 +141,16 @@ def convert_keyboard_event(e): "keyboard_event", self.__on_keyboard_event.get_handler() ) - self.__method_calls: Dict[str, threading.Event] = {} + self.__method_calls: Dict[str, Union[threading.Event, asyncio.Event]] = {} self.__method_call_results: Dict[ - threading.Event, tuple[Optional[str], Optional[str]] + Union[threading.Event, asyncio.Event], tuple[Optional[str], Optional[str]] ] = {} - self._add_event_handler("invoke_method_result", self._on_invoke_method_result) + self._add_event_handler( + "invoke_method_result", + self.__on_invoke_method_result + if not is_asyncio() + else self.__on_invoke_method_result_async, + ) self.__on_window_event = EventHandler() self._add_event_handler("window_event", self.__on_window_event.get_handler()) @@ -467,7 +474,7 @@ def login( fetch_groups=fetch_groups, scope=scope, ) - if saved_token == None: + if saved_token is None: authorization_url, state = self.__authorization.get_authorization_data() auth_attrs = {"state": state} if complete_page_html: @@ -491,6 +498,50 @@ def login( self.__on_login.get_handler()(LoginEvent(error="", error_description="")) return self.__authorization + async def login_async( + self, + provider: OAuthProvider, + fetch_user=True, + fetch_groups=False, + scope: Optional[List[str]] = None, + saved_token: Optional[str] = None, + on_open_authorization_url=None, + complete_page_html: Optional[str] = None, + redirect_to_page=False, + authorization=Authorization, + ): + self.__authorization = authorization( + provider, + fetch_user=fetch_user, + fetch_groups=fetch_groups, + scope=scope, + ) + if saved_token is None: + authorization_url, state = self.__authorization.get_authorization_data() + auth_attrs = {"state": state} + if complete_page_html: + auth_attrs["completePageHtml"] = complete_page_html + if redirect_to_page: + up = urlparse(provider.redirect_url) + auth_attrs["completePageUrl"] = up._replace( + path=self.__conn.page_name + ).geturl() + result = await self._send_command_async("oauthAuthorize", attrs=auth_attrs) + if result.error != "": + raise Exception(result.error) + if on_open_authorization_url: + await on_open_authorization_url(authorization_url) + else: + await self.launch_url_async( + authorization_url, "flet_oauth_signin", web_popup_window=self.web + ) + else: + await self.__authorization.dehydrate_token_async(saved_token) + await self.__on_login.get_handler()( + LoginEvent(error="", error_description="") + ) + return self.__authorization + def __on_authorize(self, e): assert self.__authorization is not None d = json.loads(e.data) @@ -518,12 +569,45 @@ def __on_authorize(self, e): login_evt.error = str(ex) self.__on_login.get_handler()(login_evt) + async def __on_authorize_async(self, e): + assert self.__authorization is not None + d = json.loads(e.data) + state = d["state"] + assert state == self.__authorization.state + + if not self.web: + if self.platform in ["ios", "android"]: + # close web view on mobile + await self.close_in_app_web_view_async() + else: + # activate desktop window + await self.window_to_front_async() + + login_evt = LoginEvent( + error=d["error"], error_description=d["error_description"] + ) + if login_evt.error == "": + # perform token request + code = d["code"] + assert code not in [None, ""] + try: + await self.__authorization.request_token_async(code) + except Exception as ex: + login_evt.error = str(ex) + await self.__on_login.get_handler()(login_evt) + def logout(self): self.__authorization = None self.__on_logout.get_handler()( ControlEvent(target="page", name="logout", data="", control=self, page=self) ) + async def logout_async(self): + self.__authorization = None + await self.__on_logout.get_handler()( + ControlEvent(target="page", name="logout", data="", control=self, page=self) + ) + def _send_command( self, name: str, @@ -571,6 +655,45 @@ def launch_url( web_popup_window: bool = False, window_width: Optional[int] = None, window_height: Optional[int] = None, + ): + self.invoke_method( + "launchUrl", + self.__get_launch_url_args( + url=url, + web_window_name=web_window_name, + web_popup_window=web_popup_window, + window_width=window_width, + window_height=window_height, + ), + ) + + @beartype + async def launch_url_async( + self, + url: str, + web_window_name: Optional[str] = None, + web_popup_window: bool = False, + window_width: Optional[int] = None, + window_height: Optional[int] = None, + ): + await self.invoke_method_async( + "launchUrl", + self.__get_launch_url_args( + url=url, + web_window_name=web_window_name, + web_popup_window=web_popup_window, + window_width=window_width, + window_height=window_height, + ), + ) + + def __get_launch_url_args( + self, + url: str, + web_window_name: Optional[str] = None, + web_popup_window: bool = False, + window_width: Optional[int] = None, + window_height: Optional[int] = None, ): args = {"url": url} if web_window_name != None: @@ -581,20 +704,35 @@ def launch_url( args["window_width"] = str(window_width) if window_height != None: args["window_height"] = str(window_height) - self.invoke_method("launchUrl", args) + return args @beartype def can_launch_url(self, url: str): args = {"url": url} return self.invoke_method("canLaunchUrl", args, wait_for_result=True) == "true" + @beartype + async def can_launch_url_async(self, url: str): + args = {"url": url} + return ( + await self.invoke_method_async("canLaunchUrl", args, wait_for_result=True) + == "true" + ) + def close_in_app_web_view(self): self.invoke_method("closeInAppWebView") + async def close_in_app_web_view_async(self): + await self.invoke_method_async("closeInAppWebView") + @beartype def window_to_front(self): self.invoke_method("windowToFront") + @beartype + async def window_to_front_async(self): + await self.invoke_method_async("windowToFront") + def invoke_method( self, method_name: str, @@ -637,7 +775,60 @@ def invoke_method( return None return result - def _on_invoke_method_result(self, e): + async def invoke_method_async( + self, + method_name: str, + arguments: Optional[Dict[str, str]] = None, + wait_for_result: bool = False, + ) -> Optional[str]: + method_id = uuid.uuid4().hex + + # register callback + evt: Optional[asyncio.Event] = None + if wait_for_result: + evt = asyncio.Event() + self.__method_calls[method_id] = evt + + # call method + result = await self._send_command_async( + "invokeMethod", values=[method_id, method_name], attrs=arguments + ) + + if result.error != "": + if wait_for_result: + del self.__method_calls[method_id] + raise Exception(result.error) + + if not wait_for_result: + return + + assert evt is not None + + try: + await asyncio.wait_for(evt.wait(), timeout=5) + except TimeoutError: + del self.__method_calls[method_id] + raise Exception( + f"Timeout waiting for invokeMethod {method_name}({arguments}) call" + ) + + result, err = self.__method_call_results.pop(evt) + if err != None: + raise Exception(err) + if result == None: + return None + return result + + def __on_invoke_method_result(self, e): + d = json.loads(e.data) + result = InvokeMethodResults(**d) + evt = self.__method_calls.pop(result.method_id, None) + if evt == None: + return + self.__method_call_results[evt] = (result.result, result.error) + evt.set() + + async def __on_invoke_method_result_async(self, e): d = json.loads(e.data) result = InvokeMethodResults(**d) evt = self.__method_calls.pop(result.method_id, None) From 0a6c4f3e2abbe540ee798e0a09b3928789514a4e Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 5 Jan 2023 11:07:15 -0800 Subject: [PATCH 26/34] All methods async --- package/lib/src/controls/audio.dart | 2 +- sdk/python/flet/audio.py | 88 ++++++++++------------------- sdk/python/flet/callable_control.py | 42 +++++++++++++- sdk/python/flet/clipboard.py | 6 ++ sdk/python/flet/haptic_feedback.py | 15 ++++- sdk/python/flet/page.py | 47 +++++++++------ sdk/python/flet/utils.py | 4 ++ 7 files changed, 124 insertions(+), 80 deletions(-) diff --git a/package/lib/src/controls/audio.dart b/package/lib/src/controls/audio.dart index 9a872d0b3..52d206728 100644 --- a/package/lib/src/controls/audio.dart +++ b/package/lib/src/controls/audio.dart @@ -182,7 +182,7 @@ class _AudioControlState extends State { sendResult(Object? result, String? error) { ws.pageEventFromWeb( eventTarget: widget.control.id, - eventName: "result", + eventName: "method_result", eventData: json.encode({ "i": i, "r": result != null ? json.encode(result) : null, diff --git a/sdk/python/flet/audio.py b/sdk/python/flet/audio.py index 044ad285b..1e6725754 100644 --- a/sdk/python/flet/audio.py +++ b/sdk/python/flet/audio.py @@ -1,12 +1,10 @@ -import dataclasses -import json -import threading from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, Optional from beartype import beartype +from flet.callable_control import CallableControl -from flet.control import Control, OptionalNumber +from flet.control import OptionalNumber from flet.ref import Ref @@ -16,21 +14,7 @@ class ReleaseMode(Enum): STOP = "stop" -@dataclasses.dataclass -class AudioMethodCall: - i: int - n: str - p: List[str] - - -@dataclasses.dataclass -class AudioMethodResults: - i: int - r: Optional[str] - e: Optional[str] - - -class Audio(Control): +class Audio(CallableControl): """ A control to simultaneously play multiple audio files. Works on macOS, Linux, Windows, iOS, Android and web. Based on audioplayers Flutter widget (https://pub.dev/packages/audioplayers). @@ -57,6 +41,7 @@ def main(page: ft.Page): Online docs: https://flet.dev/docs/controls/audio """ + def __init__( self, src: Optional[str] = None, @@ -76,16 +61,12 @@ def __init__( on_seek_complete=None, ): - Control.__init__( + CallableControl.__init__( self, ref=ref, data=data, ) - self.__call_counter = 0 - self.__calls: Dict[int, threading.Event] = {} - self.__results: Dict[threading.Event, tuple[Optional[str], Optional[str]]] = {} - self._add_event_handler("result", self._on_result) self.src = src self.src_base64 = src_base64 self.autoplay = autoplay @@ -105,61 +86,52 @@ def _get_control_name(self): def play(self): self._call_method("play", params=[], wait_for_result=False) + async def play_async(self): + await self._call_method_async("play", params=[], wait_for_result=False) + def pause(self): self._call_method("pause", params=[], wait_for_result=False) + async def pause_async(self): + await self._call_method_async("pause", params=[], wait_for_result=False) + def resume(self): self._call_method("resume", params=[], wait_for_result=False) + async def resume_async(self): + await self._call_method_async("resume", params=[], wait_for_result=False) + def release(self): self._call_method("release", params=[], wait_for_result=False) + async def release_async(self): + await self._call_method_async("release", params=[], wait_for_result=False) + def seek(self, position_milliseconds: int): self._call_method( "seek", params=[str(position_milliseconds)], wait_for_result=False ) + async def seek_async(self, position_milliseconds: int): + await self._call_method_async( + "seek", params=[str(position_milliseconds)], wait_for_result=False + ) + def get_duration(self) -> Optional[int]: sr = self._call_method("get_duration", []) return int(sr) if sr else None + async def get_duration_async(self) -> Optional[int]: + sr = await self._call_method_async("get_duration", []) + return int(sr) if sr else None + def get_current_position(self) -> Optional[int]: sr = self._call_method("get_current_position", []) return int(sr) if sr else None - def _call_method(self, name: str, params: List[str], wait_for_result=True) -> Any: - m = AudioMethodCall(i=self.__call_counter, n=name, p=params) - self.__call_counter += 1 - self._set_attr_json("method", m) - - evt: Optional[threading.Event] = None - if wait_for_result: - evt = threading.Event() - self.__calls[m.i] = evt - self.update() - - if not wait_for_result: - return - - assert evt is not None - if not evt.wait(5): - del self.__calls[m.i] - raise Exception(f"Timeout waiting for Audio.{name}({params}) method call") - result, err = self.__results.pop(evt) - if err != None: - raise Exception(err) - if result == None: - return None - return json.loads(result) - - def _on_result(self, e): - d = json.loads(e.data) - result = AudioMethodResults(**d) - evt = self.__calls.pop(result.i, None) - if evt == None: - return - self.__results[evt] = (result.r, result.e) - evt.set() + async def get_current_position_async(self) -> Optional[int]: + sr = await self._call_method_async("get_current_position", []) + return int(sr) if sr else None # src @property diff --git a/sdk/python/flet/callable_control.py b/sdk/python/flet/callable_control.py index 8daccc3dd..e66abc013 100644 --- a/sdk/python/flet/callable_control.py +++ b/sdk/python/flet/callable_control.py @@ -1,7 +1,8 @@ +import asyncio import dataclasses import json import threading -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Union from beartype import beartype @@ -37,8 +38,10 @@ def __init__( ) self.__call_counter = 0 - self.__calls: Dict[int, threading.Event] = {} - self.__results: Dict[threading.Event, tuple[Optional[str], Optional[str]]] = {} + self.__calls: Dict[int, Union[threading.Event, asyncio.Event]] = {} + self.__results: Dict[ + Union[threading.Event, asyncio.Event], tuple[Optional[str], Optional[str]] + ] = {} self._add_event_handler("method_result", self._on_result) def _call_method(self, name: str, params: List[str], wait_for_result=True) -> Any: @@ -68,6 +71,39 @@ def _call_method(self, name: str, params: List[str], wait_for_result=True) -> An return None return json.loads(result) + async def _call_method_async( + self, name: str, params: List[str], wait_for_result=True + ) -> Any: + m = ControlMethodCall(i=self.__call_counter, n=name, p=params) + self.__call_counter += 1 + self._set_attr_json("method", m) + + evt: Optional[asyncio.Event] = None + if wait_for_result: + evt = asyncio.Event() + self.__calls[m.i] = evt + await self.update_async() + + if not wait_for_result: + return + + assert evt is not None + + try: + await asyncio.wait_for(evt.wait(), timeout=5) + except TimeoutError: + del self.__calls[m.i] + raise Exception( + f"Timeout waiting for {self.__class__.__name__}.{name}({params}) method call" + ) + + result, err = self.__results.pop(evt) + if err != None: + raise Exception(err) + if result == None: + return None + return json.loads(result) + def _on_result(self, e): d = json.loads(e.data) result = ControlMethodResults(**d) diff --git a/sdk/python/flet/clipboard.py b/sdk/python/flet/clipboard.py index ffd0d87bf..2fad832d3 100644 --- a/sdk/python/flet/clipboard.py +++ b/sdk/python/flet/clipboard.py @@ -36,5 +36,11 @@ def _is_isolated(self): def set_data(self, data: str): self._call_method("set_data", [data], wait_for_result=False) + async def set_data_async(self, data: str): + await self._call_method_async("set_data", [data], wait_for_result=False) + def get_data(self) -> str: return self._call_method("get_data", []) + + async def get_data_async(self) -> str: + return await self._call_method_async("get_data", []) diff --git a/sdk/python/flet/haptic_feedback.py b/sdk/python/flet/haptic_feedback.py index c85e9de9e..df1e343aa 100644 --- a/sdk/python/flet/haptic_feedback.py +++ b/sdk/python/flet/haptic_feedback.py @@ -11,7 +11,7 @@ class HapticFeedback(CallableControl): Allows access to the haptic feedback interface on the device. It is non-visual and should be added to `page.overlay` list. - + Example: ``` import flet as ft @@ -34,6 +34,7 @@ def main(page: ft.Page): Online docs: https://flet.dev/docs/controls/hapticfeedback """ + def __init__( self, ref: Optional[Ref] = None, @@ -55,11 +56,23 @@ def _is_isolated(self): def heavy_impact(self): self._call_method("heavy_impact", [], wait_for_result=False) + async def heavy_impact_async(self): + await self._call_method_async("heavy_impact", [], wait_for_result=False) + def light_impact(self): self._call_method("light_impact", [], wait_for_result=False) + async def light_impact_async(self): + await self._call_method_async("light_impact", [], wait_for_result=False) + def medium_impact(self): self._call_method("medium_impact", [], wait_for_result=False) + async def medium_impact_async(self): + await self._call_method_async("medium_impact", [], wait_for_result=False) + def vibrate(self): self._call_method("vibrate", [], wait_for_result=False) + + async def vibrate_async(self): + await self._call_method_async("vibrate", [], wait_for_result=False) diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index 4c938c53a..4d093e4d3 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -40,7 +40,7 @@ ThemeMode, ThemeModeString, ) -from flet.utils import is_asyncio +from flet.utils import is_asyncio, is_coroutine from flet.view import View try: @@ -145,12 +145,7 @@ def convert_keyboard_event(e): self.__method_call_results: Dict[ Union[threading.Event, asyncio.Event], tuple[Optional[str], Optional[str]] ] = {} - self._add_event_handler( - "invoke_method_result", - self.__on_invoke_method_result - if not is_asyncio() - else self.__on_invoke_method_result_async, - ) + self._add_event_handler("invoke_method_result", self.__on_invoke_method_result) self.__on_window_event = EventHandler() self._add_event_handler("window_event", self.__on_window_event.get_handler()) @@ -400,7 +395,10 @@ async def on_event_async(self, e: Event): ce = ControlEvent(e.target, e.name, e.data, self._index[e.target], self) handler = self._index[e.target].event_handlers.get(e.name) if handler: - await handler(ce) + if is_coroutine(handler): + await handler(ce) + else: + handler(ce) def __on_page_change_event(self, data): for props in json.loads(data): @@ -644,9 +642,16 @@ async def _send_command_async( def set_clipboard(self, value: str): self.__offstage.clipboard.set_data(value) + @beartype + async def set_clipboard_async(self, value: str): + await self.__offstage.clipboard.set_data_async(value) + def get_clipboard(self): return self.__offstage.clipboard.get_data() + async def get_clipboard_async(self): + return await self.__offstage.clipboard.get_data_async() + @beartype def launch_url( self, @@ -828,32 +833,40 @@ def __on_invoke_method_result(self, e): self.__method_call_results[evt] = (result.result, result.error) evt.set() - async def __on_invoke_method_result_async(self, e): - d = json.loads(e.data) - result = InvokeMethodResults(**d) - evt = self.__method_calls.pop(result.method_id, None) - if evt == None: - return - self.__method_call_results[evt] = (result.result, result.error) - evt.set() - @beartype def show_snack_bar(self, snack_bar: SnackBar): self.__offstage.snack_bar = snack_bar self.__offstage.update() + @beartype + async def show_snack_bar_async(self, snack_bar: SnackBar): + self.__offstage.snack_bar = snack_bar + await self.__offstage.update_async() + def window_destroy(self): self._set_attr("windowDestroy", "true") self.update() + async def window_destroy_async(self): + self._set_attr("windowDestroy", "true") + await self.update_async() + def window_center(self): self._set_attr("windowCenter", str(time.time())) self.update() + async def window_center_async(self): + self._set_attr("windowCenter", str(time.time())) + await self.update_async() + def window_close(self): self._set_attr("windowClose", str(time.time())) self.update() + async def window_close_async(self): + self._set_attr("windowClose", str(time.time())) + await self.update_async() + # QueryString @property def query(self): diff --git a/sdk/python/flet/utils.py b/sdk/python/flet/utils.py index cdb2ed428..80efe768b 100644 --- a/sdk/python/flet/utils.py +++ b/sdk/python/flet/utils.py @@ -74,6 +74,10 @@ def is_asyncio(): return False +def is_coroutine(method): + return inspect.iscoroutinefunction(method) + + # https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python def which(program, exclude_exe=None): import os From 8df3071b3353091546f697715520cbf727e49252 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 5 Jan 2023 14:05:04 -0800 Subject: [PATCH 27/34] sync/async asserts --- sdk/python/flet/page.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index 4d093e4d3..043039d43 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -224,6 +224,7 @@ def __set_page_details(self, values): self._set_attr("windowLeft", values[9], False) def update(self, *controls): + assert self.__lock, "Sync method calls are not supported in async app." with self.__lock: if len(controls) == 0: r = self.__update(self) @@ -232,6 +233,9 @@ def update(self, *controls): self.__handle_mount_unmount(*r) async def update_async(self, *controls): + assert ( + self.__async_lock + ), "Async method calls are not supported in a regular app." async with self.__async_lock: if len(controls) == 0: r = await self.__update_async(self) @@ -240,18 +244,23 @@ async def update_async(self, *controls): await self.__handle_mount_unmount_async(*r) def add(self, *controls): + assert self.__lock, "Sync method calls are not supported in async app." with self.__lock: self._controls.extend(controls) r = self.__update(self) self.__handle_mount_unmount(*r) async def add_async(self, *controls): + assert ( + self.__async_lock + ), "Async method calls are not supported in a regular app." async with self.__async_lock: self._controls.extend(controls) r = await self.__update_async(self) await self.__handle_mount_unmount_async(*r) def insert(self, at, *controls): + assert self.__lock, "Sync method calls are not supported in async app." with self.__lock: n = at for control in controls: @@ -261,6 +270,9 @@ def insert(self, at, *controls): self.__handle_mount_unmount(*r) async def insert_async(self, at, *controls): + assert ( + self.__async_lock + ), "Async method calls are not supported in a regular app." async with self.__async_lock: n = at for control in controls: @@ -270,6 +282,7 @@ async def insert_async(self, at, *controls): await self.__handle_mount_unmount_async(*r) def remove(self, *controls): + assert self.__lock, "Sync method calls are not supported in async app." with self.__lock: for control in controls: self._controls.remove(control) @@ -277,6 +290,9 @@ def remove(self, *controls): self.__handle_mount_unmount(*r) async def remove_async(self, *controls): + assert ( + self.__async_lock + ), "Async method calls are not supported in a regular app." async with self.__async_lock: for control in controls: self._controls.remove(control) @@ -284,24 +300,32 @@ async def remove_async(self, *controls): await self.__handle_mount_unmount_async(*r) def remove_at(self, index): + assert self.__lock, "Sync method calls are not supported in async app." with self.__lock: self._controls.pop(index) r = self.__update(self) self.__handle_mount_unmount(*r) async def remove_at_async(self, index): + assert ( + self.__async_lock + ), "Async method calls are not supported in a regular app." async with self.__async_lock: self._controls.pop(index) r = await self.__update_async(self) await self.__handle_mount_unmount_async(*r) def clean(self): + assert self.__lock, "Sync method calls are not supported in async app." with self.__lock: self._controls.clear() r = self.__update(self) self.__handle_mount_unmount(*r) async def clean_async(self): + assert ( + self.__async_lock + ), "Async method calls are not supported in a regular app." async with self.__async_lock: self._controls.clear() r = await self.__update_async(self) @@ -363,16 +387,18 @@ async def __handle_mount_unmount_async(self, added_controls, removed_controls): await ctrl.did_mount_async() def error(self, message=""): + assert self.__lock, "Sync method calls are not supported in async app." with self.__lock: self._send_command("error", [message]) async def error_async(self, message=""): + assert self.__async_lock, "Async method calls are not supported in a regular app." async with self.__async_lock: await self._send_command_async("error", [message]) def on_event(self, e: Event): logging.info(f"page.on_event: {e.target} {e.name} {e.data}") - + assert self.__lock, "Sync method calls are not supported in async app." with self.__lock: if e.target == "page" and e.name == "change": self.__on_page_change_event(e.data) From 102481d76e1941db160cef5d823cb77cf988d57c Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Thu, 5 Jan 2023 14:44:16 -0800 Subject: [PATCH 28/34] focus_async --- sdk/python/flet/dropdown.py | 4 ++++ sdk/python/flet/textfield.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/sdk/python/flet/dropdown.py b/sdk/python/flet/dropdown.py index 96f9b2f60..1dd9f43ed 100644 --- a/sdk/python/flet/dropdown.py +++ b/sdk/python/flet/dropdown.py @@ -210,6 +210,10 @@ def focus(self): self._set_attr_json("focus", FocusData()) self.update() + async def focus_async(self): + self._set_attr_json("focus", FocusData()) + await self.update_async() + # options @property def options(self): diff --git a/sdk/python/flet/textfield.py b/sdk/python/flet/textfield.py index 01bf2a9a4..02167b429 100644 --- a/sdk/python/flet/textfield.py +++ b/sdk/python/flet/textfield.py @@ -273,6 +273,10 @@ def focus(self): self._set_attr_json("focus", FocusData()) self.update() + async def focus_async(self): + self._set_attr_json("focus", FocusData()) + await self.update_async() + # value @property def value(self) -> Optional[str]: From 870a76a94254242b1e08197965b5898a9dfadc8d Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 6 Jan 2023 09:43:23 -0800 Subject: [PATCH 29/34] PubSub async --- sdk/python/flet/pubsub.py | 121 ++++++++++++++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 11 deletions(-) diff --git a/sdk/python/flet/pubsub.py b/sdk/python/flet/pubsub.py index 632016269..8b9bce161 100644 --- a/sdk/python/flet/pubsub.py +++ b/sdk/python/flet/pubsub.py @@ -1,11 +1,15 @@ +import asyncio import logging import threading -from typing import Any, Callable, Dict, Iterable +from typing import Any, Callable, Coroutine, Dict, Iterable + +from flet.utils import is_asyncio class PubSubHub: def __init__(self): self.__lock = threading.Lock() + self.__async_lock = asyncio.Lock() self.__subscribers: Dict[str, Callable] = {} # key: session_id, value: handler self.__topic_subscribers: Dict[ str, Dict[str, Callable] @@ -20,6 +24,12 @@ def send_all(self, message: Any): for handler in self.__subscribers.values(): self.__send(handler, [message]) + async def send_all_async(self, message: Any): + logging.debug(f"pubsub.send_all_async({message})") + async with self.__async_lock: + for handler in self.__subscribers.values(): + await self.__send_async(handler, [message]) + def send_all_on_topic(self, topic: str, message: Any): logging.debug(f"pubsub.send_all_on_topic({topic}, {message})") with self.__lock: @@ -27,6 +37,13 @@ def send_all_on_topic(self, topic: str, message: Any): for handler in self.__topic_subscribers[topic].values(): self.__send(handler, [topic, message]) + async def send_all_on_topic_async(self, topic: str, message: Any): + logging.debug(f"pubsub.send_all_on_topic_async({topic}, {message})") + async with self.__async_lock: + if topic in self.__topic_subscribers: + for handler in self.__topic_subscribers[topic].values(): + await self.__send_async(handler, [topic, message]) + def send_others(self, except_session_id: str, message: Any): logging.debug(f"pubsub.send_others({except_session_id}, {message})") with self.__lock: @@ -34,6 +51,13 @@ def send_others(self, except_session_id: str, message: Any): if except_session_id != session_id: self.__send(handler, [message]) + async def send_others_async(self, except_session_id: str, message: Any): + logging.debug(f"pubsub.send_others_async({except_session_id}, {message})") + async with self.__async_lock: + for session_id, handler in self.__subscribers.items(): + if except_session_id != session_id: + await self.__send_async(handler, [message]) + def send_others_on_topic(self, except_session_id: str, topic: str, message: Any): logging.debug( f"pubsub.send_others_on_topic({except_session_id}, {topic}, {message})" @@ -44,35 +68,70 @@ def send_others_on_topic(self, except_session_id: str, topic: str, message: Any) if except_session_id != session_id: self.__send(handler, [topic, message]) + async def send_others_on_topic_async( + self, except_session_id: str, topic: str, message: Any + ): + logging.debug( + f"pubsub.send_others_on_topic_async({except_session_id}, {topic}, {message})" + ) + async with self.__async_lock: + if topic in self.__topic_subscribers: + for session_id, handler in self.__topic_subscribers[topic].items(): + if except_session_id != session_id: + await self.__send_async(handler, [topic, message]) + def subscribe(self, session_id: str, handler: Callable): logging.debug(f"pubsub.subscribe({session_id})") with self.__lock: self.__subscribers[session_id] = handler + async def subscribe_async(self, session_id: str, handler): + logging.debug(f"pubsub.subscribe_async({session_id})") + async with self.__async_lock: + self.__subscribers[session_id] = handler + def subscribe_topic(self, session_id: str, topic: str, handler: Callable): logging.debug(f"pubsub.subscribe_topic({session_id}, {topic})") with self.__lock: - topic_subscribers = self.__topic_subscribers.get(topic) - if topic_subscribers is None: - topic_subscribers = {} - self.__topic_subscribers[topic] = topic_subscribers - topic_subscribers[session_id] = handler - subscriber_topics = self.__subscriber_topics.get(session_id) - if subscriber_topics is None: - subscriber_topics = {} - self.__subscriber_topics[session_id] = subscriber_topics - subscriber_topics[topic] = handler + self.__subscribe_topic(session_id, topic, handler) + + async def subscribe_topic_async(self, session_id: str, topic: str, handler): + logging.debug(f"pubsub.subscribe_topic_async({session_id}, {topic})") + async with self.__async_lock: + self.__subscribe_topic(session_id, topic, handler) + + def __subscribe_topic(self, session_id: str, topic: str, handler): + topic_subscribers = self.__topic_subscribers.get(topic) + if topic_subscribers is None: + topic_subscribers = {} + self.__topic_subscribers[topic] = topic_subscribers + topic_subscribers[session_id] = handler + subscriber_topics = self.__subscriber_topics.get(session_id) + if subscriber_topics is None: + subscriber_topics = {} + self.__subscriber_topics[session_id] = subscriber_topics + subscriber_topics[topic] = handler def unsubscribe(self, session_id: str): logging.debug(f"pubsub.unsubscribe({session_id})") with self.__lock: self.__unsubscribe(session_id) + async def unsubscribe_async(self, session_id: str): + logging.debug(f"pubsub.unsubscribe_async({session_id})") + async with self.__async_lock: + self.__unsubscribe(session_id) + def unsubscribe_topic(self, session_id: str, topic: str): logging.debug(f"pubsub.unsubscribe({session_id}, {topic})") with self.__lock: self.__unsubscribe_topic(session_id, topic) + async def unsubscribe_topic_async(self, session_id: str, topic: str): + logging.debug(f"pubsub.unsubscribe_topic_async({session_id}, {topic})") + async with self.__async_lock: + self.__unsubscribe_topic(session_id, topic) + def unsubscribe_all(self, session_id: str): logging.debug(f"pubsub.unsubscribe_all({session_id})") with self.__lock: @@ -81,6 +140,14 @@ def unsubscribe_all(self, session_id: str): for topic in self.__subscriber_topics[session_id].keys(): self.__unsubscribe_topic(session_id, topic) + async def unsubscribe_all_async(self, session_id: str): + logging.debug(f"pubsub.unsubscribe_all_async({session_id})") + async with self.__async_lock: + self.__unsubscribe(session_id) + if session_id in self.__subscriber_topics: + for topic in self.__subscriber_topics[session_id].keys(): + self.__unsubscribe_topic(session_id, topic) + def __unsubscribe(self, session_id: str): logging.debug(f"pubsub.__unsubscribe({session_id})") self.__subscribers.pop(session_id) @@ -106,6 +173,9 @@ def __send(self, handler: Callable, args: Iterable): ) th.start() + async def __send_async(self, handler, args): + asyncio.create_task(handler(**args)) + class PubSub: def __init__(self, pubsub: PubSubHub, session_id: str): @@ -115,26 +185,55 @@ def __init__(self, pubsub: PubSubHub, session_id: str): def send_all(self, message: Any): self.__pubsub.send_all(message) + async def send_all_async(self, message: Any): + await self.__pubsub.send_all_async(message) + def send_all_on_topic(self, topic: str, message: Any): self.__pubsub.send_all_on_topic(topic, message) + async def send_all_on_topic_async(self, topic: str, message: Any): + await self.__pubsub.send_all_on_topic_async(topic, message) + def send_others(self, message: Any): self.__pubsub.send_others(self.__session_id, message) + async def send_others_async(self, message: Any): + await self.__pubsub.send_others_async(self.__session_id, message) + def send_others_on_topic(self, topic: str, message: Any): self.__pubsub.send_others_on_topic(self.__session_id, topic, message) + async def send_others_on_topic_async(self, topic: str, message: Any): + await self.__pubsub.send_others_on_topic_async( + self.__session_id, topic, message + ) + def subscribe(self, handler: Callable): self.__pubsub.subscribe(self.__session_id, handler) + async def subscribe_async(self, handler: Callable): + await self.__pubsub.subscribe_async(self.__session_id, handler) + def subscribe_topic(self, topic: str, handler: Callable): self.__pubsub.subscribe_topic(self.__session_id, topic, handler) + async def subscribe_topic_async(self, topic: str, handler: Callable): + await self.__pubsub.subscribe_topic_async(self.__session_id, topic, handler) + def unsubscribe(self): self.__pubsub.unsubscribe(self.__session_id) + async def unsubscribe_async(self): + await self.__pubsub.unsubscribe_async(self.__session_id) + def unsubscribe_topic(self, topic: str): self.__pubsub.unsubscribe_topic(self.__session_id, topic) + async def unsubscribe_topic_async(self, topic: str): + await self.__pubsub.unsubscribe_topic_async(self.__session_id, topic) + def unsubscribe_all(self): self.__pubsub.unsubscribe_all(self.__session_id) + + async def unsubscribe_all_async(self): + await self.__pubsub.unsubscribe_all_async(self.__session_id) From 3bd5a6ec86452f446f2e18486aeac6cef1b9d9e6 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 6 Jan 2023 09:47:38 -0800 Subject: [PATCH 30/34] ClientStorage async --- sdk/python/flet/client_storage.py | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/sdk/python/flet/client_storage.py b/sdk/python/flet/client_storage.py index 90c07b93a..1df41f342 100644 --- a/sdk/python/flet/client_storage.py +++ b/sdk/python/flet/client_storage.py @@ -16,6 +16,15 @@ def set(self, key: str, value: Any) -> bool: == "true" ) + async def set_async(self, key: str, value: Any) -> bool: + jv = self.__page._convert_attr_json(value) + assert jv is not None + return ( + await self.__page.invoke_method_async( + "clientStorage:set", {"key": key, "value": jv}, wait_for_result=True + ) + ) == "true" + def get(self, key: str): jv = self.__page.invoke_method( "clientStorage:get", {"key": key}, wait_for_result=True @@ -24,6 +33,14 @@ def get(self, key: str): return json.loads(json.loads(jv)) return None + async def get_async(self, key: str): + jv = await self.__page.invoke_method_async( + "clientStorage:get", {"key": key}, wait_for_result=True + ) + if jv: + return json.loads(json.loads(jv)) + return None + def contains_key(self, key: str) -> bool: return ( self.__page.invoke_method( @@ -32,6 +49,14 @@ def contains_key(self, key: str) -> bool: == "true" ) + async def contains_key_async(self, key: str) -> bool: + return ( + await self.__page.invoke_method_async( + "clientStorage:containskey", {"key": key}, wait_for_result=True + ) + == "true" + ) + def remove(self, key: str) -> bool: return ( self.__page.invoke_method( @@ -40,6 +65,13 @@ def remove(self, key: str) -> bool: == "true" ) + async def remove_async(self, key: str) -> bool: + return ( + await self.__page.invoke_method_async( + "clientStorage:remove", {"key": key}, wait_for_result=True + ) + ) == "true" + def get_keys(self, key_prefix: str) -> List[str]: jr = self.__page.invoke_method( "clientStorage:getkeys", {"key_prefix": key_prefix}, wait_for_result=True @@ -47,8 +79,23 @@ def get_keys(self, key_prefix: str) -> List[str]: assert jr is not None return json.loads(jr) + async def get_keys_async(self, key_prefix: str) -> List[str]: + jr = await self.__page.invoke_method_async( + "clientStorage:getkeys", {"key_prefix": key_prefix}, wait_for_result=True + ) + assert jr is not None + return json.loads(jr) + def clear(self) -> bool: return ( self.__page.invoke_method("clientStorage:clear", wait_for_result=True) == "true" ) + + async def clear_async(self) -> bool: + return ( + await self.__page.invoke_method_async( + "clientStorage:clear", wait_for_result=True + ) + == "true" + ) From 8514aeda0cd4d7bf3cd392162e2176cbc9fa0ebe Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 6 Jan 2023 09:50:29 -0800 Subject: [PATCH 31/34] FilePicker async --- sdk/python/flet/file_picker.py | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/sdk/python/flet/file_picker.py b/sdk/python/flet/file_picker.py index 42c21db5d..b22130f13 100644 --- a/sdk/python/flet/file_picker.py +++ b/sdk/python/flet/file_picker.py @@ -171,6 +171,22 @@ def pick_files( self.allow_multiple = allow_multiple self.update() + async def pick_files_async( + self, + dialog_title: Optional[str] = None, + initial_directory: Optional[str] = None, + file_type: FilePickerFileType = FilePickerFileType.ANY, + allowed_extensions: Optional[List[str]] = None, + allow_multiple: Optional[bool] = False, + ): + self.state = "pickFiles" + self.dialog_title = dialog_title + self.initial_directory = initial_directory + self.file_type = file_type + self.allowed_extensions = allowed_extensions + self.allow_multiple = allow_multiple + await self.update_async() + def save_file( self, dialog_title: Optional[str] = None, @@ -187,6 +203,22 @@ def save_file( self.allowed_extensions = allowed_extensions self.update() + async def save_file_async( + self, + dialog_title: Optional[str] = None, + file_name: Optional[str] = None, + initial_directory: Optional[str] = None, + file_type: FilePickerFileType = FilePickerFileType.ANY, + allowed_extensions: Optional[List[str]] = None, + ): + self.state = "saveFile" + self.dialog_title = dialog_title + self.file_name = file_name + self.initial_directory = initial_directory + self.file_type = file_type + self.allowed_extensions = allowed_extensions + await self.update_async() + def get_directory_path( self, dialog_title: Optional[str] = None, @@ -197,10 +229,24 @@ def get_directory_path( self.initial_directory = initial_directory self.update() + async def get_directory_path_async( + self, + dialog_title: Optional[str] = None, + initial_directory: Optional[str] = None, + ): + self.state = "getDirectoryPath" + self.dialog_title = dialog_title + self.initial_directory = initial_directory + await self.update_async() + def upload(self, files: List[FilePickerUploadFile]): self.__upload = files self.update() + async def upload_async(self, files: List[FilePickerUploadFile]): + self.__upload = files + await self.update_async() + # state @property def state(self) -> Optional[FilePickerState]: From 8bf419c738d00e5ffcb855f3453be4c373132ea4 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 6 Jan 2023 11:19:18 -0800 Subject: [PATCH 32/34] Fix OAuth authorization flow --- sdk/python/flet/auth/authorization.py | 34 +++++++++++++------ .../auth/providers/github_oauth_provider.py | 34 ++++++++++++++----- sdk/python/flet/page.py | 4 ++- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/sdk/python/flet/auth/authorization.py b/sdk/python/flet/auth/authorization.py index 0137ed0c7..1e6272ca4 100644 --- a/sdk/python/flet/auth/authorization.py +++ b/sdk/python/flet/auth/authorization.py @@ -14,6 +14,8 @@ from flet.auth.oauth_token import OAuthToken from flet.auth.user import User +from flet.version import version + class Authorization: def __init__( @@ -29,8 +31,8 @@ def __init__( self.provider = provider self.__token: Optional[OAuthToken] = None self.user: Optional[User] = None - self.__lock = threading.Lock() if not is_asyncio() else None - self.__async_lock = asyncio.Lock() if is_asyncio() else None + self.__lock = threading.Lock() + self.__async_lock = asyncio.Lock() # fix scopes self.scope.extend(self.provider.scopes) @@ -80,8 +82,9 @@ def get_authorization_data(self) -> Tuple[str, str]: def request_token(self, code: str): req = self.__get_request_token_request(code) - with httpx.Client() as client: + with httpx.Client(follow_redirects=True) as client: resp = client.send(req) + resp.raise_for_status() client = WebApplicationClient(self.provider.client_id) t = client.parse_request_body_response(resp.text) self.__token = self.__convert_token(t) @@ -89,8 +92,9 @@ def request_token(self, code: str): async def request_token_async(self, code: str): req = self.__get_request_token_request(code) - async with httpx.AsyncClient() as client: + async with httpx.AsyncClient(follow_redirects=True) as client: resp = await client.send(req) + resp.raise_for_status() client = WebApplicationClient(self.provider.client_id) t = client.parse_request_body_response(resp.text) self.__token = self.__convert_token(t) @@ -104,7 +108,8 @@ def __get_request_token_request(self, code: str): client_id=self.provider.client_id, client_secret=self.provider.client_secret, ) - headers = {"content-type": "application/x-www-form-urlencoded"} + headers = self.__get_default_headers() + headers["content-type"] = "application/x-www-form-urlencoded" return httpx.Request( "POST", self.provider.token_endpoint, content=data, headers=headers ) @@ -152,14 +157,14 @@ def __convert_token(self, t: OAuth2Token): def __refresh_token(self): refresh_req = self.__get_refresh_token_request() if refresh_req: - with httpx.Client() as client: + with httpx.Client(follow_redirects=True) as client: refresh_resp = client.send(refresh_req) self.__complete_refresh_token_request(refresh_resp) async def __refresh_token_async(self): refresh_req = self.__get_refresh_token_request() if refresh_req: - async with httpx.AsyncClient() as client: + async with httpx.AsyncClient(follow_redirects=True) as client: refresh_resp = await client.send(refresh_req) self.__complete_refresh_token_request(refresh_resp) @@ -180,12 +185,14 @@ def __get_refresh_token_request(self): refresh_token=self.__token.refresh_token, redirect_uri=self.provider.redirect_url, ) - headers = {"content-type": "application/x-www-form-urlencoded"} + headers = self.__get_default_headers() + headers["content-type"] = "application/x-www-form-urlencoded" return httpx.Request( "POST", url=self.provider.token_endpoint, content=data, headers=headers ) def __complete_refresh_token_request(self, refresh_resp): + refresh_resp.raise_for_status() assert self.__token is not None client = WebApplicationClient(self.provider.client_id) t = client.parse_request_body_response(refresh_resp.text) @@ -201,17 +208,24 @@ def __get_user(self): async def __get_user_async(self): user_req = self.__get_user_request() - async with httpx.AsyncClient() as client: + async with httpx.AsyncClient(follow_redirects=True) as client: user_resp = await client.send(user_req) return self.__complete_user_request(user_resp) def __get_user_request(self): assert self.token is not None assert self.provider.user_endpoint is not None - headers = {"Authorization": "Bearer {}".format(self.token.access_token)} + headers = self.__get_default_headers() + headers["Authorization"] = "Bearer {}".format(self.token.access_token) return httpx.Request("GET", self.provider.user_endpoint, headers=headers) def __complete_user_request(self, user_resp): + user_resp.raise_for_status() assert self.provider.user_id_fn is not None uj = json.loads(user_resp.text) return User(uj, str(self.provider.user_id_fn(uj))) + + def __get_default_headers(self): + return { + "User-Agent": "Flet/{}".format(version), + } diff --git a/sdk/python/flet/auth/providers/github_oauth_provider.py b/sdk/python/flet/auth/providers/github_oauth_provider.py index 24029745f..ad3083ad1 100644 --- a/sdk/python/flet/auth/providers/github_oauth_provider.py +++ b/sdk/python/flet/auth/providers/github_oauth_provider.py @@ -7,6 +7,8 @@ from flet.auth.oauth_provider import OAuthProvider from flet.auth.user import User +from flet.version import version + class GitHubOAuthProvider(OAuthProvider): def __init__(self, client_id: str, client_secret: str, redirect_url: str) -> None: @@ -21,12 +23,12 @@ def __init__(self, client_id: str, client_secret: str, redirect_url: str) -> Non ) def _fetch_groups(self, access_token: str) -> List[Group]: - with httpx.Client() as client: + with httpx.Client(follow_redirects=True) as client: teams_resp = client.send(self.__get_user_teams_request(access_token)) return self.__complete_fetch_groups(teams_resp) async def _fetch_groups_async(self, access_token: str) -> List[Group]: - async with httpx.AsyncClient() as client: + async with httpx.AsyncClient(follow_redirects=True) as client: teams_resp = await client.send(self.__get_user_teams_request(access_token)) return self.__complete_fetch_groups(teams_resp) @@ -34,10 +36,11 @@ def __get_user_teams_request(self, access_token): return httpx.Request( "GET", "https://api.github.com/user/teams", - headers={"Authorization": "Bearer {}".format(access_token)}, + headers=self.__get_client_headers(access_token), ) def __complete_fetch_groups(self, teams_resp): + teams_resp.raise_for_status() groups = [] tj = json.loads(teams_resp.text) for t in tj: @@ -51,26 +54,35 @@ def __complete_fetch_groups(self, teams_resp): def _fetch_user(self, access_token: str) -> Optional[User]: user_req, emails_req = self.__get_user_details_requests(access_token) - with httpx.Client() as client: + with httpx.Client(follow_redirects=True) as client: user_resp = client.send(user_req) emails_resp = client.send(emails_req) return self.__complete_fetch_user_details(user_resp, emails_resp) async def _fetch_user_async(self, access_token: str) -> Optional[User]: user_req, emails_req = self.__get_user_details_requests(access_token) - async with httpx.AsyncClient() as client: + async with httpx.AsyncClient(follow_redirects=True) as client: user_resp = await client.send(user_req) emails_resp = await client.send(emails_req) return self.__complete_fetch_user_details(user_resp, emails_resp) def __get_user_details_requests(self, access_token): - headers = {"Authorization": "Bearer {}".format(access_token)} return ( - httpx.Request("GET", "https://api.github.com/user", headers=headers), - httpx.Request("GET", "https://api.github.com/user/emails", headers=headers), + httpx.Request( + "GET", + "https://api.github.com/user", + headers=self.__get_client_headers(access_token), + ), + httpx.Request( + "GET", + "https://api.github.com/user/emails", + headers=self.__get_client_headers(access_token), + ), ) def __complete_fetch_user_details(self, user_resp, emails_resp): + user_resp.raise_for_status() + emails_resp.raise_for_status() uj = json.loads(user_resp.text) ej = json.loads(emails_resp.text) for e in ej: @@ -78,3 +90,9 @@ def __complete_fetch_user_details(self, user_resp, emails_resp): uj["email"] = e["email"] break return User(uj, id=str(uj["id"])) + + def __get_client_headers(self, access_token): + return { + "Authorization": "Bearer {}".format(access_token), + "User-Agent": "Flet/{}".format(version), + } diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index 043039d43..3cb82cd55 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -392,7 +392,9 @@ def error(self, message=""): self._send_command("error", [message]) async def error_async(self, message=""): - assert self.__async_lock, "Async method calls are not supported in a regular app." + assert ( + self.__async_lock + ), "Async method calls are not supported in a regular app." async with self.__async_lock: await self._send_command_async("error", [message]) From 0f0e6456b0d9790d6d9e2bd12ad9e1c6320e7bb2 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 6 Jan 2023 11:32:53 -0800 Subject: [PATCH 33/34] Add cryptography to dev dependencies --- sdk/python/pdm.lock | 120 +++++++++++++++++++++++++++++++++++++- sdk/python/pyproject.toml | 1 + 2 files changed, 120 insertions(+), 1 deletion(-) diff --git a/sdk/python/pdm.lock b/sdk/python/pdm.lock index 5848346cd..840ee9c27 100644 --- a/sdk/python/pdm.lock +++ b/sdk/python/pdm.lock @@ -27,6 +27,14 @@ version = "2022.12.7" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." +[[package]] +name = "cffi" +version = "1.15.1" +summary = "Foreign Function Interface for Python calling C code." +dependencies = [ + "pycparser", +] + [[package]] name = "cfgv" version = "3.3.1" @@ -39,6 +47,15 @@ version = "0.4.6" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" summary = "Cross-platform colored terminal text." +[[package]] +name = "cryptography" +version = "39.0.0" +requires_python = ">=3.6" +summary = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +dependencies = [ + "cffi>=1.12", +] + [[package]] name = "distlib" version = "0.3.6" @@ -167,6 +184,12 @@ dependencies = [ "virtualenv>=20.0.8", ] +[[package]] +name = "pycparser" +version = "2.21" +requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +summary = "C parser in Python" + [[package]] name = "pytest" version = "7.2.0" @@ -286,7 +309,7 @@ summary = "Backport of pathlib-compatible object wrapper for zip files" [metadata] lock_version = "4.0" -content_hash = "sha256:56985dab38f0f277a90f27e9e63677cb8632a75f4499101ecab6e6757a1d43ac" +content_hash = "sha256:00c87240a392fbe1f9fa51eef0084924b4e4cb4a7860d2df87620a0cb8084136" [metadata.files] "anyio 3.6.2" = [ @@ -305,6 +328,72 @@ content_hash = "sha256:56985dab38f0f277a90f27e9e63677cb8632a75f4499101ecab6e6757 {url = "https://files.pythonhosted.org/packages/37/f7/2b1b0ec44fdc30a3d31dfebe52226be9ddc40cd6c0f34ffc8923ba423b69/certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, {url = "https://files.pythonhosted.org/packages/71/4c/3db2b8021bd6f2f0ceb0e088d6b2d49147671f25832fb17970e9b583d742/certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, ] +"cffi 1.15.1" = [ + {url = "https://files.pythonhosted.org/packages/00/05/23a265a3db411b0bfb721bf7a116c7cecaf3eb37ebd48a6ea4dfb0a3244d/cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {url = "https://files.pythonhosted.org/packages/03/7b/259d6e01a6083acef9d3c8c88990c97d313632bb28fa84d6ab2bb201140a/cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {url = "https://files.pythonhosted.org/packages/0e/65/0d7b5dad821ced4dcd43f96a362905a68ce71e6b5f5cfd2fada867840582/cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {url = "https://files.pythonhosted.org/packages/0e/e2/a23af3d81838c577571da4ff01b799b0c2bbde24bd924d97e228febae810/cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {url = "https://files.pythonhosted.org/packages/10/72/617ee266192223a38b67149c830bd9376b69cf3551e1477abc72ff23ef8e/cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {url = "https://files.pythonhosted.org/packages/18/8f/5ff70c7458d61fa8a9752e5ee9c9984c601b0060aae0c619316a1e1f1ee5/cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {url = "https://files.pythonhosted.org/packages/1d/76/bcebbbab689f5f6fc8a91e361038a3001ee2e48c5f9dbad0a3b64a64cc9e/cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {url = "https://files.pythonhosted.org/packages/22/c6/df826563f55f7e9dd9a1d3617866282afa969fe0d57decffa1911f416ed8/cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {url = "https://files.pythonhosted.org/packages/23/8b/2e8c2469eaf89f7273ac685164949a7e644cdfe5daf1c036564208c3d26b/cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {url = "https://files.pythonhosted.org/packages/2b/a8/050ab4f0c3d4c1b8aaa805f70e26e84d0e27004907c5b8ecc1d31815f92a/cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, + {url = "https://files.pythonhosted.org/packages/2d/86/3ca57cddfa0419f6a95d1c8478f8f622ba597e3581fd501bbb915b20eb75/cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {url = "https://files.pythonhosted.org/packages/2e/7a/68c35c151e5b7a12650ecc12fdfb85211aa1da43e9924598451c4a0a3839/cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {url = "https://files.pythonhosted.org/packages/32/2a/63cb8c07d151de92ff9d897b2eb27ba6a0e78dda8e4c5f70d7b8c16cd6a2/cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {url = "https://files.pythonhosted.org/packages/32/bd/d0809593f7976828f06a492716fbcbbfb62798bbf60ea1f65200b8d49901/cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {url = "https://files.pythonhosted.org/packages/37/5a/c37631a86be838bdd84cc0259130942bf7e6e32f70f4cab95f479847fb91/cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {url = "https://files.pythonhosted.org/packages/3a/12/d6066828014b9ccb2bbb8e1d9dc28872d20669b65aeb4a86806a0757813f/cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {url = "https://files.pythonhosted.org/packages/3a/75/a162315adeaf47e94a3b7f886a8e31d77b9e525a387eef2d6f0efc96a7c8/cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {url = "https://files.pythonhosted.org/packages/3f/fa/dfc242febbff049509e5a35a065bdc10f90d8c8585361c2c66b9c2f97a01/cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {url = "https://files.pythonhosted.org/packages/43/a0/cc7370ef72b6ee586369bacd3961089ab3d94ae712febf07a244f1448ffd/cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {url = "https://files.pythonhosted.org/packages/47/51/3049834f07cd89aceef27f9c56f5394ca6725ae6a15cff5fbdb2f06a24ad/cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {url = "https://files.pythonhosted.org/packages/47/97/137f0e3d2304df2060abb872a5830af809d7559a5a4b6a295afb02728e65/cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {url = "https://files.pythonhosted.org/packages/50/34/4cc590ad600869502c9838b4824982c122179089ed6791a8b1c95f0ff55e/cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {url = "https://files.pythonhosted.org/packages/5b/1a/e1ee5bed11d8b6540c05a8e3c32448832d775364d4461dd6497374533401/cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {url = "https://files.pythonhosted.org/packages/5d/4e/4e0bb5579b01fdbfd4388bd1eb9394a989e1336203a4b7f700d887b233c1/cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {url = "https://files.pythonhosted.org/packages/5d/6f/3a2e167113eabd46ed300ff3a6a1e9277a3ad8b020c4c682f83e9326fcf7/cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {url = "https://files.pythonhosted.org/packages/69/bf/335f8d95510b1a26d7c5220164dc739293a71d5540ecd54a2f66bac3ecb8/cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {url = "https://files.pythonhosted.org/packages/71/d7/0fe0d91b0bbf610fb7254bb164fa8931596e660d62e90fb6289b7ee27b09/cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {url = "https://files.pythonhosted.org/packages/77/b7/d3618d612be01e184033eab90006f8ca5b5edafd17bf247439ea4e167d8a/cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {url = "https://files.pythonhosted.org/packages/79/4b/33494eb0adbcd884656c48f6db0c98ad8a5c678fb8fb5ed41ab546b04d8c/cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {url = "https://files.pythonhosted.org/packages/7c/3e/5d823e5bbe00285e479034bcad44177b7353ec9fdcd7795baac5ccf82950/cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {url = "https://files.pythonhosted.org/packages/85/1f/a3c533f8d377da5ca7edb4f580cc3edc1edbebc45fac8bb3ae60f1176629/cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {url = "https://files.pythonhosted.org/packages/87/4b/64e8bd9d15d6b22b6cb11997094fbe61edf453ea0a97c8675cb7d1c3f06f/cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {url = "https://files.pythonhosted.org/packages/87/ee/ddc23981fc0f5e7b5356e98884226bcb899f95ebaefc3e8e8b8742dd7e22/cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {url = "https://files.pythonhosted.org/packages/88/89/c34caf63029fb7628ec2ebd5c88ae0c9bd17db98c812e4065a4d020ca41f/cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {url = "https://files.pythonhosted.org/packages/91/bc/b7723c2fe7a22eee71d7edf2102cd43423d5f95ff3932ebaa2f82c7ec8d0/cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {url = "https://files.pythonhosted.org/packages/93/d0/2e2b27ea2f69b0ec9e481647822f8f77f5fc23faca2dd00d1ff009940eb7/cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {url = "https://files.pythonhosted.org/packages/9f/52/1e2b43cfdd7d9a39f48bc89fcaee8d8685b1295e205a4f1044909ac14d89/cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {url = "https://files.pythonhosted.org/packages/a4/42/54bdf22cf6c8f95113af645d0bd7be7f9358ea5c2d57d634bb11c6b4d0b2/cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {url = "https://files.pythonhosted.org/packages/a8/16/06b84a7063a4c0a2b081030fdd976022086da9c14e80a9ed4ba0183a98a9/cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {url = "https://files.pythonhosted.org/packages/a9/ba/e082df21ebaa9cb29f2c4e1d7e49a29b90fcd667d43632c6674a16d65382/cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {url = "https://files.pythonhosted.org/packages/aa/02/ab15b3aa572759df752491d5fa0f74128cd14e002e8e3257c1ab1587810b/cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {url = "https://files.pythonhosted.org/packages/ad/26/7b3a73ab7d82a64664c7c4ea470e4ec4a3c73bb4f02575c543a41e272de5/cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {url = "https://files.pythonhosted.org/packages/af/cb/53b7bba75a18372d57113ba934b27d0734206c283c1dfcc172347fbd9f76/cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {url = "https://files.pythonhosted.org/packages/af/da/9441d56d7dd19d07dcc40a2a5031a1f51c82a27cee3705edf53dadcac398/cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {url = "https://files.pythonhosted.org/packages/b3/b8/89509b6357ded0cbacc4e430b21a4ea2c82c2cdeb4391c148b7c7b213bed/cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {url = "https://files.pythonhosted.org/packages/b5/7d/df6c088ef30e78a78b0c9cca6b904d5abb698afb5bc8f5191d529d83d667/cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {url = "https://files.pythonhosted.org/packages/b5/80/ce5ba093c2475a73df530f643a61e2969a53366e372b24a32f08cd10172b/cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {url = "https://files.pythonhosted.org/packages/b7/8b/06f30caa03b5b3ac006de4f93478dbd0239e2a16566d81a106c322dc4f79/cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {url = "https://files.pythonhosted.org/packages/b9/4a/dde4d093a3084d0b0eadfb2703f71e31a5ced101a42c839ac5bbbd1710f2/cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {url = "https://files.pythonhosted.org/packages/c1/25/16a082701378170559bb1d0e9ef2d293cece8dc62913d79351beb34c5ddf/cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {url = "https://files.pythonhosted.org/packages/c2/0b/3b09a755ddb977c167e6d209a7536f6ade43bb0654bad42e08df1406b8e4/cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {url = "https://files.pythonhosted.org/packages/c5/ff/3f9d73d480567a609e98beb0c64359f8e4f31cb6a407685da73e5347b067/cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {url = "https://files.pythonhosted.org/packages/c6/3d/dd085bb831b22ce4d0b7ba8550e6d78960f02f770bbd1314fea3580727f8/cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {url = "https://files.pythonhosted.org/packages/c9/e3/0a52838832408cfbbf3a59cb19bcd17e64eb33795c9710ca7d29ae10b5b7/cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {url = "https://files.pythonhosted.org/packages/d3/56/3e94aa719ae96eeda8b68b3ec6e347e0a23168c6841dc276ccdcdadc9f32/cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {url = "https://files.pythonhosted.org/packages/d3/e1/e55ca2e0dd446caa2cc8f73c2b98879c04a1f4064ac529e1836683ca58b8/cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {url = "https://files.pythonhosted.org/packages/da/ff/ab939e2c7b3f40d851c0f7192c876f1910f3442080c9c846532993ec3cef/cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {url = "https://files.pythonhosted.org/packages/df/02/aef53d4aa43154b829e9707c8c60bab413cd21819c4a36b0d7aaa83e2a61/cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {url = "https://files.pythonhosted.org/packages/e8/ff/c4b7a358526f231efa46a375c959506c87622fb4a2c5726e827c55e6adf2/cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {url = "https://files.pythonhosted.org/packages/ea/be/c4ad40ad441ac847b67c7a37284ae3c58f39f3e638c6b0f85fb662233825/cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {url = "https://files.pythonhosted.org/packages/ed/a3/c5f01988ddb70a187c3e6112152e01696188c9f8a4fa4c68aa330adbb179/cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {url = "https://files.pythonhosted.org/packages/ef/41/19da352d341963d29a33bdb28433ba94c05672fb16155f794fad3fd907b0/cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {url = "https://files.pythonhosted.org/packages/f9/96/fc9e118c47b7adc45a0676f413b4a47554e5f3b6c99b8607ec9726466ef1/cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {url = "https://files.pythonhosted.org/packages/ff/fe/ac46ca7b00e9e4f9c62e7928a11bc9227c86e2ff43526beee00cdfb4f0e8/cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, +] "cfgv 3.3.1" = [ {url = "https://files.pythonhosted.org/packages/6d/82/0a0ebd35bae9981dea55c06f8e6aaf44a49171ad798795c72c6f64cba4c2/cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {url = "https://files.pythonhosted.org/packages/c4/bf/d0d622b660d414a47dc7f0d303791a627663f554345b21250e39e7acb48b/cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, @@ -313,6 +402,31 @@ content_hash = "sha256:56985dab38f0f277a90f27e9e63677cb8632a75f4499101ecab6e6757 {url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +"cryptography 39.0.0" = [ + {url = "https://files.pythonhosted.org/packages/06/b1/6b6f8dccd1432a6a998e92f75ff235b0f69e8a8c509b5739d673ea2ba548/cryptography-39.0.0-cp36-abi3-win32.whl", hash = "sha256:f671c1bb0d6088e94d61d80c606d65baacc0d374e67bf895148883461cd848de"}, + {url = "https://files.pythonhosted.org/packages/0d/5c/b83623ce6e7d6653d858d5c85916217bb1e66a4c7a2da7051588fd5d9e0a/cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f97109336df5c178ee7c9c711b264c502b905c2d2a29ace99ed761533a3460f"}, + {url = "https://files.pythonhosted.org/packages/12/e3/c46c274cf466b24e5d44df5d5cd31a31ff23e57f074a2bb30931a8c9b01a/cryptography-39.0.0.tar.gz", hash = "sha256:f964c7dcf7802d133e8dbd1565914fa0194f9d683d82411989889ecd701e8adf"}, + {url = "https://files.pythonhosted.org/packages/13/56/7ebf13cfd85f2948480a45937bcc43d6f01edfde99dab47443e72aed564a/cryptography-39.0.0-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:80ee674c08aaef194bc4627b7f2956e5ba7ef29c3cc3ca488cf15854838a8f72"}, + {url = "https://files.pythonhosted.org/packages/2d/62/7c62efcb4a1b1905ad16476f9dcb55a2913bf4dd0049a083390a622901c8/cryptography-39.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f3ed2d864a2fa1666e749fe52fb8e23d8e06b8012e8bd8147c73797c506e86f1"}, + {url = "https://files.pythonhosted.org/packages/2e/90/1fffa1dd2e0894cdd8ef33b7d95de7c4d6de5fb77fb23cd21b24b069047e/cryptography-39.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:887cbc1ea60786e534b00ba8b04d1095f4272d380ebd5f7a7eb4cc274710fad9"}, + {url = "https://files.pythonhosted.org/packages/34/b3/3011b5f6c5cc935113fc58f8b07d42fcdd03e7a76b1c3c8ba27d276e8833/cryptography-39.0.0-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:875aea1039d78557c7c6b4db2fe0e9d2413439f4676310a5f269dd342ca7a717"}, + {url = "https://files.pythonhosted.org/packages/41/3f/8b3676edb61a9d2dc0e78ba9d450ebb75d958f70ed3dea9cb143262c8406/cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:e5d71c5d5bd5b5c3eebcf7c5c2bb332d62ec68921a8c593bea8c394911a005ce"}, + {url = "https://files.pythonhosted.org/packages/43/7d/0d0756853ad357b7f12d63595a8aac66d255ea68061e3c1983f4cfebc73b/cryptography-39.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e0a05aee6a82d944f9b4edd6a001178787d1546ec7c6223ee9a848a7ade92e39"}, + {url = "https://files.pythonhosted.org/packages/44/0a/4170788974aef7baf5eab77947246887c64a0a2c371f769f79259835af89/cryptography-39.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:e324de6972b151f99dc078defe8fb1b0a82c6498e37bff335f5bc6b1e3ab5a1e"}, + {url = "https://files.pythonhosted.org/packages/4e/9e/102aae84e2f1c4733ab76fd311d0b4612699daaba04a3a872567274dc211/cryptography-39.0.0-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bae6c7f4a36a25291b619ad064a30a07110a805d08dc89984f4f441f6c1f3f96"}, + {url = "https://files.pythonhosted.org/packages/54/34/5669347a730ba5f02b89499f03acf4563123fb98b27546d1fadcccf34564/cryptography-39.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7dacfdeee048814563eaaec7c4743c8aea529fe3dd53127313a792f0dadc1773"}, + {url = "https://files.pythonhosted.org/packages/78/23/ca3a4d7cb681fb4b7f9a088e7392f0aa2c1a51017a8a23fff377bb155af7/cryptography-39.0.0-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52a1a6f81e738d07f43dab57831c29e57d21c81a942f4602fac7ee21b27f288"}, + {url = "https://files.pythonhosted.org/packages/7a/46/8b58d6b8244ff613ecb983b9428d1168dd0b014a34e13fb19737b9ba1fc1/cryptography-39.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a6915075c6d3a5e1215eab5d99bcec0da26036ff2102a1038401d6ef5bef25b"}, + {url = "https://files.pythonhosted.org/packages/b3/f6/ee2cd6c13d62ecd4f93722327b5f79808d4a243d6d86e6c1058fe361dc68/cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:407cec680e811b4fc829de966f88a7c62a596faa250fc1a4b520a0355b9bc190"}, + {url = "https://files.pythonhosted.org/packages/b4/68/1857c44826171a995e65a11d4b507cd0aa0bace926c2842d7252d8b1dcca/cryptography-39.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f6c0db08d81ead9576c4d94bbb27aed8d7a430fa27890f39084c2d0e2ec6b0df"}, + {url = "https://files.pythonhosted.org/packages/d0/8a/5c567f8a6f12966c46d6f884d259ddf4f8ae908272e8c7c0807a53cdc255/cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ee1fd0de9851ff32dbbb9362a4d833b579b4a6cc96883e8e6d2ff2a6bc7104f"}, + {url = "https://files.pythonhosted.org/packages/dc/05/2cee803d6b83fef95229f9864646ba399f1ebda03333e34c2ddee210aaa1/cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:844ad4d7c3850081dffba91cdd91950038ee4ac525c575509a42d3fc806b83c8"}, + {url = "https://files.pythonhosted.org/packages/e5/9e/ed757a5244649d3400d62967d247af10e85d804882ba56fdf164c3f0c575/cryptography-39.0.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:754978da4d0457e7ca176f58c57b1f9de6556591c19b25b8bcce3c77d314f5eb"}, + {url = "https://files.pythonhosted.org/packages/ee/9f/f9f4e4410e1945550883bc07afc32986dc1e5d59bc327aff88f0ddbf0fb7/cryptography-39.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:fec8b932f51ae245121c4671b4bbc030880f363354b2f0e0bd1366017d891458"}, + {url = "https://files.pythonhosted.org/packages/fa/31/52ccfb7147564fefe83fdbbebc9dbd4c6749663c019a053ab2c83469c47a/cryptography-39.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50386acb40fbabbceeb2986332f0287f50f29ccf1497bae31cf5c3e7b4f4b34f"}, + {url = "https://files.pythonhosted.org/packages/fc/b2/3b946e24de214fc49adeefeea6214bcbc4bce2bd745877f074d1dd13c9a2/cryptography-39.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:76c24dd4fd196a80f9f2f5405a778a8ca132f16b10af113474005635fe7e066c"}, + {url = "https://files.pythonhosted.org/packages/fd/59/bacaaed27787b87b660b7de016e1034a9bf2aaf5031d2b7d085cd83413f3/cryptography-39.0.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad04f413436b0781f20c52a661660f1e23bcd89a0e9bb1d6d20822d048cf2856"}, +] "distlib 0.3.6" = [ {url = "https://files.pythonhosted.org/packages/58/07/815476ae605bcc5f95c87a62b95e74a1bce0878bc7a3119bc2bf4178f175/distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, {url = "https://files.pythonhosted.org/packages/76/cb/6bbd2b10170ed991cf64e8c8b85e01f2fb38f95d1bc77617569e0b0b26ac/distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, @@ -377,6 +491,10 @@ content_hash = "sha256:56985dab38f0f277a90f27e9e63677cb8632a75f4499101ecab6e6757 {url = "https://files.pythonhosted.org/packages/1e/ba/8cf8b88d0e07588818de46877effc9971305541d9421bc6377b06639d135/pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, {url = "https://files.pythonhosted.org/packages/b2/6c/9ccb5213a3d9fd3f8c0fd69d207951901eaef86b7a1a69bcc478364d3072/pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, ] +"pycparser 2.21" = [ + {url = "https://files.pythonhosted.org/packages/5e/0b/95d387f5f4433cb0f53ff7ad859bd2c6051051cebbb564f139a999ab46de/pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, + {url = "https://files.pythonhosted.org/packages/62/d5/5f610ebe421e85889f2e55e33b7f9a6795bd982198517d912eb1c76e1a53/pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, +] "pytest 7.2.0" = [ {url = "https://files.pythonhosted.org/packages/0b/21/055f39bf8861580b43f845f9e8270c7786fe629b2f8562ff09007132e2e7/pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, {url = "https://files.pythonhosted.org/packages/67/68/a5eb36c3a8540594b6035e6cdae40c1ef1b6a2bfacbecc3d1a544583c078/pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 6ad32fb9e..92dcad2a8 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -39,6 +39,7 @@ tests = [ ] dev = [ "pre-commit>=2.17.0", + "cryptography>=39.0.0", ] [project.scripts] From 6ab424c95a2cf76edd7e40632603a315c83cb7a9 Mon Sep 17 00:00:00 2001 From: Feodor Fitsner Date: Fri, 6 Jan 2023 13:02:18 -0800 Subject: [PATCH 34/34] clean() method is back! --- sdk/python/flet/column.py | 8 +++++++ sdk/python/flet/control.py | 14 ++++++++---- sdk/python/flet/grid_view.py | 8 +++++++ sdk/python/flet/list_view.py | 8 +++++++ sdk/python/flet/page.py | 36 +++++++++++++++++++++++++------ sdk/python/flet/responsive_row.py | 8 +++++++ sdk/python/flet/row.py | 8 +++++++ 7 files changed, 79 insertions(+), 11 deletions(-) diff --git a/sdk/python/flet/column.py b/sdk/python/flet/column.py index 334f59ffc..ab8071ee6 100644 --- a/sdk/python/flet/column.py +++ b/sdk/python/flet/column.py @@ -140,6 +140,14 @@ def _get_control_name(self): def _get_children(self): return self.__controls + def clean(self): + super().clean() + self.__controls.clear() + + async def clean_async(self): + await super().clean_async() + self.__controls.clear() + # tight @property def tight(self) -> Optional[bool]: diff --git a/sdk/python/flet/control.py b/sdk/python/flet/control.py index b862ac9d1..eb2d641fd 100644 --- a/sdk/python/flet/control.py +++ b/sdk/python/flet/control.py @@ -258,15 +258,21 @@ def data(self, value): # public methods def update(self): - if not self.__page: - raise Exception("Control must be added to the page first.") + assert self.__page, "Control must be added to the page first." self.__page.update(self) async def update_async(self): - if not self.__page: - raise Exception("Control must be added to the page first.") + assert self.__page, "Control must be added to the page first." await self.__page.update_async(self) + def clean(self): + assert self.__page, "Control must be added to the page first." + self.__page._clean(self) + + async def clean_async(self): + assert self.__page, "Control must be added to the page first." + await self.__page._clean_async(self) + def build_update_commands( self, index, commands, added_controls, removed_controls, isolated=False ): diff --git a/sdk/python/flet/grid_view.py b/sdk/python/flet/grid_view.py index 0c2c16db9..7142c1125 100644 --- a/sdk/python/flet/grid_view.py +++ b/sdk/python/flet/grid_view.py @@ -149,6 +149,14 @@ def _before_build_command(self): def _get_children(self): return self.__controls + def clean(self): + super().clean() + self.__controls.clear() + + async def clean_async(self): + await super().clean_async() + self.__controls.clear() + # horizontal @property def horizontal(self) -> Optional[bool]: diff --git a/sdk/python/flet/list_view.py b/sdk/python/flet/list_view.py index ea2c75b99..fa0644a5e 100644 --- a/sdk/python/flet/list_view.py +++ b/sdk/python/flet/list_view.py @@ -141,6 +141,14 @@ def _before_build_command(self): def _get_children(self): return self.__controls + def clean(self): + super().clean() + self.__controls.clear() + + async def clean_async(self): + await super().clean_async() + self.__controls.clear() + # horizontal @property def horizontal(self) -> Optional[bool]: diff --git a/sdk/python/flet/page.py b/sdk/python/flet/page.py index 3cb82cd55..c59142f1f 100644 --- a/sdk/python/flet/page.py +++ b/sdk/python/flet/page.py @@ -316,20 +316,42 @@ async def remove_at_async(self, index): await self.__handle_mount_unmount_async(*r) def clean(self): + self._clean(self) + self._controls.clear() + + async def clean_async(self): + await self._clean_async(self) + self._controls.clear() + + def _clean(self, control: Control): assert self.__lock, "Sync method calls are not supported in async app." with self.__lock: - self._controls.clear() - r = self.__update(self) - self.__handle_mount_unmount(*r) + control._previous_children.clear() + assert control.uid is not None + removed_controls = [] + for child in control._get_children(): + removed_controls.extend( + self._remove_control_recursively(self.index, child) + ) + self._send_command("clean", [control.uid]) + for c in removed_controls: + c.will_unmount() - async def clean_async(self): + async def _clean_async(self, control: Control): assert ( self.__async_lock ), "Async method calls are not supported in a regular app." async with self.__async_lock: - self._controls.clear() - r = await self.__update_async(self) - await self.__handle_mount_unmount_async(*r) + control._previous_children.clear() + assert control.uid is not None + removed_controls = [] + for child in control._get_children(): + removed_controls.extend( + self._remove_control_recursively(self.index, child) + ) + await self._send_command_async("clean", [control.uid]) + for c in removed_controls: + await c.will_unmount_async() def __update(self, *controls) -> Tuple[List[Control], List[Control]]: commands, added_controls, removed_controls = self.__prepare_update(*controls) diff --git a/sdk/python/flet/responsive_row.py b/sdk/python/flet/responsive_row.py index ae3bd8c79..ef01f3024 100644 --- a/sdk/python/flet/responsive_row.py +++ b/sdk/python/flet/responsive_row.py @@ -134,6 +134,14 @@ def _before_build_command(self): def _get_children(self): return self.__controls + def clean(self): + super().clean() + self.__controls.clear() + + async def clean_async(self): + await super().clean_async() + self.__controls.clear() + # horizontal_alignment @property def alignment(self) -> MainAxisAlignment: diff --git a/sdk/python/flet/row.py b/sdk/python/flet/row.py index 0982bd8d6..aad1217ac 100644 --- a/sdk/python/flet/row.py +++ b/sdk/python/flet/row.py @@ -143,6 +143,14 @@ def _get_control_name(self): def _get_children(self): return self.__controls + def clean(self): + super().clean() + self.__controls.clear() + + async def clean_async(self): + await super().clean_async() + self.__controls.clear() + # tight @property def tight(self) -> Optional[bool]: