From 7cf175f75a323d1afdfa4fe44b9c40d972704e07 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 21:59:16 +0300 Subject: [PATCH 01/12] fix docker with fastapi --- Dockerfile | 36 ++++++++++++++++++++++++++++++------ start.sh | 13 +++++++++++-- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index d9fe6223..daacc199 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,7 @@ -# Use a single stage build with FalkorDB base image +# Multi-stage build: Start with Python 3.12 base +FROM python:3.12-bookworm as python-base + +# Main stage: Use FalkorDB base and copy Python 3.12 FROM falkordb/falkordb:latest ENV PYTHONUNBUFFERED=1 \ @@ -7,12 +10,15 @@ ENV PYTHONUNBUFFERED=1 \ USER root -# Install Python and pip, netcat for wait loop in start.sh +# Copy Python 3.12 from the python base image +COPY --from=python-base /usr/local /usr/local + +# Install netcat for wait loop in start.sh RUN apt-get update && apt-get install -y \ - python3 \ - python3-pip \ netcat-openbsd \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && ln -sf /usr/local/bin/python3.12 /usr/bin/python3 \ + && ln -sf /usr/local/bin/python3.12 /usr/bin/python WORKDIR /app @@ -25,6 +31,25 @@ COPY Pipfile Pipfile.lock ./ # Install Python dependencies from Pipfile RUN PIP_BREAK_SYSTEM_PACKAGES=1 pipenv sync --system +# Install Node.js (Node 22) so we can build the frontend inside the image. +# Use NodeSource setup script to get a recent Node version on Debian-based images. +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get update && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Copy only frontend package files so Docker can cache npm installs when +# package.json / package-lock.json don't change. +COPY app/package*.json ./app/ + +# Install frontend dependencies (reproducible install using package-lock) +RUN if [ -f ./app/package-lock.json ]; then \ + npm --prefix ./app ci --no-audit --no-fund; \ + elif [ -f ./app/package.json ]; then \ + npm --prefix ./app install --no-audit --no-fund; \ + else \ + echo "No frontend package.json found, skipping npm install"; \ + fi + # Copy application code COPY . . @@ -34,6 +59,5 @@ RUN chmod +x /start.sh EXPOSE 5000 6379 3000 - # Use start.sh as entrypoint ENTRYPOINT ["/start.sh"] diff --git a/start.sh b/start.sh index c0db7ef6..3b655a06 100644 --- a/start.sh +++ b/start.sh @@ -17,5 +17,14 @@ while ! nc -z "$FALKORDB_HOST" "$FALKORDB_PORT"; do done -echo "FalkorDB is up - launching Flask..." -exec python3 -m flask --app api.index run --host=0.0.0.0 --port=5000 \ No newline at end of file +echo "FalkorDB is up - launching FastAPI..." +# Determine whether to run in reload (debug) mode. The project uses FLASK_DEBUG +# environment variable historically; keep compatibility by honoring it here. +if [ "${FLASK_DEBUG:-False}" = "True" ] || [ "${FLASK_DEBUG:-true}" = "true" ]; then + RELOAD_FLAG="--reload" +else + RELOAD_FLAG="" +fi + +echo "FalkorDB is up - launching FastAPI (uvicorn)..." +exec uvicorn api.index:app --host 0.0.0.0 --port 5000 $RELOAD_FLAG \ No newline at end of file From 9fd98756509232c39e41d2289c5086bb530242ee Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 22:19:35 +0300 Subject: [PATCH 02/12] clean flask --- .env.example | 4 ++-- .github/copilot-instructions.md | 8 ++++---- .github/workflows/e2e-tests.yml | 4 ++-- .github/workflows/tests.yml | 8 ++++---- README.md | 4 ++-- api/app_factory.py | 4 ++-- api/auth/user_management.py | 2 +- api/index.py | 5 +++-- app/README.md | 2 +- start.sh | 4 ++-- tests/e2e/README.md | 6 +++--- 11 files changed, 26 insertions(+), 25 deletions(-) diff --git a/.env.example b/.env.example index 65fdf1db..7fdd4f9c 100644 --- a/.env.example +++ b/.env.example @@ -17,8 +17,8 @@ GOOGLE_CLIENT_SECRET=your_google_client_secret GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret -# Flask configuration -FLASK_SECRET_KEY=your_super_secret_key_here +# FASTAPI configuration +FASTAPI_SECRET_KEY=your_super_secret_key_here # FalkorDB configuration FALKORDB_HOST=localhost diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index aee7722e..5d9bdd66 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -126,8 +126,8 @@ make clean ```bash # REQUIRED for FastAPI to start -FLASK_SECRET_KEY=your_super_secret_key_here -FLASK_DEBUG=False +FASTAPI_SECRET_KEY=your_super_secret_key_here +FASTAPI_DEBUG=False # REQUIRED for database connection (most functionality) FALKORDB_HOST=localhost @@ -146,8 +146,8 @@ GITHUB_CLIENT_SECRET=your_github_client_secret **For testing in CI/development**, minimal `.env` setup: ```bash -FLASK_SECRET_KEY=test-secret-key -FLASK_DEBUG=False +FASTAPI_SECRET_KEY=test-secret-key +FASTAPI_DEBUG=False FALKORDB_HOST=localhost FALKORDB_PORT=6379 ``` diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index c3354bd6..360a3bde 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,8 +55,8 @@ jobs: cp .env.example .env echo "FALKORDB_HOST=localhost" >> .env echo "FALKORDB_PORT=6379" >> .env - echo "FLASK_SECRET_KEY=test-secret-key-for-ci" >> .env - echo "FLASK_DEBUG=False" >> .env + echo "FASTAPI_SECRET_KEY=test-secret-key-for-ci" >> .env + echo "FASTAPI_DEBUG=False" >> .env - name: Wait for FalkorDB run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e951c298..fc2f2443 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,8 +51,8 @@ jobs: - name: Create test environment file run: | cp .env.example .env - echo "FLASK_SECRET_KEY=test-secret-key" >> .env - echo "FLASK_DEBUG=False" >> .env + echo "FASTAPI_SECRET_KEY=test-secret-key" >> .env + echo "FASTAPI_DEBUG=False" >> .env - name: Run unit tests run: | @@ -109,8 +109,8 @@ jobs: cp .env.example .env echo "FALKORDB_HOST=localhost" >> .env echo "FALKORDB_PORT=6379" >> .env - echo "FLASK_SECRET_KEY=test-secret-key-for-ci" >> .env - echo "FLASK_DEBUG=False" >> .env + echo "FASTAPI_SECRET_KEY=test-secret-key-for-ci" >> .env + echo "FASTAPI_DEBUG=False" >> .env - name: Wait for FalkorDB run: | diff --git a/README.md b/README.md index 49ce7314..207e3bb8 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ This application supports authentication via Google and GitHub OAuth. You'll nee ### Running the Application ```bash -pipenv run flask --app api.index run +pipenv run uvicorn api.index:app --host "localhost" --port "5000" ``` The application will be available at `http://localhost:5000`. @@ -97,7 +97,7 @@ You can configure the application by passing environment variables using the `-e ```bash docker run -p 5000:5000 -it \ - -e FLASK_SECRET_KEY=your_super_secret_key_here \ + -e FASTAPI_SECRET_KEY=your_super_secret_key_here \ -e GOOGLE_CLIENT_ID=your_google_client_id \ -e GOOGLE_CLIENT_SECRET=your_google_client_secret \ -e GITHUB_CLIENT_ID=your_github_client_id \ diff --git a/api/app_factory.py b/api/app_factory.py index 25c3ceee..34ac5a74 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -46,10 +46,10 @@ def create_app(): app = FastAPI(title="QueryWeaver", description="Text2SQL with Graph-Powered Schema Understanding") # Get secret key for sessions - secret_key = os.getenv("FLASK_SECRET_KEY") + secret_key = os.getenv("FASTAPI_SECRET_KEY") if not secret_key: secret_key = secrets.token_hex(32) - logging.warning("FLASK_SECRET_KEY not set, using generated key. Set this in production!") + logging.warning("FASTAPI_SECRET_KEY not set, using generated key. Set this in production!") # Add session middleware with explicit settings to ensure OAuth state persists app.add_middleware( diff --git a/api/auth/user_management.py b/api/auth/user_management.py index a4aa7980..3e2ffc9a 100644 --- a/api/auth/user_management.py +++ b/api/auth/user_management.py @@ -298,7 +298,7 @@ async def wrapper(request: Request, *args, **kwargs): detail="Unauthorized - Please log in" ) - # Attach user_id to request.state (like Flask's g.user_id) + # Attach user_id to request.state (like FASTAPI's g.user_id) request.state.user_id = user_info.get("id") if not request.state.user_id: request.session.pop("user_info", None) diff --git a/api/index.py b/api/index.py index 8b30c4e8..e845047d 100644 --- a/api/index.py +++ b/api/index.py @@ -8,7 +8,8 @@ import os import uvicorn - debug_mode = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true' + # Read FASTAPI_DEBUG to determine debug mode + debug_mode = os.environ.get('FASTAPI_DEBUG', 'False').lower() == 'true' uvicorn.run( "api.index:app", host="127.0.0.1", @@ -17,5 +18,5 @@ log_level="info" if debug_mode else "warning" ) # This allows running the app with `uvicorn api.index:app` or directly with `python api/index.py` -# Ensure the environment variable FLASK_DEBUG is set to 'True' for debug mode +# Ensure the environment variable FASTAPI_DEBUG is set to 'True' for debug mode # or 'False' for production mode. diff --git a/app/README.md b/app/README.md index 9bebaa41..934d490c 100644 --- a/app/README.md +++ b/app/README.md @@ -9,7 +9,7 @@ npm install npm run build ``` -This will bundle the frontend and place the result in `api/static/dist/bundle.js` which your Flask templates should load via `/static/dist/bundle.js`. +This will bundle the frontend and place the result in `api/static/dist/bundle.js` which your FASTAPI templates should load via `/static/dist/bundle.js`. Notes: - Keep original JS files in `api/static/js/` for backward compatibility until you update templates. diff --git a/start.sh b/start.sh index 3b655a06..3e840cb9 100644 --- a/start.sh +++ b/start.sh @@ -18,9 +18,9 @@ done echo "FalkorDB is up - launching FastAPI..." -# Determine whether to run in reload (debug) mode. The project uses FLASK_DEBUG +# Determine whether to run in reload (debug) mode. The project uses FASTAPI_DEBUG # environment variable historically; keep compatibility by honoring it here. -if [ "${FLASK_DEBUG:-False}" = "True" ] || [ "${FLASK_DEBUG:-true}" = "true" ]; then +if [ "${FASTAPI_DEBUG:-False}" = "True" ] || [ "${FASTAPI_DEBUG:-true}" = "true" ]; then RELOAD_FLAG="--reload" else RELOAD_FLAG="" diff --git a/tests/e2e/README.md b/tests/e2e/README.md index e7584342..5afe5eb1 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -117,9 +117,9 @@ These test the API directly: Key environment variables for testing: ```bash -# Required for Flask -FLASK_SECRET_KEY=your-secret-key -FLASK_DEBUG=False +# Required for FastAPI +FASTAPI_SECRET_KEY=your-secret-key +FASTAPI_DEBUG=False # Database connection (optional for basic tests) FALKORDB_HOST=localhost From 56b9c368a4d283fcc6e33652de6daba7bdcacb34 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 22:43:25 +0300 Subject: [PATCH 03/12] update deps --- Pipfile | 6 +- Pipfile.lock | 156 +++++++++++++++++++++++++-------------------------- 2 files changed, 80 insertions(+), 82 deletions(-) diff --git a/Pipfile b/Pipfile index c53905eb..7c2c0c2e 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] fastapi = "~=0.116.1" uvicorn = "~=0.32.0" -litellm = "~=1.74.14" +litellm = "~=1.75.9" falkordb = "~=1.2.0" psycopg2-binary = "~=2.9.9" pymysql = "~=1.1.0" @@ -20,9 +20,9 @@ jinja2 = "~=3.1.4" [dev-packages] pytest = "~=8.4.1" pylint = "~=3.3.4" -playwright = "~=1.47.0" +playwright = "~=1.54.0" pytest-playwright = "~=0.7.0" -pytest-asyncio = "~=0.24.0" +pytest-asyncio = "~=1.1.0" [requires] python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock index 73be691d..09375fad 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a601aec67637af6bc6737f23f2c7e8496d620baa4c6b0553d335e33c352be31c" + "sha256": "4573e7a507f4e8c5c10787062953a7b4219cdf0ee3f0a3130c36c4abdd067f51" }, "pipfile-spec": 6, "requires": { @@ -708,11 +708,12 @@ }, "litellm": { "hashes": [ - "sha256:8eddb1c8a6a5a7048f8ba16e652aba23d6ca996dd87cb853c874ba375aa32479" + "sha256:a72c3e05bcb0e50ac1804f0df09d0d7bf5cb41e84351e1609a960033b0ef01c1", + "sha256:d8baf4b9988df599b55cb675808bbe22cedee2f099ba883684fe3f23af8d13a9" ], "index": "pypi", "markers": "python_version not in '2.7, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7' and python_version >= '3.8'", - "version": "==1.74.15.post2" + "version": "==1.75.9" }, "markupsafe": { "hashes": [ @@ -1947,67 +1948,63 @@ }, "greenlet": { "hashes": [ - "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67", - "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6", - "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257", - "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4", - "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676", - "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61", - "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc", - "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca", - "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7", - "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728", - "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305", - "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6", - "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379", - "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414", - "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04", - "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a", - "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf", - "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491", - "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559", - "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e", - "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274", - "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb", - "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b", - "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9", - "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b", - "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be", - "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506", - "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405", - "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113", - "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f", - "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5", - "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230", - "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d", - "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f", - "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a", - "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e", - "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61", - "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6", - "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d", - "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71", - "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22", - "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2", - "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3", - "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067", - "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc", - "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881", - "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3", - "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e", - "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac", - "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53", - "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0", - "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b", - "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83", - "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41", - "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c", - "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf", - "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da", - "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33" + "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", + "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", + "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079", + "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", + "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433", + "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58", + "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", + "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", + "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246", + "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", + "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", + "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", + "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d", + "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", + "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", + "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", + "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", + "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", + "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633", + "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", + "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", + "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31", + "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9", + "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b", + "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4", + "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", + "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c", + "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98", + "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f", + "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c", + "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590", + "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3", + "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2", + "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", + "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5", + "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", + "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", + "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", + "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c", + "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594", + "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", + "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d", + "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", + "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6", + "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b", + "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df", + "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", + "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", + "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb", + "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", + "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", + "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", + "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c", + "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968" ], - "markers": "python_version >= '3.7'", - "version": "==3.0.3" + "markers": "python_version >= '3.9'", + "version": "==3.2.4" }, "idna": { "hashes": [ @@ -2059,17 +2056,18 @@ }, "playwright": { "hashes": [ - "sha256:0ec1056042d2e86088795a503347407570bffa32cbe20748e5d4c93dba085280", - "sha256:1b977ed81f6bba5582617684a21adab9bad5676d90a357ebf892db7bdf4a9974", - "sha256:7fc820faf6885f69a52ba4ec94124e575d3c4a4003bf29200029b4a4f2b2d0ab", - "sha256:8e212dc472ff19c7d46ed7e900191c7a786ce697556ac3f1615986ec3aa00341", - "sha256:a1935672531963e4b2a321de5aa59b982fb92463ee6e1032dd7326378e462955", - "sha256:e0a1b61473d6f7f39c5d77d4800b3cbefecb03344c90b98f3fbcae63294ad249", - "sha256:f205df24edb925db1a4ab62f1ab0da06f14bb69e382efecfb0deedc4c7f4b8cd" + "sha256:09919f45cc74c64afb5432646d7fef0d19fff50990c862cb8d9b0577093f40cc", + "sha256:0b108622ffb6906e28566f3f31721cd57dda637d7e41c430287804ac01911f56", + "sha256:13ae206c55737e8e3eae51fb385d61c0312eeef31535643bb6232741b41b6fdc", + "sha256:780928b3ca2077aea90414b37e54edd0c4bbb57d1aafc42f7aa0b3fd2c2fac02", + "sha256:81d0b6f28843b27f288cfe438af0a12a4851de57998009a519ea84cee6fbbfb9", + "sha256:9e5aee9ae5ab1fdd44cd64153313a2045b136fcbcfb2541cc0a3d909132671a2", + "sha256:a975815971f7b8dca505c441a4c56de1aeb56a211290f8cc214eeef5524e8d75", + "sha256:bf3b845af744370f1bd2286c2a9536f474cc8a88dc995b72ea9a5be714c9a77d" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.47.0" + "markers": "python_version >= '3.9'", + "version": "==1.54.0" }, "pluggy": { "hashes": [ @@ -2081,11 +2079,11 @@ }, "pyee": { "hashes": [ - "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990", - "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145" + "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498", + "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37" ], "markers": "python_version >= '3.8'", - "version": "==12.0.0" + "version": "==13.0.0" }, "pygments": { "hashes": [ @@ -2115,12 +2113,12 @@ }, "pytest-asyncio": { "hashes": [ - "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b", - "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276" + "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", + "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.24.0" + "markers": "python_version >= '3.9'", + "version": "==1.1.0" }, "pytest-base-url": { "hashes": [ From e655e7513e6fd1cd4827089c00938ca2fe2c4e03 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 23:03:23 +0300 Subject: [PATCH 04/12] clean lint --- api/auth/__init__.py | 10 +++++-- api/auth/oauth_handlers.py | 42 ++++++++++++++++++--------- api/index.py | 2 +- tests/conftest.py | 10 +++---- tests/e2e/test_api_endpoints.py | 3 +- tests/e2e/test_basic_functionality.py | 8 ++--- tests/test_mysql_loader.py | 38 ++++++++++++++---------- 7 files changed, 69 insertions(+), 44 deletions(-) diff --git a/api/auth/__init__.py b/api/auth/__init__.py index 14e4eeeb..853c3c6b 100644 --- a/api/auth/__init__.py +++ b/api/auth/__init__.py @@ -1,10 +1,14 @@ -# Authentication module for text2sql API +"""Authentication helpers exported by the auth package. + +This module exposes commonly used authentication helpers for the +application and keeps the package's public API tidy. +""" from .user_management import ( ensure_user_in_organizations, update_identity_last_login, validate_and_cache_user, - token_required + token_required, ) from .oauth_handlers import setup_oauth_handlers @@ -13,5 +17,5 @@ "update_identity_last_login", "validate_and_cache_user", "token_required", - "setup_oauth_handlers" + "setup_oauth_handlers", ] diff --git a/api/auth/oauth_handlers.py b/api/auth/oauth_handlers.py index 8b7d894f..4e58c5a9 100644 --- a/api/auth/oauth_handlers.py +++ b/api/auth/oauth_handlers.py @@ -1,10 +1,12 @@ -"""OAuth signal handlers for Google and GitHub authentication.""" +"""OAuth signal handlers for Google and GitHub authentication. + +Lightweight handlers are stored on the FastAPI app state so route +callbacks can invoke them when processing OAuth responses. +""" import logging -import time from typing import Dict, Any -import requests from fastapi import FastAPI, Request from authlib.integrations.starlette_client import OAuth @@ -13,11 +15,13 @@ def setup_oauth_handlers(app: FastAPI, oauth: OAuth): """Set up OAuth handlers for both Google and GitHub.""" - + # Store oauth in app state for access in routes app.state.oauth = oauth - - async def handle_google_callback(request: Request, token: Dict[str, Any], user_info: Dict[str, Any]): + + async def handle_google_callback(_request: Request, + _token: Dict[str, Any], + user_info: Dict[str, Any]): """Handle Google OAuth callback processing""" try: user_id = user_info.get("id") @@ -31,15 +35,21 @@ async def handle_google_callback(request: Request, token: Dict[str, Any], user_i # Check if identity exists in Organizations graph, create if new _, _ = ensure_user_in_organizations( - user_id, email, name, "google", user_info.get("picture") + user_id, + email, + name, + "google", + user_info.get("picture"), ) return True - except Exception as e: - logging.error("Error handling Google OAuth callback: %s", e) + except Exception as exc: # capture exception for logging + logging.error("Error handling Google OAuth callback: %s", exc) return False - async def handle_github_callback(request: Request, token: Dict[str, Any], user_info: Dict[str, Any]): + async def handle_github_callback(_request: Request, + _token: Dict[str, Any], + user_info: Dict[str, Any]): """Handle GitHub OAuth callback processing""" try: user_id = user_info.get("id") @@ -53,14 +63,18 @@ async def handle_github_callback(request: Request, token: Dict[str, Any], user_i # Check if identity exists in Organizations graph, create if new _, _ = ensure_user_in_organizations( - user_id, email, name, "github", user_info.get("picture") + user_id, + email, + name, + "github", + user_info.get("picture"), ) return True - except Exception as e: - logging.error("Error handling GitHub OAuth callback: %s", e) + except Exception as exc: # capture exception for logging + logging.error("Error handling GitHub OAuth callback: %s", exc) return False - + # Store handlers in app state for use in route callbacks app.state.google_callback_handler = handle_google_callback app.state.github_callback_handler = handle_github_callback diff --git a/api/index.py b/api/index.py index e845047d..c13a541f 100644 --- a/api/index.py +++ b/api/index.py @@ -15,7 +15,7 @@ host="127.0.0.1", port=5000, reload=debug_mode, - log_level="info" if debug_mode else "warning" + log_level="info" if debug_mode else "warning", ) # This allows running the app with `uvicorn api.index:app` or directly with `python api/index.py` # Ensure the environment variable FASTAPI_DEBUG is set to 'True' for debug mode diff --git a/tests/conftest.py b/tests/conftest.py index a4d97da8..a4c8f1ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,16 @@ -""" -Playwright configuration for E2E tests. -""" -import pytest +"""Playwright configuration for E2E tests.""" + +import os import subprocess import time + +import pytest import requests @pytest.fixture(scope="session") def fastapi_app(): """Start the FastAPI application for testing.""" - import os # Get the project root directory (parent of tests directory) current_dir = os.path.dirname(os.path.abspath(__file__)) diff --git a/tests/e2e/test_api_endpoints.py b/tests/e2e/test_api_endpoints.py index 4a338640..123e3cf9 100644 --- a/tests/e2e/test_api_endpoints.py +++ b/tests/e2e/test_api_endpoints.py @@ -1,6 +1,8 @@ """ Test API endpoints functionality. """ +import time + import pytest import requests @@ -73,7 +75,6 @@ def test_cors_headers(self, app_url): def test_response_times(self, app_url): """Test that response times are reasonable.""" - import time start_time = time.time() response = requests.get(app_url, timeout=10) diff --git a/tests/e2e/test_basic_functionality.py b/tests/e2e/test_basic_functionality.py index aa0a67a5..40efadd7 100644 --- a/tests/e2e/test_basic_functionality.py +++ b/tests/e2e/test_basic_functionality.py @@ -15,7 +15,9 @@ def test_home_page_loads(self, page_with_base_url): # Check that the page title contains QueryWeaver title = home_page.get_page_title() - assert "QueryWeaver" in title or "Text2SQL" in title + assert ( + "QueryWeaver" in title or "Text2SQL" in title + ) def test_application_structure(self, page_with_base_url): """Test that key UI elements are present.""" @@ -55,9 +57,7 @@ def test_file_upload_interface(self, page_with_base_url): home_page = HomePage(page_with_base_url) home_page.navigate_to_home() - # This test would require authentication - # Placeholder for when auth is set up - pass + # This test would require authentication; placeholder for future test def test_responsive_design(self, page_with_base_url): """Test responsive design at different screen sizes.""" diff --git a/tests/test_mysql_loader.py b/tests/test_mysql_loader.py index 895c374f..99bbaebf 100644 --- a/tests/test_mysql_loader.py +++ b/tests/test_mysql_loader.py @@ -1,7 +1,11 @@ """Tests for MySQL loader functionality.""" -import pytest +import datetime +import decimal from unittest.mock import patch, MagicMock + +import pytest + from api.loaders.mysql_loader import MySQLLoader @@ -58,23 +62,20 @@ def test_parse_mysql_url_missing_database(self): def test_serialize_value(self): """Test value serialization for JSON compatibility.""" - from datetime import datetime, date, time - from decimal import Decimal - # Test datetime - dt = datetime(2023, 1, 1, 12, 0, 0) + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) assert MySQLLoader._serialize_value(dt) == "2023-01-01T12:00:00" # Test date - d = date(2023, 1, 1) + d = datetime.date(2023, 1, 1) assert MySQLLoader._serialize_value(d) == "2023-01-01" # Test time - t = time(12, 0, 0) + t = datetime.time(12, 0, 0) assert MySQLLoader._serialize_value(t) == "12:00:00" # Test decimal - dec = Decimal("123.45") + dec = decimal.Decimal("123.45") assert MySQLLoader._serialize_value(dec) == 123.45 # Test None @@ -88,8 +89,10 @@ def test_is_schema_modifying_query(self): # Schema-modifying queries assert MySQLLoader.is_schema_modifying_query("CREATE TABLE test (id INT)")[0] is True assert MySQLLoader.is_schema_modifying_query("DROP TABLE test")[0] is True - assert MySQLLoader.is_schema_modifying_query("ALTER TABLE test ADD COLUMN name VARCHAR(50)")[0] is True - assert MySQLLoader.is_schema_modifying_query(" CREATE INDEX idx_name ON test(name)")[0] is True + assert MySQLLoader.is_schema_modifying_query( + "ALTER TABLE test ADD COLUMN name VARCHAR(50)")[0] is True + assert MySQLLoader.is_schema_modifying_query( + " CREATE INDEX idx_name ON test(name)")[0] is True # Non-schema-modifying queries assert MySQLLoader.is_schema_modifying_query("SELECT * FROM test")[0] is False @@ -99,16 +102,16 @@ def test_is_schema_modifying_query(self): # Edge cases assert MySQLLoader.is_schema_modifying_query("")[0] is False - assert MySQLLoader.is_schema_modifying_query(None)[0] is False + assert MySQLLoader.is_schema_modifying_query("")[0] is False @patch('pymysql.connect') def test_connection_error(self, mock_connect): """Test handling of MySQL connection errors.""" # Mock connection failure mock_connect.side_effect = Exception("Connection failed") - + success, message = MySQLLoader.load("test_prefix", "mysql://user:pass@host:3306/db") - + assert success is False assert "Error loading MySQL schema" in message @@ -126,10 +129,13 @@ def test_successful_load(self, mock_load_to_graph, mock_connect): mock_connect.return_value = mock_conn # Mock the extract methods to return minimal data - with patch.object(MySQLLoader, 'extract_tables_info', return_value={'users': {'description': 'User table'}}): + with patch.object(MySQLLoader, 'extract_tables_info', + return_value={'users': {'description': 'User table'}}): with patch.object(MySQLLoader, 'extract_relationships', return_value={}): - success, message = MySQLLoader.load("test_prefix", "mysql://user:pass@localhost:3306/testdb") + success, message = MySQLLoader.load( + "test_prefix", "mysql://user:pass@localhost:3306/testdb" + ) assert success is True assert "MySQL schema loaded successfully" in message - mock_load_to_graph.assert_called_once() \ No newline at end of file + mock_load_to_graph.assert_called_once() From 9c2029c4433ae9e4a06c8cbbe497637c2a0fa440 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 23:12:28 +0300 Subject: [PATCH 05/12] fix lints --- api/app_factory.py | 8 +++++++- api/loaders/mysql_loader.py | 5 ++++- api/routes/auth.py | 14 +++++++++++--- api/routes/database.py | 28 ++++++++++++++++++--------- api/routes/graphs.py | 22 +++++++++++++++------ tests/e2e/test_basic_functionality.py | 6 ++++-- tests/e2e/test_chat_functionality.py | 5 ++++- 7 files changed, 65 insertions(+), 23 deletions(-) diff --git a/api/app_factory.py b/api/app_factory.py index 34ac5a74..cb53445d 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -43,7 +43,13 @@ async def dispatch(self, request: Request, call_next): def create_app(): """Create and configure the FastAPI application.""" - app = FastAPI(title="QueryWeaver", description="Text2SQL with Graph-Powered Schema Understanding") + app = FastAPI( + title="QueryWeaver", + description=( + "Text2SQL with " + "Graph-Powered Schema Understanding" + ), + ) # Get secret key for sessions secret_key = os.getenv("FASTAPI_SECRET_KEY") diff --git a/api/loaders/mysql_loader.py b/api/loaders/mysql_loader.py index 6d453860..24577d18 100644 --- a/api/loaders/mysql_loader.py +++ b/api/loaders/mysql_loader.py @@ -83,7 +83,10 @@ def _parse_mysql_url(connection_url: str) -> Dict[str, str]: if connection_url.startswith('mysql://'): url = connection_url[8:] else: - raise ValueError("Invalid MySQL URL format. Expected mysql://username:password@host:port/database") + raise ValueError( + "Invalid MySQL URL format. Expected " + "mysql://username:password@host:port/database" + ) # Parse components if '@' not in url: diff --git a/api/routes/auth.py b/api/routes/auth.py index e3dd420b..5ada36c3 100644 --- a/api/routes/auth.py +++ b/api/routes/auth.py @@ -34,7 +34,13 @@ def _get_provider_client(request: Request, provider: str): def _clear_auth_session(session: dict): """Remove only auth-related keys from session instead of clearing everything.""" - for key in ["user_info", "google_token", "github_token", "token_validated_at", "oauth_google_auth"]: + for key in [ + "user_info", + "google_token", + "github_token", + "token_validated_at", + "oauth_google_auth", + ]: session.pop(key, None) @auth_router.get("/chat", name="auth.chat", response_class=HTMLResponse) @@ -148,7 +154,8 @@ async def google_authorized(request: Request) -> RedirectResponse: @auth_router.get("/login/google/callback", response_class=RedirectResponse) async def google_callback_compat(request: Request) -> RedirectResponse: qs = f"?{request.url.query}" if request.url.query else "" - return RedirectResponse(url=f"/login/google/authorized{qs}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) + redirect = f"/login/google/authorized{qs}" + return RedirectResponse(url=redirect, status_code=status.HTTP_307_TEMPORARY_REDIRECT) @auth_router.get("/login/github", name="github.login", response_class=RedirectResponse) @@ -221,7 +228,8 @@ async def github_authorized(request: Request) -> RedirectResponse: @auth_router.get("/login/github/callback", response_class=RedirectResponse) async def github_callback_compat(request: Request) -> RedirectResponse: qs = f"?{request.url.query}" if request.url.query else "" - return RedirectResponse(url=f"/login/github/authorized{qs}", status_code=status.HTTP_307_TEMPORARY_REDIRECT) + redirect = f"/login/github/authorized{qs}" + return RedirectResponse(url=redirect, status_code=status.HTTP_307_TEMPORARY_REDIRECT) @auth_router.get("/logout", response_class=RedirectResponse) diff --git a/api/routes/database.py b/api/routes/database.py index cd4bcac9..0d0f0c9e 100644 --- a/api/routes/database.py +++ b/api/routes/database.py @@ -1,8 +1,7 @@ """Database connection routes for the text2sql API.""" import logging -from typing import Dict, Any -from fastapi import APIRouter, Request, HTTPException, status +from fastapi import APIRouter, Request, HTTPException from fastapi.responses import JSONResponse from pydantic import BaseModel @@ -36,7 +35,7 @@ async def connect_database(request: Request, db_request: DatabaseConnectionReque try: success = False result = "" - + # Check for PostgreSQL URL if url.startswith("postgres://") or url.startswith("postgresql://"): try: @@ -44,8 +43,11 @@ async def connect_database(request: Request, db_request: DatabaseConnectionReque success, result = PostgresLoader.load(request.state.user_id, url) except (ValueError, ConnectionError) as e: logging.error("PostgreSQL connection error: %s", str(e)) - raise HTTPException(status_code=500, detail="Failed to connect to PostgreSQL database") - + raise HTTPException( + status_code=500, + detail="Failed to connect to PostgreSQL database", + ) + # Check for MySQL URL elif url.startswith("mysql://"): try: @@ -53,10 +55,18 @@ async def connect_database(request: Request, db_request: DatabaseConnectionReque success, result = MySQLLoader.load(request.state.user_id, url) except (ValueError, ConnectionError) as e: logging.error("MySQL connection error: %s", str(e)) - raise HTTPException(status_code=500, detail="Failed to connect to MySQL database") - + raise HTTPException( + status_code=500, detail="Failed to connect to MySQL database" + ) + else: - raise HTTPException(status_code=400, detail="Invalid database URL. Supported formats: postgresql:// or mysql://") + raise HTTPException( + status_code=400, + detail=( + "Invalid database URL. Supported formats: postgresql:// " + "or mysql://" + ), + ) if success: return JSONResponse(content={ @@ -67,7 +77,7 @@ async def connect_database(request: Request, db_request: DatabaseConnectionReque # Don't return detailed error messages to prevent information exposure logging.error("Database loader failed: %s", result) raise HTTPException(status_code=400, detail="Failed to load database schema") - + except (ValueError, TypeError) as e: logging.error("Unexpected error in database connection: %s", str(e)) raise HTTPException(status_code=500, detail="Internal server error") diff --git a/api/routes/graphs.py b/api/routes/graphs.py index 9a05787e..fde46b50 100644 --- a/api/routes/graphs.py +++ b/api/routes/graphs.py @@ -5,9 +5,8 @@ import time from concurrent.futures import ThreadPoolExecutor from concurrent.futures import TimeoutError as FuturesTimeoutError -from typing import Dict, Any -from fastapi import APIRouter, Request, HTTPException, status, UploadFile, File, Form +from fastapi import APIRouter, Request, HTTPException, UploadFile, File from fastapi.responses import JSONResponse, StreamingResponse from pydantic import BaseModel @@ -328,7 +327,10 @@ async def generate(): logging.info("Finding relevant tables took %.2f seconds", find_elapsed) # Total time for the pre-analysis phase step1_elapsed = time.perf_counter() - step1_start - logging.info("Step 1 (relevancy + table finding) took %.2f seconds", step1_elapsed) + logging.info( + "Step 1 (relevancy + table finding) took %.2f seconds", + step1_elapsed, + ) except FuturesTimeoutError: yield json.dumps( { @@ -506,13 +508,21 @@ async def generate(): @graphs_router.post("/{graph_id}/confirm") @token_required -async def confirm_destructive_operation(request: Request, graph_id: str, confirm_data: ConfirmRequest): +async def confirm_destructive_operation( + request: Request, + graph_id: str, + confirm_data: ConfirmRequest, +): """ Handle user confirmation for destructive SQL operations """ graph_id = request.state.user_id + "_" + graph_id.strip() - - confirmation = confirm_data.confirmation.strip().upper() if hasattr(confirm_data, 'confirmation') else "" + + if hasattr(confirm_data, 'confirmation'): + confirmation = confirm_data.confirmation.strip().upper() + else: + confirmation = "" + sql_query = confirm_data.sql_query if hasattr(confirm_data, 'sql_query') else "" queries_history = confirm_data.chat if hasattr(confirm_data, 'chat') else [] diff --git a/tests/e2e/test_basic_functionality.py b/tests/e2e/test_basic_functionality.py index 40efadd7..371535d0 100644 --- a/tests/e2e/test_basic_functionality.py +++ b/tests/e2e/test_basic_functionality.py @@ -90,5 +90,7 @@ def test_error_handling(self, page_with_base_url): # Should handle 404 gracefully # Could be 404 page or redirect to home - response_status = page.evaluate("() => window.fetch('/nonexistent-route').then(r => r.status)") - assert response_status in [404, 302, 200] # Various valid responses + response_status = page.evaluate( + "() => window.fetch('/nonexistent-route').then(r => r.status)" + ) + assert response_status in [404, 302, 200] diff --git a/tests/e2e/test_chat_functionality.py b/tests/e2e/test_chat_functionality.py index d9d3001b..d9702c65 100644 --- a/tests/e2e/test_chat_functionality.py +++ b/tests/e2e/test_chat_functionality.py @@ -86,7 +86,10 @@ def test_input_validation(self, page_with_base_url): long_text = "a" * 1000 # Try to find any visible and enabled text input - enabled_inputs = page.locator("input[type='text']:not([disabled]):visible, textarea:not([disabled]):visible").all() + enabled_inputs = page.locator( + "input[type='text']:not([disabled]):visible, " + "textarea:not([disabled]):visible", + ).all() if enabled_inputs: # Get the first enabled input element From 9d42263769f7ef506c9bab1280d414731b4ba71c Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 23:28:59 +0300 Subject: [PATCH 06/12] clean lints --- api/routes/database.py | 5 +++++ api/routes/graphs.py | 15 +++++++++++++++ app/.eslintrc.cjs | 14 -------------- app/eslint.config.cjs | 31 +++++++++++++++++++++++++++++++ app/ts/modules/chat.ts | 5 ++--- app/ts/modules/schema.ts | 10 +++++----- 6 files changed, 58 insertions(+), 22 deletions(-) delete mode 100644 app/.eslintrc.cjs create mode 100644 app/eslint.config.cjs diff --git a/api/routes/database.py b/api/routes/database.py index 0d0f0c9e..4254d45f 100644 --- a/api/routes/database.py +++ b/api/routes/database.py @@ -13,6 +13,11 @@ class DatabaseConnectionRequest(BaseModel): + """Database connection request model. + + Args: + BaseModel (_type_): _description_ + """ url: str diff --git a/api/routes/graphs.py b/api/routes/graphs.py index fde46b50..6fbab7fb 100644 --- a/api/routes/graphs.py +++ b/api/routes/graphs.py @@ -27,16 +27,31 @@ class GraphData(BaseModel): + """Graph data model. + + Args: + BaseModel (_type_): _description_ + """ database: str class ChatRequest(BaseModel): + """Chat request model. + + Args: + BaseModel (_type_): _description_ + """ chat: list result: list = None instructions: str = None class ConfirmRequest(BaseModel): + """Confirmation request model. + + Args: + BaseModel (_type_): _description_ + """ sql_query: str confirmation: str = "" chat: list = [] diff --git a/app/.eslintrc.cjs b/app/.eslintrc.cjs deleted file mode 100644 index 486fa0f3..00000000 --- a/app/.eslintrc.cjs +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - root: true, - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module' - }, - plugins: ['@typescript-eslint'], - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], - rules: { - // customize as needed - '@typescript-eslint/no-explicit-any': 'off' - } -}; diff --git a/app/eslint.config.cjs b/app/eslint.config.cjs new file mode 100644 index 00000000..b054baed --- /dev/null +++ b/app/eslint.config.cjs @@ -0,0 +1,31 @@ +// ESLint v9 flat config equivalent for the project's TypeScript rules +module.exports = [ + { + ignores: ['**/node_modules/**', 'dist/**'], + }, + { + languageOptions: { + parser: require('@typescript-eslint/parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module', + }, + }, + }, + { + plugins: { + '@typescript-eslint': require('@typescript-eslint/eslint-plugin'), + }, + }, + { + rules: { + // Base JS recommended rules + 'no-unused-vars': 'warn', + // TypeScript rules + '@typescript-eslint/no-explicit-any': 'off', + }, + linterOptions: { + reportUnusedDisableDirectives: true, + }, + }, +]; diff --git a/app/ts/modules/chat.ts b/app/ts/modules/chat.ts index a9cd41b4..9c4157a9 100644 --- a/app/ts/modules/chat.ts +++ b/app/ts/modules/chat.ts @@ -73,7 +73,6 @@ async function processStreamingResponse(response: Response) { const decoder = new TextDecoder(); let buffer = ''; - // eslint-disable-next-line no-constant-condition while (true) { const { done, value } = await reader.read(); if (done) { @@ -81,7 +80,7 @@ async function processStreamingResponse(response: Response) { try { const step = JSON.parse(buffer); addMessage(step.message || JSON.stringify(step), false); - } catch (e) { + } catch { addMessage(buffer, false); } } @@ -101,7 +100,7 @@ async function processStreamingResponse(response: Response) { try { const step = JSON.parse(message); handleStreamMessage(step); - } catch (e) { + } catch { addMessage('Failed: ' + message, false); } } diff --git a/app/ts/modules/schema.ts b/app/ts/modules/schema.ts index bdbc0992..f5550db5 100644 --- a/app/ts/modules/schema.ts +++ b/app/ts/modules/schema.ts @@ -86,9 +86,9 @@ export function showGraph(data: any) { const L = 0.2126 * r + 0.7152 * g + 0.0722 * b; return L > 0.6 ? '#111' : '#ffffff'; } - } catch (e) { - // ignore - } + } catch { + // ignore + } return '#ffffff'; }; @@ -127,7 +127,7 @@ export function showGraph(data: any) { const L = 0.2126 * r + 0.7152 * g + 0.0722 * b; return L > 0.6 ? '#111' : '#ffffff'; } - } catch (e) { /* empty */ } + } catch { /* empty */ } return '#ffffff'; }; const edgeColor = (() => { @@ -139,7 +139,7 @@ export function showGraph(data: any) { Graph.linkColor(() => edgeColor) .linkDirectionalArrowLength(6).linkDirectionalArrowRelPos(1); - } catch (e) { + } catch { Graph.linkDirectionalArrowLength(6).linkDirectionalArrowRelPos(1); } } From c2793341dbe35cf0066466461e0f9d612b3d5b2a Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 23:33:54 +0300 Subject: [PATCH 07/12] fix --- .github/workflows/e2e-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 360a3bde..c3354bd6 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,8 +55,8 @@ jobs: cp .env.example .env echo "FALKORDB_HOST=localhost" >> .env echo "FALKORDB_PORT=6379" >> .env - echo "FASTAPI_SECRET_KEY=test-secret-key-for-ci" >> .env - echo "FASTAPI_DEBUG=False" >> .env + echo "FLASK_SECRET_KEY=test-secret-key-for-ci" >> .env + echo "FLASK_DEBUG=False" >> .env - name: Wait for FalkorDB run: | From 890876a5ecceb02f8c27cf01b6cf826813c58c3e Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 23:36:38 +0300 Subject: [PATCH 08/12] fix tests --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fc2f2443..6aa82345 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -51,8 +51,8 @@ jobs: - name: Create test environment file run: | cp .env.example .env - echo "FASTAPI_SECRET_KEY=test-secret-key" >> .env - echo "FASTAPI_DEBUG=False" >> .env + echo "FASTAPI_SECRET_KEY=test-secret-key" >> .env + echo "FASTAPI_DEBUG=False" >> .env - name: Run unit tests run: | From 61897fe95589a75f00d9b14569b5b462e70d324b Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 23:38:00 +0300 Subject: [PATCH 09/12] fix tests --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6aa82345..1f6920df 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -109,8 +109,8 @@ jobs: cp .env.example .env echo "FALKORDB_HOST=localhost" >> .env echo "FALKORDB_PORT=6379" >> .env - echo "FASTAPI_SECRET_KEY=test-secret-key-for-ci" >> .env - echo "FASTAPI_DEBUG=False" >> .env + echo "FASTAPI_SECRET_KEY=test-secret-key-for-ci" >> .env + echo "FASTAPI_DEBUG=False" >> .env - name: Wait for FalkorDB run: | From 80d931582b1bfd014a13d420a6f5831d1f6c3501 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Thu, 21 Aug 2025 23:42:28 +0300 Subject: [PATCH 10/12] fix tests --- .github/workflows/tests.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1f6920df..4997da6d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -42,7 +42,6 @@ jobs: pipenv sync --dev - name: Install frontend dependencies - if: "exists('app/package.json')" run: | node --version || true npm --version || true @@ -58,14 +57,9 @@ jobs: run: | pipenv run pytest tests/ -k "not e2e" --verbose - - name: Run pylint + - name: Run lint run: | - pipenv run pylint "$(git ls-files '*.py')" || true - - - name: Run frontend lint - if: "exists('app/package.json')" - run: | - (cd app && npm run lint) + make lint e2e-tests: runs-on: ubuntu-latest From c0639c762620a3d4b1792aa6e875b79324c43c72 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Fri, 22 Aug 2025 01:36:48 +0300 Subject: [PATCH 11/12] add design --- app/public/css/landing.css | 115 +++++++++++++++++++++++++++++++++++ app/templates/base.j2 | 2 +- app/templates/landing.j2 | 119 ++++++++++++++++++++++++++++++------- 3 files changed, 215 insertions(+), 21 deletions(-) diff --git a/app/public/css/landing.css b/app/public/css/landing.css index 16f1188c..231ad072 100644 --- a/app/public/css/landing.css +++ b/app/public/css/landing.css @@ -42,6 +42,8 @@ color: #fff; text-decoration: none; box-shadow: 0 8px 20px rgba(91, 107, 192, 0.14); + transition: transform 140ms ease, box-shadow 140ms ease, filter 140ms ease; + cursor: pointer; } .btn-ghost { @@ -67,6 +69,20 @@ box-shadow: 0 0 0 3px rgba(91, 107, 192, 0.08); } +.btn-pill:hover { + transform: translateY(-4px); + box-shadow: 0 18px 36px rgba(91, 107, 192, 0.18); +} + +.btn-pill:active { + transform: translateY(-1px) scale(0.997); +} + +.btn-pill:focus { + outline: none; + box-shadow: 0 0 0 4px rgba(59,130,246,0.12); +} + .demo-card { background: var(--falkor-secondary); border-radius: 12px; @@ -81,6 +97,7 @@ border-radius: 8px; padding: 1rem; border: 1px solid var(--falkor-border-tertiary); + border: none; } .demo-label { @@ -137,6 +154,14 @@ line-height: 1.25; } +/* SQL token colors for demo code */ +.demo-sql .sql-keyword { color: #7c3aed; font-weight: 700; } +.demo-sql .sql-string { color: #059669; } +.demo-sql .sql-func { color: #2563eb; } +.demo-sql .sql-number { color: #b45309; } + +.demo-sql { white-space: pre-wrap; font-family: monospace; } + .demo-sql.typing { position: relative; } @@ -175,6 +200,22 @@ background: #e7f1ff; color: var(--falkor-primary); text-decoration: none; + border: none; +} + +.demo-cta .btn-full:hover { + transform: translateY(-3px); + box-shadow: 0 14px 30px rgba(11,18,32,0.12); + background: #d9ecff; +} + +.demo-cta .btn-full:active { + transform: translateY(-1px) scale(0.998); +} + +.demo-cta .btn-full:focus { + outline: none; + box-shadow: 0 0 0 4px rgba(59,130,246,0.08); } @media (max-width: 900px) { @@ -187,3 +228,77 @@ font-size: 2.4rem; } } + +/* Feature boxes row */ +.features-row { + display: flex; + gap: 1rem; + margin-top: 4.5rem; + align-items: stretch; + justify-content: center; +} + +.feature-card { + width: 280px; + background: var(--falkor-secondary); + border-radius: 6px; + padding: 0.9rem 1rem; + border: 1px solid var(--falkor-border-tertiary); + box-shadow: 0 8px 20px rgba(11, 18, 32, 0.04); + text-align: center; + transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease; + border: none; +} + +.feature-card:hover { + transform: translateY(-6px); + box-shadow: 0 20px 40px rgba(11, 18, 32, 0.18); + border: solid 1px var(--falkor-border-secondary); +} + +.feature-card .feature-icon { + width: 36px; + height: 36px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: rgba(59,130,246,0.06); + margin: 0 auto 0.6rem auto; +} + +.feature-card:hover .feature-icon { + background: rgba(59,130,246,0.12); +} + +.feature-title { + font-size: 0.9rem; + margin: 0 0 0.35rem 0; + color: var(--text-primary); + font-weight: 700; +} + +.feature-desc { + font-size: 0.82rem; + color: var(--text-secondary); + margin: 0; + line-height: 1.3; +} + +.feature-highlight { + border: 1px solid rgba(59,130,246,0.15); +} + +@media (max-width: 900px) { + .features-row { + flex-direction: column; + gap: 0.75rem; + margin-top: 2rem; + align-items: center; + } + + .feature-card { + width: 100%; + max-width: 520px; + } +} diff --git a/app/templates/base.j2 b/app/templates/base.j2 index d1b578ac..ba07d051 100644 --- a/app/templates/base.j2 +++ b/app/templates/base.j2 @@ -1,5 +1,5 @@ - + diff --git a/app/templates/landing.j2 b/app/templates/landing.j2 index c7779620..d5dac08f 100644 --- a/app/templates/landing.j2 +++ b/app/templates/landing.j2 @@ -11,8 +11,9 @@
-

