From 5ac236bec8c5f3d60f2bc9d4dae562913c9a6329 Mon Sep 17 00:00:00 2001 From: Winz79 Date: Sat, 9 Aug 2025 18:32:05 +0200 Subject: [PATCH 1/3] Handle missing Notion token and fix pydantic compat --- app/main.py | 43 ++++++++++++++++++++++++++++++++++--------- requirements.txt | 2 +- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/app/main.py b/app/main.py index dc96927..b58cdd2 100644 --- a/app/main.py +++ b/app/main.py @@ -10,10 +10,22 @@ from notion_client import Client NOTION_TOKEN = os.getenv("NOTION_TOKEN") -if not NOTION_TOKEN: - raise RuntimeError("NOTION_TOKEN not found in environment. Please set it in .env or env vars") +# Only initialise the Notion client when a token is available. This allows +# the application (and its tests) to start without a configured token. The +# individual request handlers will raise an HTTP error if the client is not +# configured, which keeps the module import side-effect free and makes the +# module easier to test. +notion = Client(auth=NOTION_TOKEN) if NOTION_TOKEN else None -notion = Client(auth=NOTION_TOKEN) + +def _get_client() -> Client: + """Return the configured Notion client or raise an HTTP error.""" + if notion is None: + raise HTTPException( + status_code=500, + detail="Notion client not configured – set NOTION_TOKEN", + ) + return notion app = FastAPI(title="Notion API for ChatGPT Actions", description="A lightweight wrapper around Notion API for ChatGPT Actions and OpenWebUI tools", @@ -42,8 +54,11 @@ def health(): @app.get("/v1/databases") async def list_databases(): try: - resp = notion.search(filter={"property": "object", "value": "database"}) + client = _get_client() + resp = client.search(filter={"property": "object", "value": "database"}) return resp + except HTTPException: + raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -51,40 +66,50 @@ async def list_databases(): async def list_database_rows(database_id: str = Path(..., description="Notion Database ID"), page_size: int = 50): try: - resp = notion.databases.query(database_id=database_id, page_size=page_size) + client = _get_client() + resp = client.databases.query(database_id=database_id, page_size=page_size) return resp + except HTTPException: + raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/v1/pages/{page_id}") async def get_page(page_id: str = Path(..., description="Notion Page ID")): try: - page = notion.pages.retrieve(page_id=page_id) - blocks = notion.blocks.children.list(block_id=page_id) + client = _get_client() + page = client.pages.retrieve(page_id=page_id) + blocks = client.blocks.children.list(block_id=page_id) return {"page": page, "blocks": blocks} + except HTTPException: + raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.post("/v1/pages") async def create_page(req: CreatePageRequest): try: + client = _get_client() payload = { "parent": {"database_id": req.parent_database_id} if req.parent_database_id else {"type": "page_id"}, "properties": req.properties } if req.children: payload["children"] = req.children - resp = notion.pages.create(**payload) + resp = client.pages.create(**payload) return resp + except HTTPException: + raise except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.patch("/v1/pages/{page_id}") async def update_page(page_id: str, req: UpdatePageRequest): try: + client = _get_client() if not req.properties: raise HTTPException(status_code=400, detail="No properties provided") - resp = notion.pages.update(page_id=page_id, properties=req.properties) + resp = client.pages.update(page_id=page_id, properties=req.properties) return resp except HTTPException: raise diff --git a/requirements.txt b/requirements.txt index cfcc9e3..53ea189 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ fastapi==0.95.2 uvicorn[standard]==0.21.1 python-dotenv==1.0.0 notion-client==0.6.0 -pydantic==1.10.10 +pydantic==1.10.22 httpx==0.24.1 pytest==7.4.0 pytest-asyncio==0.21.0 From 0412e380b979f418e40f4e96108e2f78b7de3777 Mon Sep 17 00:00:00 2001 From: Winz79 Date: Sat, 9 Aug 2025 18:39:52 +0200 Subject: [PATCH 2/3] Lazily create Notion client on demand --- app/main.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/main.py b/app/main.py index b58cdd2..59f9374 100644 --- a/app/main.py +++ b/app/main.py @@ -9,22 +9,23 @@ from notion_client import Client -NOTION_TOKEN = os.getenv("NOTION_TOKEN") -# Only initialise the Notion client when a token is available. This allows -# the application (and its tests) to start without a configured token. The -# individual request handlers will raise an HTTP error if the client is not -# configured, which keeps the module import side-effect free and makes the -# module easier to test. -notion = Client(auth=NOTION_TOKEN) if NOTION_TOKEN else None - +# Defer client creation until it's actually needed. Importing the module +# doesn't require configured credentials which keeps startup side-effect +# free and makes testing easier. +notion: Optional[Client] = None def _get_client() -> Client: """Return the configured Notion client or raise an HTTP error.""" - if notion is None: + global notion + if notion is not None: + return notion + token = os.getenv("NOTION_TOKEN") + if token is None: raise HTTPException( status_code=500, detail="Notion client not configured – set NOTION_TOKEN", ) + notion = Client(auth=token) return notion app = FastAPI(title="Notion API for ChatGPT Actions", From 11d6f5734f689ee28f34031bfd91b4bab838d2aa Mon Sep 17 00:00:00 2001 From: Winz79 Date: Sat, 9 Aug 2025 18:58:19 +0200 Subject: [PATCH 3/3] Run pytest via python -m pytest; install full requirements in CI; fix image tag templating --- .github/workflows/ci.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6809c4d..a189c39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,11 +28,11 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - name: Install flake8 run: | python -m pip install --upgrade pip - pip install flake8 + python -m pip install flake8 - name: Run flake8 run: | flake8 || true @@ -53,14 +53,14 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install -r requirements.txt + python -m pip install --upgrade pip setuptools wheel + python -m pip install -r requirements.txt - name: Run unit tests with coverage run: | - pytest --cov=app --cov-report=xml:coverage.xml --cov-report=term-missing tests/unit -q + python -m pytest --cov=app --cov-report=xml:coverage.xml --cov-report=term-missing tests/unit -q - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: @@ -99,8 +99,8 @@ jobs: file: Dockerfile.prod push: true tags: | - ghcr.io/${ github.repository_owner }}/${ github.repository }}:latest - ghcr.io/${ github.repository_owner }}/${ github.repository }}:${ github.sha }} + ghcr.io/${{ github.repository }}:latest + ghcr.io/${{ github.repository }}:${{ github.sha }} - name: Optionally push to Docker Hub if: ${{ env.DOCKERHUB_USERNAME && env.DOCKERHUB_TOKEN }} uses: docker/build-push-action@v4 @@ -109,13 +109,13 @@ jobs: file: Dockerfile.prod push: true tags: | - ${{ env.DOCKERHUB_USERNAME }}/${ github.repository }}:latest - ${{ env.DOCKERHUB_USERNAME }}/${ github.repository }}:${ github.sha }} + ${{ env.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:latest + ${{ env.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ github.sha }} - name: Create release (optional) if: github.event_name == 'workflow_dispatch' uses: ncipollo/release-action@v1 with: - tag: v${ github.run_number }} + tag: v${{ github.run_number }} name: "Automated release ${{ github.run_number }}" body: | Automated release from CI build ${{ github.run_id }}