diff --git a/.gitignore b/.gitignore index 67f0bba..bd49c17 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ SECRET.uuid4 *files/ *credentials.txt -*.vscode/ \ No newline at end of file +*.vscode/ +.logfire/ \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 204c0a5..02c404c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -151,6 +151,20 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + [[package]] name = "async-timeout" version = "4.0.3" @@ -483,6 +497,23 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] + [[package]] name = "docutils" version = "0.19" @@ -494,6 +525,20 @@ files = [ {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, ] +[[package]] +name = "executing" +version = "2.0.1" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.5" +files = [ + {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, + {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] + [[package]] name = "fastapi" version = "0.109.2" @@ -612,6 +657,23 @@ files = [ [package.dependencies] prometheus_client = ">=0.0.9" +[[package]] +name = "googleapis-common-protos" +version = "1.63.0" +description = "Common protobufs used in Google APIs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e"}, + {file = "googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632"}, +] + +[package.dependencies] +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] + [[package]] name = "h11" version = "0.14.0" @@ -752,6 +814,25 @@ files = [ {file = "ifaddr-0.2.0.tar.gz", hash = "sha256:cc0cbfcaabf765d44595825fb96a99bb12c79716b73b44330ea38ee2b0c4aed4"}, ] +[[package]] +name = "importlib-metadata" +version = "7.0.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.0.0-py3-none-any.whl", hash = "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"}, + {file = "importlib_metadata-7.0.0.tar.gz", hash = "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] + [[package]] name = "isort" version = "5.13.2" @@ -794,6 +875,68 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "logfire" +version = "0.32.0" +description = "The best Python observability tool! 🪵🔥" +optional = false +python-versions = "*" +files = [ + {file = "logfire-0.32.0-py2.py3-none-any.whl", hash = "sha256:bdfca682425d39ac23403009fafdfc72d6febd9edf9c60199d102d6c84056440"}, + {file = "logfire-0.32.0.tar.gz", hash = "sha256:f2cefbc9a4a81e5f27e587d8f2c64443aabb7027574de4a6bd5846cea2de84f7"}, +] + +[package.dependencies] +executing = ">=2.0.1" +opentelemetry-exporter-otlp-proto-http = ">=1.21.0" +opentelemetry-instrumentation = ">=0.41b0" +opentelemetry-instrumentation-system-metrics = {version = ">=0.42b0", optional = true, markers = "extra == \"system-metrics\""} +opentelemetry-sdk = ">=1.21.0" +protobuf = ">=4.23.4" +rich = ">=13.4.2" +typing-extensions = ">=4.1.0" + +[package.extras] +aiohttp = ["opentelemetry-instrumentation-aiohttp-client (>=0.42b0)"] +asyncpg = ["opentelemetry-instrumentation-asyncpg (>=0.42b0)"] +celery = ["opentelemetry-instrumentation-celery (>=0.42b0)"] +django = ["opentelemetry-instrumentation-django (>=0.42b0)"] +fastapi = ["opentelemetry-instrumentation-fastapi (>=0.42b0)"] +flask = ["opentelemetry-instrumentation-flask (>=0.42b0)"] +httpx = ["opentelemetry-instrumentation-httpx (>=0.42b0)"] +psycopg = ["opentelemetry-instrumentation-psycopg (>=0.42b0)", "packaging"] +psycopg2 = ["opentelemetry-instrumentation-psycopg2 (>=0.42b0)", "packaging"] +pymongo = ["opentelemetry-instrumentation-pymongo (>=0.42b0)"] +redis = ["opentelemetry-instrumentation-redis (>=0.42b0)"] +requests = ["opentelemetry-instrumentation-requests (>=0.42b0)"] +sqlalchemy = ["opentelemetry-instrumentation-sqlalchemy (>=0.42b0)"] +starlette = ["opentelemetry-instrumentation-starlette (>=0.42b0)"] +system-metrics = ["opentelemetry-instrumentation-system-metrics (>=0.42b0)"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markdown2" version = "2.4.13" @@ -879,6 +1022,17 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + [[package]] name = "multidict" version = "6.0.5" @@ -1026,6 +1180,238 @@ native = ["pywebview (>=4.4.0,<5.0.0)"] plotly = ["plotly (>=5.13.0,<6.0.0)"] sass = ["libsass (>=0.23.0,<0.24.0)"] +[[package]] +name = "opentelemetry-api" +version = "1.24.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_api-1.24.0-py3-none-any.whl", hash = "sha256:0f2c363d98d10d1ce93330015ca7fd3a65f60be64e05e30f557c61de52c80ca2"}, + {file = "opentelemetry_api-1.24.0.tar.gz", hash = "sha256:42719f10ce7b5a9a73b10a4baf620574fb8ad495a9cbe5c18d76b75d8689c67e"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +importlib-metadata = ">=6.0,<=7.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.24.0" +description = "OpenTelemetry Protobuf encoding" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.24.0-py3-none-any.whl", hash = "sha256:e51f2c9735054d598ad2df5d3eca830fecfb5b0bda0a2fa742c9c7718e12f641"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.24.0.tar.gz", hash = "sha256:5d31fa1ff976cacc38be1ec4e3279a3f88435c75b38b1f7a099a1faffc302461"}, +] + +[package.dependencies] +opentelemetry-proto = "1.24.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.24.0" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.24.0-py3-none-any.whl", hash = "sha256:25af10e46fdf4cd3833175e42f4879a1255fc01655fe14c876183a2903949836"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.24.0.tar.gz", hash = "sha256:704c066cc96f5131881b75c0eac286cd73fc735c490b054838b4513254bd7850"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.24.0" +opentelemetry-proto = "1.24.0" +opentelemetry-sdk = ">=1.24.0,<1.25.0" +requests = ">=2.7,<3.0" + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.45b0" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation-0.45b0-py3-none-any.whl", hash = "sha256:06c02e2c952c1b076e8eaedf1b82f715e2937ba7eeacab55913dd434fbcec258"}, + {file = "opentelemetry_instrumentation-0.45b0.tar.gz", hash = "sha256:6c47120a7970bbeb458e6a73686ee9ba84b106329a79e4a4a66761f933709c7e"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +setuptools = ">=16.0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.45b0" +description = "ASGI instrumentation for OpenTelemetry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation_asgi-0.45b0-py3-none-any.whl", hash = "sha256:8be1157ed62f0db24e45fdf7933c530c4338bd025c5d4af7830e903c0756021b"}, + {file = "opentelemetry_instrumentation_asgi-0.45b0.tar.gz", hash = "sha256:97f55620f163fd3d20323e9fd8dc3aacc826c03397213ff36b877e0f4b6b08a6"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.45b0" +opentelemetry-semantic-conventions = "0.45b0" +opentelemetry-util-http = "0.45b0" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] + +[[package]] +name = "opentelemetry-instrumentation-asyncio" +version = "0.45b0" +description = "OpenTelemetry instrumentation for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation_asyncio-0.45b0-py3-none-any.whl", hash = "sha256:063aaa04b83b3fe10b44b44624b8d090268ca49c0ec14c024523cd28631eda29"}, + {file = "opentelemetry_instrumentation_asyncio-0.45b0.tar.gz", hash = "sha256:38e766ebc1132c6d58cd0eecd750a0b30e84004c48cd9065c683c217c59d0388"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.14,<2.0" +opentelemetry-instrumentation = "0.45b0" +opentelemetry-semantic-conventions = "0.45b0" +opentelemetry-test-utils = "0.45b0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-instrumentation-asyncpg" +version = "0.45b0" +description = "OpenTelemetry instrumentation for AsyncPG" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation_asyncpg-0.45b0-py3-none-any.whl", hash = "sha256:a5ff319c09bfb2beeccdf4b5b2401930a3fd07a4e34aaa7eefd9e7f206507263"}, + {file = "opentelemetry_instrumentation_asyncpg-0.45b0.tar.gz", hash = "sha256:1782ea1b2b563db4ed821b1a6b7f1e2e6147256e5f3cde92cd20961378d378e9"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.45b0" +opentelemetry-semantic-conventions = "0.45b0" + +[package.extras] +instruments = ["asyncpg (>=0.12.0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.45b0" +description = "OpenTelemetry FastAPI Instrumentation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.45b0-py3-none-any.whl", hash = "sha256:77d9c123a363129148f5f66d44094f3d67aaaa2b201396d94782b4a7f9ce4314"}, + {file = "opentelemetry_instrumentation_fastapi-0.45b0.tar.gz", hash = "sha256:5a6b91e1c08a01601845fcfcfdefd0a2aecdb3c356d4a436a3210cb58c21487e"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.45b0" +opentelemetry-instrumentation-asgi = "0.45b0" +opentelemetry-semantic-conventions = "0.45b0" +opentelemetry-util-http = "0.45b0" + +[package.extras] +instruments = ["fastapi (>=0.58,<1.0)"] + +[[package]] +name = "opentelemetry-instrumentation-system-metrics" +version = "0.45b0" +description = "OpenTelemetry System Metrics Instrumentation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation_system_metrics-0.45b0-py3-none-any.whl", hash = "sha256:05c87cdbc959845c4cec6a7684a7ff279995752766be9d227fc6fdbbe67c043f"}, + {file = "opentelemetry_instrumentation_system_metrics-0.45b0.tar.gz", hash = "sha256:776ec5dd71d7de9bd499d2b561424a2be323a36b32c07c3d3983fcf4e03215e7"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.11,<2.0" +opentelemetry-instrumentation = "0.45b0" +opentelemetry-sdk = ">=1.11,<2.0" +psutil = ">=5.9,<6.0" + +[package.extras] +instruments = ["psutil (>=5)"] + +[[package]] +name = "opentelemetry-proto" +version = "1.24.0" +description = "OpenTelemetry Python Proto" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_proto-1.24.0-py3-none-any.whl", hash = "sha256:bcb80e1e78a003040db71ccf83f2ad2019273d1e0828089d183b18a1476527ce"}, + {file = "opentelemetry_proto-1.24.0.tar.gz", hash = "sha256:ff551b8ad63c6cabb1845ce217a6709358dfaba0f75ea1fa21a61ceddc78cab8"}, +] + +[package.dependencies] +protobuf = ">=3.19,<5.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.24.0" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_sdk-1.24.0-py3-none-any.whl", hash = "sha256:fa731e24efe832e98bcd90902085b359dcfef7d9c9c00eb5b9a18587dae3eb59"}, + {file = "opentelemetry_sdk-1.24.0.tar.gz", hash = "sha256:75bc0563affffa827700e0f4f4a68e1e257db0df13372344aebc6f8a64cde2e5"}, +] + +[package.dependencies] +opentelemetry-api = "1.24.0" +opentelemetry-semantic-conventions = "0.45b0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.45b0" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_semantic_conventions-0.45b0-py3-none-any.whl", hash = "sha256:a4a6fb9a7bacd9167c082aa4681009e9acdbfa28ffb2387af50c2fef3d30c864"}, + {file = "opentelemetry_semantic_conventions-0.45b0.tar.gz", hash = "sha256:7c84215a44ac846bc4b8e32d5e78935c5c43482e491812a0bb8aaf87e4d92118"}, +] + +[[package]] +name = "opentelemetry-test-utils" +version = "0.45b0" +description = "Test utilities for OpenTelemetry unit tests" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_test_utils-0.45b0-py3-none-any.whl", hash = "sha256:d22f0dd35506fedcc656f147860bc0b1848eb1d1d10ad0000ccfb99711657c3e"}, + {file = "opentelemetry_test_utils-0.45b0.tar.gz", hash = "sha256:589891cff3e4b3637f610ddc321ea458c83af54bc99f5be564fff9e3d04e1a91"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = "1.24.0" +opentelemetry-sdk = "1.24.0" + +[[package]] +name = "opentelemetry-util-http" +version = "0.45b0" +description = "Web util for OpenTelemetry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_util_http-0.45b0-py3-none-any.whl", hash = "sha256:6628868b501b3004e1860f976f410eeb3d3499e009719d818000f24ce17b6e33"}, + {file = "opentelemetry_util_http-0.45b0.tar.gz", hash = "sha256:4ce08b6a7d52dd7c96b7705b5b4f06fdb6aa3eac1233b3b0bfef8a0cab9a92cd"}, +] + [[package]] name = "orjson" version = "3.9.15" @@ -1151,6 +1537,26 @@ files = [ prometheus-client = ">=0.8.0,<1.0.0" starlette = ">=0.30.0,<1.0.0" +[[package]] +name = "protobuf" +version = "4.25.3" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, + {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, + {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, + {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, + {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, + {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, + {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, + {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, + {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, + {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, +] + [[package]] name = "pscript" version = "0.7.7" @@ -1464,6 +1870,40 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "setuptools" +version = "69.5.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, + {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "simple-websocket" version = "1.0.0" @@ -1803,6 +2243,85 @@ files = [ {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] + [[package]] name = "wsproto" version = "1.2.0" @@ -1920,7 +2439,22 @@ files = [ idna = ">=2.0" multidict = ">=4.0" +[[package]] +name = "zipp" +version = "3.18.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "5c29d65c78b9f5fd3b59f70b63c986a693678e6d96307b590e8d913c03def0ac" +content-hash = "9e988a5233679b3cbd2cfa48f8029518999fe1b417054eb87097cc8322d475ae" diff --git a/pyproject.toml b/pyproject.toml index 37bf3d1..740412b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,10 @@ starlette-exporter = "^0.21.0" gc-prometheus = "^0.1.0" requests = "^2.31.0" aiohttp = "^3.9.4" +logfire = {extras = ["system-metrics"], version = "^0.32.0"} +opentelemetry-instrumentation-fastapi = "^0.45b0" +opentelemetry-instrumentation-asyncpg = "^0.45b0" +opentelemetry-instrumentation-asyncio = "^0.45b0" [tool.poetry.group.dev.dependencies] diff --git a/src/__init__.py b/src/__init__.py index b0d19db..1410df8 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -12,51 +12,50 @@ from nicegui import Client, app, ui from prometheus_fastapi_instrumentator import Instrumentator, metrics from prometheus_fastapi_instrumentator.metrics import Info +import logfire load_dotenv() -import logging import os from . import middlewares +from .initialize_telemetry import initialize_telemetry from .views import install from .views.utils import db_ping db = asyncpg.create_pool(host=os.getenv("FOLDERISTIC_HOST"), user=os.getenv("FOLDERISTIC_USER"), password=os.getenv("FOLDERISTIC_PASS"), database=os.getenv("FOLDERISTIC_DB")) # type: ignore initialized_db = False - @app.on_startup async def ls(): - g = logging.getLogger("uvicorn.error") global db, initialized_db - if not initialized_db: - try: - a = await db - assert a is not None - db = a - except: - pass - finally: - initialized_db = True - install(app, db) - if db is None: - raise Exception("pool is none?") - g.info("Connected to database") + with logfire.span("Connecting to database"): + if not initialized_db: + try: + a = await db + assert a is not None + db = a + except: + pass + finally: + initialized_db = True + install(app, db) + logfire.info("Connected to database successfully") with open("./src/types.sql") as s: try: await db.execute(s.read()) # type: ignore except: pass finally: - g.info("Executed types statements") + logfire.info("Executed types statements") with open("./src/start.sql") as s: await db.execute(s.read()) # type: ignore - g.info("Executed starter statements") + logfire.info("Executed starter statements") @app.on_shutdown async def die(): if not initialized_db: + logfire.info("Closing database") await db.close() @@ -68,7 +67,9 @@ async def die(): with open("./SECRET.uuid4", "w") as fp: fp.write(secret) app.add_middleware(middlewares.auth.AuthMiddleWare, database=db) +logfire.info("Added authentication middleware") app.add_middleware(starlette_exporter.middleware.PrometheusMiddleware) +logfire.info("Added prometheus middleware") def db_latency(): @@ -162,8 +163,9 @@ def thingy(app: fastapi.FastAPI): prometheus.instrument(app) prometheus.expose(app) - + app.add_route("/metrics-starlette", starlette_exporter.handle_metrics) + initialize_telemetry(app) thingy(app) @@ -174,3 +176,4 @@ def thingy(app: fastapi.FastAPI): storage_secret=secret, favicon="./favicon.ico", ) +logfire.configure(console=False,send_to_logfire='if-token-present', collect_system_metrics=True, service_name="folderistic-main", project_name="folderistic", inspect_arguments=True, ) \ No newline at end of file diff --git a/src/initialize_telemetry.py b/src/initialize_telemetry.py new file mode 100644 index 0000000..1b49919 --- /dev/null +++ b/src/initialize_telemetry.py @@ -0,0 +1,14 @@ +import logfire.integrations +import opentelemetry.instrumentation.asyncio +import opentelemetry.instrumentation.asyncpg +import opentelemetry.instrumentation.fastapi +import logfire + +import fastapi + +def initialize_telemetry(app: fastapi.FastAPI): + opentelemetry.instrumentation.asyncio.AsyncioInstrumentor().instrument() + opentelemetry.instrumentation.asyncpg.AsyncPGInstrumentor(False).instrument() + opentelemetry.instrumentation.fastapi.FastAPIInstrumentor().instrument_app(app) + logfire.instrument_asyncpg() + logfire.instrument_fastapi(app) \ No newline at end of file diff --git a/src/middlewares/auth.py b/src/middlewares/auth.py index aa90ac4..0a4f365 100644 --- a/src/middlewares/auth.py +++ b/src/middlewares/auth.py @@ -2,6 +2,7 @@ import asyncpg import fastapi +import logfire import starlette.middleware.base import starlette.types from nicegui import Client @@ -18,19 +19,24 @@ def __init__(self, app: starlette.types.ASGIApp, database: asyncpg.Pool) -> None napp.on_disconnect(self.on_disconnect) async def dispatch(self, request: fastapi.Request, call_next) -> fastapi.Response: - if not await self.logged_in(): - if ( - request.url.path in Client.page_routes.values() - and request.url.path not in ["/login", "/failed_auth", "/about"] - ): - return fastapi.responses.RedirectResponse("/failed_auth") - async with self.db.acquire() as d: - await d.execute( - "UPDATE users SET first_connected = $2 WHERE session = $1", - str(napp.storage.user.get("authenticator")), - datetime.datetime.now(), - ) - return await call_next(request) + with logfire.span("Authentication Middleware Triggered"): + if not await self.logged_in(): + logfire.debug("User is not logged in.") + if ( + request.url.path in Client.page_routes.values() + and request.url.path not in ["/login", "/failed_auth", "/about"] + ): + logfire.debug("Returning to failed_auth") + return fastapi.responses.RedirectResponse("/failed_auth") + async with self.db.acquire() as d: + logfire.debug("Changing the first_connected based on session") + await d.execute( + "UPDATE users SET first_connected = $2 WHERE session = $1", + str(napp.storage.user.get("authenticator")), + datetime.datetime.now(), + ) + logfire.info("Proceed to next request") + return await call_next(request) async def on_disconnect( self, client: Client @@ -38,6 +44,7 @@ async def on_disconnect( # print(napp.storage.browser) # async with self.db.acquire() as d: # await d.execute("UPDATE users SET last_connected = $2 WHERE session = $1", str(napp.storage.user.get("authenticator")), datetime.datetime.now()) + logfire.debug("Client disconnected (who?)") pass async def logged_in(self): @@ -45,27 +52,29 @@ async def logged_in(self): tries = 0 e = [] global db + logfire.debug("Tries to authenticate user based on the session ID'") while tries <= 5: + logfire.debug(f"{tries=} Try") try: - if napp.storage.user.get("authenticated", False): - if ( - len( - await self.db.fetch( - "SELECT * FROM users WHERE session = $1", - str(napp.storage.user.get("authenticator", None)), - ) + if ( + len( + await self.db.fetch( + "SELECT * FROM users WHERE session = $1", + str(napp.storage.user.get("authenticator", None)), ) - == 0 - ): - napp.storage.user["authenticated"] = False - napp.storage.user["authenticator"] = None - return False - else: + ) + == 0 + ): + logfire.debug("Session ID doesn't match at all. Resetting.") + napp.storage.user["authenticated"] = False + napp.storage.user["authenticator"] = None return False return True except Exception as a: + logfire.exception("Failed to verify. Trying again") tries += 1 e.append(a) + logfire.error("Cannot authenticate user") raise Exception("Unable to fetch data") from ExceptionGroup( "Errors while fetching data from database", e ) diff --git a/src/views/__init__.py b/src/views/__init__.py index 3617c75..d7a43a0 100644 --- a/src/views/__init__.py +++ b/src/views/__init__.py @@ -1,13 +1,16 @@ import asyncpg import fastapi +import logfire from . import about, accessing_file, failed_auth, folder, index, login def install(app: fastapi.FastAPI, db: asyncpg.Pool): + logfire.debug("Installing routes") index.install(db) login.install(db) failed_auth.install(db) folder.install(app, db) accessing_file.install(app, db) about.install(app, db) + logfire.debug("Installed routes") \ No newline at end of file diff --git a/src/views/accessing_file.py b/src/views/accessing_file.py index d101aec..bb5ab42 100644 --- a/src/views/accessing_file.py +++ b/src/views/accessing_file.py @@ -3,6 +3,7 @@ import asyncpg import fastapi +import logfire import humanize from nicegui import app, ui @@ -10,130 +11,150 @@ from .utils import show_header -async def get_file_id(db: asyncpg.Pool, folder_id: str, file_id: str): - async with db.acquire() as d: - file_path = ( - await d.fetch( - "SELECT path FROM files WHERE id = $1", - file_id, - record_class=FileRecord, - ) - )[0] - try: - return humanize.naturalsize(Path(file_path.path).stat().st_size) - except FileNotFoundError: +async def get_file_size(db: asyncpg.Pool, folder_id: str, file_id: str): + with logfire.span("Getting file ID"): async with db.acquire() as d: - await d.execute("DELETE FROM files WHERE id = $1", file_id) - ui.navigate.to(f"/folder/{folder_id}") + logfire.debug("Getting query") + file_path = ( + await d.fetch( + "SELECT path FROM files WHERE id = $1", + file_id, + record_class=FileRecord, + ) + )[0] + try: + logfire.debug("Calculating file size with humanize") + return humanize.naturalsize(Path(file_path.path).stat().st_size) + except FileNotFoundError: + logfire.debug("File isn't found. Data loss found") + async with db.acquire() as d: + await d.execute("DELETE FROM files WHERE id = $1", file_id) + ui.navigate.to(f"/folder/{folder_id}") async def delete_file(db: asyncpg.Pool, folder_id: str, file: FileRecord): - os.remove(file.path) - async with db.acquire() as d: - await d.execute("DELETE FROM files WHERE id = $1", file.id) - ui.notify("Successfully deleted a file!") - ui.timer(5, lambda: ui.navigate.to(f"/folder/{folder_id}")) + with logfire.span("Deleting file"): + os.remove(file.path) + logfire.debug("Deleted file in disk") + async with db.acquire() as d: + await d.execute("DELETE FROM files WHERE id = $1", file.id) + logfire.debug("Deleted file off the record") + ui.notify("Successfully deleted a file!") + ui.timer(5, lambda: ui.navigate.to(f"/folder/{folder_id}")) def install(fapp: fastapi.FastAPI, db: asyncpg.Pool): @ui.page("/folder/{folder_id}/{file_id}") async def accessing_file(folder_id: str, file_id: str): - async with db.acquire() as d: - if ( - len( - x := await d.fetch( - "SELECT * FROM files WHERE folder = $1 AND id = $2", - folder_id, - file_id, - record_class=FileRecord, + with logfire.span(f"/folder/{folder_id}/{file_id}: Accessing file"): + async with db.acquire() as d: + logfire.debug("Checking if file is there") + if ( + len( + x := await d.fetch( + "SELECT * FROM files WHERE folder = $1 AND id = $2", + folder_id, + file_id, + record_class=FileRecord, + ) ) - ) - == 0 - ): - await show_header(db, "Listing - Folder - Unknown File") - with ui.card().classes("absolute-center"): - ui.label("File not found!") - ui.notify( - "File not found! Redirect back to folder in 3 seconds", - type="negative", - ) - ui.timer(3, lambda: ui.navigate.to(f"/folder/{folder_id}")) - return - file = x[0] - folder = ( - await d.fetch( - "SELECT * FROM folders WHERE id = $1", - folder_id, - record_class=FolderRecord, - ) - )[0] - await show_header(db, f"Folder {folder.name} - {file.name}") - with ui.column().classes("w-auto h-auto"): - with ui.row().classes("items-center justify-center"): - ui.label(f"Folder: {folder.name} ({file.folder})") - ui.label(f"File: {file.name} ({file.id})") - ui.label( - f"When uploaded: {str(file.last_updated).replace('-', '/').split('.')[0]}" + == 0 + ): + logfire.debug("File not found") + await show_header(db, "Listing - Folder - Unknown File") + with ui.card().classes("absolute-center"): + ui.label("File not found!") + ui.notify( + "File not found! Redirect back to folder in 3 seconds", + type="negative", ) - ui.label(f"Who uploaded this file: {file.who}") - ui.separator() - with ui.column().classes("w-auto h-auto"): - with ui.row().classes("items-center justify-center"): - - async def download(): - ui.download( - f"/folder/{folder_id}/{file_id}/download", file.name - ) - - ui.button( - f"Download ({await get_file_id(db, folder_id, file.id)})", - on_click=download, + ui.timer(3, lambda: ui.navigate.to(f"/folder/{folder_id}")) + return + file = x[0] + folder = ( + await d.fetch( + "SELECT * FROM folders WHERE id = $1", + folder_id, + record_class=FolderRecord, ) - ui.button( - f"View", - on_click=lambda: ui.navigate.to( - f"/folder/{folder_id}/{file_id}/view", True - ), - ).props("color=pink") - user = ( - await d.fetch( - "SELECT * from users WHERE session = $1", - str(app.storage.user.get("authenticator")), - record_class=UserRecord, + )[0] + logfire.debug("Got folder record") + await show_header(db, f"Folder {folder.name} - {file.name}") + with ui.column().classes("w-auto h-auto"): + with ui.row().classes("items-center justify-center"): + ui.label(f"Folder: {folder.name} ({file.folder})") + ui.label(f"File: {file.name} ({file.id})") + ui.label( + f"When uploaded: {str(file.last_updated).replace('-', '/').split('.')[0]}" ) - )[0] + ui.label(f"Who uploaded this file: {file.who}") + ui.separator() + logfire.debug("Added information about file") + with ui.column().classes("w-auto h-auto"): + with ui.row().classes("items-center justify-center"): + def download(): + logfire.info("File downloading") + ui.download( + f"/folder/{folder_id}/{file_id}/download", file.name + ) - if user.roles == "admin": ui.button( - f"Delete", on_click=lambda: delete_file(db, folder_id, file) - ).props("color=red") + f"Download ({await get_file_size(db, folder_id, file.id)})", + on_click=download, + ) + ui.button( + f"View", + on_click=lambda: ui.navigate.to( + f"/folder/{folder_id}/{file_id}/view", True + ), + ).props("color=pink") + logfire.debug("Checking user role") + user = ( + await d.fetch( + "SELECT * from users WHERE session = $1", + str(app.storage.user.get("authenticator")), + record_class=UserRecord, + ) + )[0] + + if user.roles == "admin": + logfire.debug("User is admin adding ability to delete file") + ui.button( + f"Delete", on_click=lambda: delete_file(db, folder_id, file) + ).props("color=red") @fapp.get("/folder/{folder_id}/{file_id}/download") async def download_thing( folder_id: str, file_id: str ) -> fastapi.responses.FileResponse: - async with db.acquire() as d: - file = ( - await d.fetch( - "SELECT * FROM files WHERE id = $1 AND folder = $2", - file_id, - folder_id, - record_class=FileRecord, - ) - )[0] - return fastapi.responses.FileResponse(file.path, filename=file.name) + with logfire.span(f"/folder/{folder_id}/{file_id}/download: Accessing file"): + async with db.acquire() as d: + logfire.debug("Getting file information") + file = ( + await d.fetch( + "SELECT * FROM files WHERE id = $1 AND folder = $2", + file_id, + folder_id, + record_class=FileRecord, + ) + )[0] + logfire.debug("Got file information and sending FileResponse back") + return fastapi.responses.FileResponse(file.path, filename=file.name) @fapp.get("/folder/{folder_id}/{file_id}/view") async def viewery(folder_id: str, file_id: str) -> fastapi.responses.FileResponse: - async with db.acquire() as d: - file = ( - await d.fetch( - "SELECT * FROM files WHERE id = $1 AND folder = $2", - file_id, - folder_id, - record_class=FileRecord, + with logfire.span(f"/folder/{folder_id}/{file_id}/view: Accessing file"): + async with db.acquire() as d: + logfire.debug("Getting file record") + file = ( + await d.fetch( + "SELECT * FROM files WHERE id = $1 AND folder = $2", + file_id, + folder_id, + record_class=FileRecord, + ) + )[0] + logfire.debug("Sending file preview back") + return fastapi.responses.FileResponse( + file.path, filename=file.name, content_disposition_type="inline" ) - )[0] - return fastapi.responses.FileResponse( - file.path, filename=file.name, content_disposition_type="inline" - ) diff --git a/src/views/folder.py b/src/views/folder.py index 3e40427..86ec8d6 100644 --- a/src/views/folder.py +++ b/src/views/folder.py @@ -1,13 +1,13 @@ import asyncio import copy import datetime -import io import os import uuid import zipfile import asyncpg import fastapi +import logfire import starlette.background from nicegui import app, ui from nicegui.events import UploadEventArguments @@ -25,309 +25,308 @@ async def create_folders(folders: str): os.mkdir(folders) except FileExistsError: pass - +async def a(): + ui.notify("Redirecting back in 5 seconds", type="ongoing") + await asyncio.sleep(5) + ui.navigate.to("/") def install(fapp: fastapi.FastAPI, db: asyncpg.Pool): @ui.page("/folder/{folder_id}") async def get_contents(folder_id: str): - async def a(): - ui.notify("Redirecting back in 5 seconds", type="ongoing") - await asyncio.sleep(5) - ui.navigate.to("/") - - async with db.acquire() as d: - if not len( - await d.fetch( - "SELECT 1 FROM folders WHERE id = $1", + with logfire.span(f"/folder/{folder_id}: Accessing folder"): + async with db.acquire() as d: + if not len( + await d.fetch( + "SELECT 1 FROM folders WHERE id = $1", + folder_id, + record_class=FolderRecord, + ) + ): + await show_header(None, f"Unknown Folder") + with ui.card().classes("absolute-center"): + ui.label( + "Folder is not exists!" + ) # .classes("flex flex-col items-center justify-center") + ui.button("Go Back", on_click=a).classes( + "justify-center" + ) # .classes("flex flex-col items-center justify-center") + ui.notify( + "Folder is not exists! Redirecting you in 5 seconds.", + type="negative", + ) + ui.timer(5, callback=lambda: ui.navigate.to("/")) + return + async with db.acquire() as d: + role = await d.fetch( + "SELECT username, roles FROM users WHERE session = $1", + str(app.storage.user["authenticator"]), + record_class=UserRecord, + ) + accessers = await d.fetch( + "SELECT accessers FROM folders WHERE id = $1", folder_id, record_class=FolderRecord, ) + if ( + role[0].username not in accessers[0].accessers + and role[0].roles != "admin" + ): + await show_header(db, f"Inaccessible Folder") + with ui.card().classes("absolute-center"): + ui.label( + "Folder is not accessible!" + ) # .classes("flex flex-col items-center justify-center") + ui.button("Go Back", on_click=a).classes( + "justify-center" + ) # .classes("flex flex-col items-center justify-center") + ui.notify( + "Folder is not accessible! Redirecting you in 5 seconds.", + type="negative", + ) + ui.timer(5, callback=lambda: ui.navigate.to("/")) + return + + async def upload_event( + u: UploadEventArguments, n: ui.notification | None = None ): - await show_header(None, f"Unknown Folder") - with ui.card().classes("absolute-center"): - ui.label( - "Folder is not exists!" - ) # .classes("flex flex-col items-center justify-center") - ui.button("Go Back", on_click=a).classes( - "justify-center" - ) # .classes("flex flex-col items-center justify-center") - ui.notify( - "Folder is not exists! Redirecting you in 5 seconds.", - type="negative", - ) - ui.timer(5, callback=lambda: ui.navigate.to("/")) - return - async with db.acquire() as d: - role = await d.fetch( - "SELECT username, roles FROM users WHERE session = $1", - str(app.storage.user["authenticator"]), - record_class=UserRecord, - ) - accessers = await d.fetch( - "SELECT accessers FROM folders WHERE id = $1", - folder_id, - record_class=FolderRecord, - ) - if ( - role[0].username not in accessers[0].accessers - and role[0].roles != "admin" - ): - await show_header(db, f"Inaccessible Folder") - with ui.card().classes("absolute-center"): - ui.label( - "Folder is not accessible!" - ) # .classes("flex flex-col items-center justify-center") - ui.button("Go Back", on_click=a).classes( - "justify-center" - ) # .classes("flex flex-col items-center justify-center") - ui.notify( - "Folder is not accessible! Redirecting you in 5 seconds.", - type="negative", + if not n: + n = ui.notification(timeout=None, type="positive") + file = u.content + file_id = uuid.uuid4() + n.spinner = True + n.message = "Processing" + try: + with open(f"files/{folder_id}/{str(file_id)}", "wb") as fp: + fp.write(file.read()) + except Exception: + await create_folders(f"files/{folder_id}") + return await upload_event(u, n) + async with db.acquire() as d: + user = ( + await d.fetch( + "SELECT * FROM users WHERE session = $1", + str(app.storage.user.get("authenticator")), + record_class=UserRecord, + ) + )[0] + await d.execute( + "INSERT INTO files(folder, id, last_updated, path, name, who) VALUES ($1, $2, $3, $4, $5, $6)", + folder_id, + str(file_id), + datetime.datetime.now(), + f"files/{folder_id}/{file_id}", + u.name, + user.username, ) - ui.timer(5, callback=lambda: ui.navigate.to("/")) - return + n.message = f"Successfully uploaded your file! ({u.name}) Please refresh manually" + ui.timer(5, n.dismiss, once=True) + ui.timer(5, lambda: ui.navigate.to(f"/folder/{folder_id}")) + + async def file_upload_popup(): + with ui.dialog(value=True): + with ui.card().classes("fixed-center absolute-center"): + ui.label( + "Please drag your files to the box under this text or just click on plus sign." + ) + ui.upload( + multiple=True, + label="Drag your files here or click me!", + on_upload=lambda u: upload_event(u), + ) - async def upload_event( - u: UploadEventArguments, n: ui.notification | None = None - ): - if not n: - n = ui.notification(timeout=None, type="positive") - file = u.content - file_id = uuid.uuid4() - n.spinner = True - n.message = "Processing" - try: - with open(f"files/{folder_id}/{str(file_id)}", "wb") as fp: - fp.write(file.read()) - except Exception: - await create_folders(f"files/{folder_id}") - return await upload_event(u, n) async with db.acquire() as d: - user = ( - await d.fetch( - "SELECT * FROM users WHERE session = $1", - str(app.storage.user.get("authenticator")), - record_class=UserRecord, - ) - )[0] - await d.execute( - "INSERT INTO files(folder, id, last_updated, path, name, who) VALUES ($1, $2, $3, $4, $5, $6)", + folder = await d.fetch( + "SELECT * FROM folders WHERE id = $1", folder_id, - str(file_id), - datetime.datetime.now(), - f"files/{folder_id}/{file_id}", - u.name, - user.username, + record_class=FolderRecord, ) - n.message = f"Successfully uploaded your file! ({u.name}) Please refresh manually" - ui.timer(5, n.dismiss, once=True) - ui.timer(5, lambda: ui.navigate.to(f"/folder/{folder_id}")) - - async def file_upload_popup(): - with ui.dialog(value=True): - with ui.card().classes("fixed-center absolute-center"): - ui.label( - "Please drag your files to the box under this text or just click on plus sign." - ) - ui.upload( - multiple=True, - label="Drag your files here or click me!", - on_upload=lambda u: upload_event(u), - ) - - async with db.acquire() as d: - folder = await d.fetch( - "SELECT * FROM folders WHERE id = $1", - folder_id, - record_class=FolderRecord, - ) - files = await d.fetch( - "SELECT * FROM files WHERE folder = $1", - folder_id, - record_class=FileRecord, - ) - role = await d.fetch( - "SELECT roles FROM users WHERE session = $1", - str(app.storage.user.get("authenticator")), - record_class=UserRecord, - ) - buttons: list[CustomButtonBuilder] = [] + files = await d.fetch( + "SELECT * FROM files WHERE folder = $1", + folder_id, + record_class=FileRecord, + ) + role = await d.fetch( + "SELECT roles FROM users WHERE session = $1", + str(app.storage.user.get("authenticator")), + record_class=UserRecord, + ) + buttons: list[CustomButtonBuilder] = [] - async def delete_folder(): - async with db.acquire() as d: - await d.execute("DELETE FROM folders WHERE id = $1", folder_id) - for file in await d.fetch( - "SELECT id, path FROM files WHERE folder = $1", - folder_id, - record_class=FileRecord, - ): + async def delete_folder(): + async with db.acquire() as d: + await d.execute("DELETE FROM folders WHERE id = $1", folder_id) + for file in await d.fetch( + "SELECT id, path FROM files WHERE folder = $1", + folder_id, + record_class=FileRecord, + ): + try: + os.remove(file.path) + except: + pass try: - os.remove(file.path) + os.rmdir(f"files/{folder_id}") except: pass - try: - os.rmdir(f"files/{folder_id}") - except: - pass - ui.notify("Successfully deleted all the files", type="positive") - ui.navigate.to("/") + ui.notify("Successfully deleted all the files", type="positive") + ui.navigate.to("/") - if role[0].roles in ["admin", "uploaders"]: - buttons.append( - CustomButtonBuilder(on_click=file_upload_popup).props( - "flat color=white icon=file_upload" + if role[0].roles in ["admin", "uploaders"]: + buttons.append( + CustomButtonBuilder(on_click=file_upload_popup).props( + "flat color=white icon=file_upload" + ) ) - ) - if role[0].roles == "admin": - buttons.append( - CustomButtonBuilder(on_click=delete_folder).props( - "flat color=white icon=folder_delete" + if role[0].roles == "admin": + buttons.append( + CustomButtonBuilder(on_click=delete_folder).props( + "flat color=white icon=folder_delete" + ) ) - ) - async def add_people(): - with ui.dialog(value=True), ui.card(): - ui.label( - "Users that have access to this folder (All administrators will have a access to every folder but they may not show up here.) (Submitting same name that is in accessible users will remove them)" - ) - with ui.element("q-list").props("bordered separator"): - async with db.acquire() as d: - users = ( - await d.fetch( - "SELECT accessers FROM folders WHERE id = $1", + async def add_people(): + with ui.dialog(value=True), ui.card(): + ui.label( + "Users that have access to this folder (All administrators will have a access to every folder but they may not show up here.) (Submitting same name that is in accessible users will remove them)" + ) + with ui.element("q-list").props("bordered separator"): + async with db.acquire() as d: + users = ( + await d.fetch( + "SELECT accessers FROM folders WHERE id = $1", + folder_id, + record_class=FolderRecord, + ) + )[0] + for user in users.accessers: + with ui.element("q-item"): + with ui.element("q-item-section"): + ui.label(user) + + async def add_user(): + usernames: list[str] = username.value.strip().split(",") + remove = [] + for u in copy.copy(usernames): + if u in users.accessers: + usernames.remove(u) + remove.append(u) + if not username: + usernames.remove(u) + remove.append(u) + async with db.acquire() as d: + await d.execute( + """ + UPDATE folders + SET accessers = ARRAY( + SELECT DISTINCT unnest + FROM ( + SELECT unnest(accessers) + UNION + SELECT unnest($2::text[]) + ) s + ) + WHERE id = $1 + """, folder_id, - record_class=FolderRecord, + usernames, ) - )[0] - for user in users.accessers: - with ui.element("q-item"): - with ui.element("q-item-section"): - ui.label(user) - - async def add_user(): - usernames: list[str] = username.value.strip().split(",") - remove = [] - for u in copy.copy(usernames): - if u in users.accessers: - usernames.remove(u) - remove.append(u) - if not username: - usernames.remove(u) - remove.append(u) - async with db.acquire() as d: - await d.execute( - """ - UPDATE folders - SET accessers = ARRAY( - SELECT DISTINCT unnest - FROM ( - SELECT unnest(accessers) - UNION - SELECT unnest($2::text[]) - ) s + await d.execute( + """ + UPDATE folders + SET accessers = ( + SELECT array_agg(a) + FROM ( + SELECT unnest(accessers) AS a + FROM folders + WHERE id = $1 + ) AS unnested + WHERE a <> ALL($2) + ) + WHERE id = $1; + """, + folder_id, + remove, ) - WHERE id = $1 - """, - folder_id, - usernames, - ) - await d.execute( - """ - UPDATE folders - SET accessers = ( - SELECT array_agg(a) - FROM ( - SELECT unnest(accessers) AS a - FROM folders - WHERE id = $1 - ) AS unnested - WHERE a <> ALL($2) + ui.notify( + "Successfully added new users to be able to access this folder!", + type="positive", ) - WHERE id = $1; - """, - folder_id, - remove, - ) - ui.notify( - "Successfully added new users to be able to access this folder!", - type="positive", + ui.timer(3, lambda: ui.navigate.to(f"/folder/{folder_id}")) + + async with db.acquire() as d: + usernames = await d.fetch( + "SELECT username FROM users", record_class=UserRecord ) - ui.timer(3, lambda: ui.navigate.to(f"/folder/{folder_id}")) + username = ui.input( + "Input your username here splitting by commas", + autocomplete=[x.username for x in usernames], + ) + ui.button("Submit", on_click=add_user) - async with db.acquire() as d: - usernames = await d.fetch( - "SELECT username FROM users", record_class=UserRecord + if role[0].roles == "admin": + buttons.append( + CustomButtonBuilder(on_click=add_people).props( + "flat color=white icon=person_add" ) - username = ui.input( - "Input your username here splitting by commas", - autocomplete=[x.username for x in usernames], ) - ui.button("Submit", on_click=add_user) - if role[0].roles == "admin": + async def zip_files_then_download(): + notification = ui.notification(timeout=None) + notification.spinner = True + notification.message = "Processing" + # notification.message = "Initializing ZipFile In Memory" + with zipfile.ZipFile( + f"files/{folder_id}.zip", "w", zipfile.ZIP_LZMA, True + ) as zipper: + notification.message = "Getting all files in the folder" + async with db.acquire() as d: + files = await d.fetch( + "SELECT * from files WHERE folder = $1", + folder_id, + record_class=FileRecord, + ) + notification.message = "Successfully got all files" + file_names = [] + for file in files: + notification.message = f"Processing {file.name}" + if file.name in file_names: + name, ext = os.path.splitext(file.name) + file_name = name + str(uuid.uuid4()) + ext + else: + file_name = file.name + notification.message = f"Writing {file.name} to zip" + zipper.write(file.path, file_name) + notification.message = "Sending download request" + ui.navigate.to(f"/folder/{folder_id}/zipped", True) + notification.spinner = True + notification.message = "Enjoy your download!" + notification.spinner = False + notification.type = "positive" + ui.timer(3, notification.dismiss, once=True) + buttons.append( - CustomButtonBuilder(on_click=add_people).props( - "flat color=white icon=person_add" + CustomButtonBuilder(on_click=zip_files_then_download).props( + "flat color=white icon=folder_zip" ) ) - - async def zip_files_then_download(): - notification = ui.notification(timeout=None) - notification.spinner = True - notification.message = "Processing" - # notification.message = "Initializing ZipFile In Memory" - with zipfile.ZipFile( - f"files/{folder_id}.zip", "w", zipfile.ZIP_LZMA, True - ) as zipper: - notification.message = "Getting all files in the folder" - async with db.acquire() as d: - files = await d.fetch( - "SELECT * from files WHERE folder = $1", - folder_id, - record_class=FileRecord, - ) - notification.message = "Successfully got all files" - file_names = [] - for file in files: - notification.message = f"Processing {file.name}" - if file.name in file_names: - name, ext = os.path.splitext(file.name) - file_name = name + str(uuid.uuid4()) + ext - else: - file_name = file.name - notification.message = f"Writing {file.name} to zip" - zipper.write(file.path, file_name) - notification.message = "Sending download request" - ui.navigate.to(f"/folder/{folder_id}/zipped", True) - notification.spinner = True - notification.message = "Enjoy your download!" - notification.spinner = False - notification.type = "positive" - ui.timer(3, notification.dismiss, once=True) - - buttons.append( - CustomButtonBuilder(on_click=zip_files_then_download).props( - "flat color=white icon=folder_zip" + await show_header( + db, f"Listing - Folder {folder[0].name} (ID: {folder[0].id})", buttons ) - ) - await show_header( - db, f"Listing - Folder {folder[0].name} (ID: {folder[0].id})", buttons - ) - with ui.row(wrap=True).classes("items-start justify-center gap-10 m-4"): - for f in files: - with ui.column().classes("w-auto h-auto"): - j = f.id + with ui.row(wrap=True).classes("items-start justify-center gap-10 m-4"): + for f in files: + with ui.column().classes("w-auto h-auto"): + j = f.id - def v( - j=j, - ): # This captures the current value of `j` for each iteration - ui.navigate.to(f"/folder/{folder_id}/{j}") + def v( + j=j, + ): # This captures the current value of `j` for each iteration + ui.navigate.to(f"/folder/{folder_id}/{j}") - with ui.button(on_click=v).classes( - "flex flex-col items-center justify-center" - ): - ui.icon("description", size="md", color="darkorange") - ui.label(f"{f.name} (ID: {f.id})") + with ui.button(on_click=v).classes( + "flex flex-col items-center justify-center" + ): + ui.icon("description", size="md", color="darkorange") + ui.label(f"{f.name} (ID: {f.id})") @fapp.get("/folder/{folder_id}/zipped") async def get_zip(