Transform Plain
English into Powerful
SQL.

-

The intelligent AI solution that connects your business questions to data across multiple databases. Ask a question in plain English and get runnable SQL and visual results.

+

Transform Plain
English into Powerful
SQL.

+

The intelligent AI solution that connects your business questions to data across multiple + databases. Ask a question in plain English and get runnable SQL and visual results.

Sign Up for Free @@ -24,14 +25,16 @@
+ + + +
+
+ - +

Knowledge Graph Power

+

Goes beyond simple column matching to understand complex business logic and relationships in + your data.

+
+ +
+ +

Multi-Database Joins

+

Ask a single question and get answers from disparate sources like PostgreSQL and Salesforce + in one go.

+
+ +
+ +

Proactive Guidance

+

Get intelligent suggestions to clarify ambiguity and optimize your queries before you even + run them.

+
+
-{% endblock %} +{% endblock %} \ No newline at end of file From 5ce6ddaec82a653d5ad09193c5501aa31e0aa1c9 Mon Sep 17 00:00:00 2001 From: Guy Korland Date: Fri, 22 Aug 2025 02:03:00 +0300 Subject: [PATCH 12/12] add header --- app/public/css/landing.css | 33 ++++++++++++++++++++++++++++++--- app/templates/landing.j2 | 8 +++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/app/public/css/landing.css b/app/public/css/landing.css index 231ad072..08acab18 100644 --- a/app/public/css/landing.css +++ b/app/public/css/landing.css @@ -8,6 +8,34 @@ padding: 0 1rem; } +/* Site header */ +.site-header { + width: 100%; + background: transparent; + border-bottom: solid 1px var(--falkor-border-secondary); +} + +.site-header-inner { + padding: 1rem; + display: inline-flex; + align-items: center; + gap: 0.4rem; + color: var(--text-primary); + text-decoration: none; + font-weight: 700; + padding-left: 0.25rem; +} + +.site-header-inner img { + height: 40px; + width: auto; + display: block; +} +.site-title { + font-size: 0.95rem; + display: none; +} + .hero-left { padding: 1rem 0; } @@ -84,7 +112,7 @@ } .demo-card { - background: var(--falkor-secondary); + background: var(--bg-tertiary); border-radius: 12px; box-shadow: 0 16px 30px rgba(11, 18, 32, 0.06); padding: 1rem; @@ -93,7 +121,6 @@ /* Use a neutral themed surface for the inner area so it adapts to light/dark */ .demo-inner { - background: var(--bg-tertiary); border-radius: 8px; padding: 1rem; border: 1px solid var(--falkor-border-tertiary); @@ -240,7 +267,7 @@ .feature-card { width: 280px; - background: var(--falkor-secondary); + background: var(--bg-tertiary); border-radius: 6px; padding: 0.9rem 1rem; border: 1px solid var(--falkor-border-tertiary); diff --git a/app/templates/landing.j2 b/app/templates/landing.j2 index d5dac08f..38e9479a 100644 --- a/app/templates/landing.j2 +++ b/app/templates/landing.j2 @@ -7,6 +7,13 @@ {% endblock %} {% block content %} + + +
@@ -123,7 +130,6 @@ clearInterval(typingTimer); typingTimer = null; sEl.classList.remove('typing'); - // ensure full highlighted SQL is rendered if (sEl) sEl.innerHTML = highlightSQL(text); // show success when typing completes if (successEl) successEl.style.display = 'flex';