Project IDXでFastAPIベースのPanelアプリを作りたかったんだけど結局よく分からなかった

Code

はじまり

リサちゃん
リサちゃん

またかぁ? このIDEはぁ?

135ml
135ml

使えるかもしれん。

※当ページはアフィリエイト広告を利用しています。

Project IDXとかいうクラウドIDEでWebアプリを作りたい

以前に存在を知った「Project IDX」という、Googleが開発中の、ブラウザ上もといクラウド上でUIテストとかも出来てしまうIDEを調べました。そして今回、それを実際に使ってみようというわけです。

「Project IDX」が気になった方は、公式のサイトはこちらです。2024-12-07時点ではWaitlist等は無く、すぐに開発を始めることが出来ます。

Project IDX  |  Google for Developers
クラウドでのフルスタックのマルチプラットフォーム アプリ開発のための AI 支援型ワークスペース。

今回作りたいWebアプリ

今回は、PythonのWeb APIフレームワークであるFastAPIと、インタラクティブなダッシュボードをWeb上に作成できるライブラリであるPanelを使ってWebアプリを作っていきます。

FastAPI
FastAPI framework, high performance, easy to learn, fast to code, ready for production
Overview — Panel v1.5.4

GitHubにあるPanelのリポジトリからその様子を見てみると、Webページ上でなんかドラッグしたりすることが出来たりしそうなので、これをProject IDX上のプレビュー画面で実際にやってみたら便利だろうなあっと思って、まずはProject IDXでその両者のパッケージを動かせる状態にするところから始めていきたいと思います。

そして、Panel公式のページの中に、FastAPIを使ってPanelのダッシュボードを作るチュートリアル的なものがあるので、そのコードを試してみます。

Running Panel apps in FastAPI — Panel v1.5.4

結論:環境の作り方がよく分からなかった。

あーーー、くそう・・・。結局はベータ版ってことかぁ・・・??

Project IDXでは現在(2024-12-07時点)では、公式から提供されているPythonのフレームワークのテンプレートは、「Python Flask」と「Python Django」の2種類しか置いてありません。

なので、FastAPI + Panelのテンプレートは自分で用意する必要がありますが・・・、これがなんともまあ訳が分からなかった。

結局、どこをどう設定すればPanelアプリをプレビューすることが出来るのかが分からなかったんですよね。

ですがしかし、その奮闘する過程で奇跡的に1つのワークスペースだけ、FastAPIベースのPanelアプリを開発することに成功した環境がありました。実際に開発して画面の中のUIを動かすことも出来たので、この謎が極まる複雑怪奇な環境構築さえ出来てしまえば、快適なWebアプリ開発を恍惚として堪能できた筈なんですよね。

これが実際にProject IDX上で、FastAPIで提供したAPIパスから色々なダッシュボードを操作している映像です。

エディタ上でレンダリングされたWebページの反応速度は悪くありません。インタラクティブにウィジェットを操作することが出来ています。

しかし結局、このプレビューを再現することは叶いませんでした・・・。

このクラウドIDEはベータ版・・・。そうは問屋が卸してくれませんでした・・・。

一応、今回動かしたPythonのソースを貼っておきます。

app.py

from src import panel

from fastapi import FastAPI
from panel.io.fastapi import add_applications
from fastapi.middleware.cors import CORSMiddleware

import uvicorn
import os

app = FastAPI()

origins = ["*"]
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,  # List of allowed origins
    allow_credentials=True,
    allow_methods=["*"],  # Allow all methods
    allow_headers=["*"],  # Allow all headers
)

@app.get("/")
async def read_root():
    return {"Hello": "World"}

def create_panel_app():
    return panel.create_slider(10, 3, "⭐")

add_applications({
    "/panel_app1": create_panel_app
    , "/panel_app2": panel.create_column("I am a Panel object!")
    , "/panel_app3": "my_panel_app.py"
    , "/panel_app4": panel.create_figure_linking_slider()
    , "/panel_app5": panel.create_tabs()
    , "/panel_app6": panel.create_data_frame(400)
    , "/panel_app7": panel.create_text_input("Input file path.")
    , "/panel_app8": panel.create_button("Read the specified file")
    , "/panel_app9": panel.create_other_widgets()
    , "/panel_app10": panel.create_big_tabs()
    , "/panel_app11": panel.create_sine_wave()
    , "/panel_app12": panel.create_figure_linking_slider()
}, app=app)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))

src/panel.py

import panel as pn
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.figure
import datetime as dt
from bokeh.models import CustomJS, Slider

pn.extension(comms="vscode")  # to use on VSCode
# pn.extension(comms="bokeh")  # to use on Bokeh

# slider = pn.widgets.FloatSlider(name="Slider", start=0, end=10, value=5)

def create_slider(end: int, default_value: int, gauge_icon: str = "⭐") -> pn.widgets.IntSlider:
    slider = pn.widgets.IntSlider(name='Slider', start=0, end=end, value=default_value)
    return slider.rx() * gauge_icon

slider = pn.widgets.FloatSlider(name="Slider", start=0, end=10, value=5)
@pn.depends(slider.param.value)
def plot_sine_curve(value) -> matplotlib.figure:
    fig, ax = plt.subplots()
    x = np.linspace(0, 10, 100)
    y = np.sin(x + value)
    ax.plot(x, y)
    plt.close(fig) # Prevent from memory leak.
    return fig

def create_figure_linking_slider() -> pn.Column:
    return pn.Column(slider, plot_sine_curve).servable()

def create_column(title: str) -> pn.Column:
    return pn.Column(title)

def create_tabs() -> pn.Column:
    tab1 = pn.Column("This is tab 1")
    tab2 = pn.Column("This is tab 2")
    tabs = pn.Tabs(("Tab 1", tab1), ("Tab 2", tab2))
    return tabs.servable()

def create_big_tabs() -> pn.Column:
    tab1 = create_other_widgets()
    tab2 = create_other_widgets()
    tabs = pn.Tabs(("Tab 11", tab1), ("Tab 12", tab2))
    return tabs.servable()

def create_data_frame(width: int) -> pn.widgets.DataFrame:
    data = {"col1": [1, 2, 3], "col2": [4, 5, 6]}
    df = pd.DataFrame(data)
    table = pn.widgets.DataFrame(df, width=width)
    return table.servable()

def create_text_input(title: str, placeholder: str = "Input me."):
    return pn.widgets.TextInput(name=title, placeholder=placeholder)

def create_button(title: str, button_type: str = "primary"):
    return pn.widgets.Button(name=title, button_type=button_type)

def create_other_widgets():
    # 2. チェックボックスタグルグループ
    checkbutton_group = pn.widgets.CheckButtonGroup(
        name="Checkbox Group"
        , value=["Apple", "Pear"]
        , options=["Apple", "Banana", "Pear", "Strawberry"]
        , orientation="horizontal"
    )
    # 2. チェックボックスタグルグループ
    checkbox_group = pn.widgets.CheckBoxGroup(
        name="Checkbox Group"
        , value=["Apple", "Pear"]
        , options=["Apple", "Banana", "Pear", "Strawberry"]
        , inline=True
    )
    # 3. ラジオボタングループ
    radio_group = pn.widgets.RadioButtonGroup(
        name="Radio Button Group"
        , options=["Biology", "Chemistry", "Physics"]
        , button_type="success"
    )
    multi_choice = pn.widgets.MultiChoice(
        name="MultiChoice"
        , value=["Apple", "Pear"]
        , options=["Apple", "Banana", "Pear", "Strawberry", "melon", "dragon fruit", "orange", "grape", "muscat", "avocado", "peach", "tomato"]
    )
    # 5. マルチセレクト
    multi_select = pn.widgets.MultiSelect(
        name="MultiSelect"
        , value=["Apple", "Pear"]
        , options=["Apple", "Banana", "Pear", "Strawberry"]
        , size=8
    )
    # 6. トグルボタン
    toggle_group = pn.widgets.ToggleGroup(
        name="ToggleGroup"
        , options=["Biology", "Chemistry", "Physics"]
    )
    # 7. セレクター (様々なウィジェットのラッパー)
    selector = pn.widgets.Select(
        name="Select"
        , options=["Biology", "Chemistry", "Physics"]
    )
    # 8. カラーピッカー
    color_picker = pn.widgets.ColorPicker(
        name="Color Picker"
        , value="#99ef78"
    )
    # 9. 日付ピッカー
    date_picker = pn.widgets.DatePicker(
        name="Date Picker"
        , value=dt.datetime(2024, 4, 1, 11, 37)
    )
    # 10. 静的なテキスト (既に使用済み)
    data_slider = pn.widgets.DateSlider(
        name="Date Slider"
        , start=dt.datetime(2019, 1, 1)
        , end=dt.datetime(2019, 6, 1)
        , value=dt.datetime(2019, 2, 8)
    )
    # 11. text editor
    text_editor = pn.widgets.TextEditor(placeholder="Enter some text")
    # 12. json editor
    spinner = pn.widgets.JSONEditor(value={
        "dict"  : {"key": "value"},
        "float" : 3.14,
        "int"   : 1,
        "list"  : [1, 2, 3],
        "string": "A string",
    }, width=400)
    # 13. ファイル入力
    file_input = pn.widgets.FileInput(name="File Input")
    # 14. リテラル入力 (コード入力)
    literal_input = pn.widgets.LiteralInput(
        name="Literal Input (dict)"
        , value={"key": [1, 2, 3]}
        , type=dict
    )
    # 15. パスワード入力
    password_input = pn.widgets.PasswordInput(
        name="Password"
        , placeholder="Enter your password here..."
    )
    # 16. テキストエリア入力
    textarea_input = pn.widgets.TextAreaInput(
        name="Text Area Input"
        , placeholder="Enter a string here..."
    )
    # 17. Switch
    switch = pn.widgets.Switch(name="Switch")
    # 18. file dropper
    file_dropper = pn.widgets.FileDropper()

    player = pn.widgets.Player(
        name="Player"
        , start=0
        , end=100
        , value=32
        , loop_policy="loop"
        , show_value=True
        , value_align="start"
    )

    autocomplete = pn.widgets.AutocompleteInput(
        name="Autocomplete Input"
        , options=["Biology", "Chemistry", "Physics"]
        , case_sensitive=False
        , search_strategy="includes"
        , placeholder="Write something here"
    )
    # 全てのウィジェットをカラムに配置
    return pn.Column(
        checkbutton_group
        , checkbox_group
        , radio_group
        , multi_choice
        , multi_select
        , toggle_group
        , selector
        , color_picker
        , date_picker
        , data_slider
        , text_editor
        , spinner
        , file_input
        , literal_input
        , password_input
        , textarea_input
        , switch
        , file_dropper
        , player
        , autocomplete
        , create_slider(10, 5)
        , create_figure_linking_slider()
        , create_column("Column Title")
        , create_tabs()
        , create_data_frame(400)
        , create_text_input("Text Input")
        , create_button("Button")
    ).servable()

def create_sine_wave():
    # Create the initial plot
    x = np.linspace(0, 10, 500)
    y = np.sin(2 * np.pi * x)

    fig, ax = plt.subplots()
    line, = ax.plot(x, y)
    ax.set_xlabel("x")
    ax.set_ylabel("sin(2πx)")
    ax.set_title("Sine Wave")

    # Create sliders for frequency and amplitude
    frequency_slider = Slider(start=0.1, end=5, value=1, step=.1, title="Frequency")
    amplitude_slider = Slider(start=0.1, end=5, value=1, step=.1, title="Amplitude")


    # Define the update function (using CustomJS for interactivity,  better for larger datasets than pn.bind)
    code = """
        const data = source.data;
        const f = cb_obj.value;
        const a = amplitude_slider.value;
        const x = data['x'];
        const y = data['y'];
        for (var i = 0; i < x.length; i++) {
            y[i] = a * Math.sin(2 * Math.PI * f * x[i]);
        }
        source.change.emit();
    """

    # Convert the matplotlib figure to a bokeh figure (required for interaction within panel)
    from bokeh.io import show
    from bokeh.plotting import figure
    from bokeh.models import ColumnDataSource

    source = ColumnDataSource(data=dict(x=x, y=y))

    p = figure(width=400, height=400, tools="crosshair,pan,wheel_zoom,box_zoom,reset")
    p.line('x', 'y', source=source, line_width=3, line_alpha=0.6)


    # Attach the callback to the sliders
    callback = CustomJS(args={'source': source, 'frequency_slider': frequency_slider, 'amplitude_slider': amplitude_slider}, code=code)
    frequency_slider.js_on_change('value', callback)
    amplitude_slider.js_on_change('value', callback)

    # Arrange the elements in a Panel layout
    dashboard = pn.Column(
        pn.Row(frequency_slider, amplitude_slider),
        p  # Use the Bokeh figure directly
    )

    return dashboard.servable()

苦戦したついでにその過程を書き連ねます

同じような野望を持って、Panelアプリを再現しようと思った後世の方々に、環境を構成するために試してみた事柄を書き連ねていきたいと思います。

dev.nixの構成

.idx/dev.nixというファイルで構成する。

まず、Project IDXでワークスペースを作成して、そのためのVMが起動すると、とある構成ファイルを元に環境が構築されていきます。それが「.idx/dev.nix」ファイルです。「NIX」という記法で書いていきます。yamlとかjsonみたいな感じの拡張子です。

その構成ファイルの作り方のリファレンスは、公式のものが載っています。

dev.nix リファレンス  |  Project IDX  |  Google for Developers

そして、その構成ファイルは主に3つのフィールドに分かれています。

  • channel・・・ワークスペースのバージョン的なもの。2024-12-04時点では、「stable-23.05」、「stable-23.11」、「stable-24.05」、「unstable」のいずれかです。(「stable-24.11」を出して欲しい・・・。)
  • packages・・・ワークスペースにインストールしたいパッケージ情報。リスト形式。
  • env・・・設定したい環境変数をkey-valueライクにリスト形式で書きます。
  • idx・・・ワークスペースを作った後にどんな処理をさせるかを書きます。

package

packageフィールドには、Pythonを入れたりpipを入れたりしていきます。はたまた、FastAPIを入れたりPanelを入れたりすることも出来るわけですが、ここのフィールドでそのライブラリとかまで指定する必要があるのかどうかは現在自分は解明できていません。

Pakcageの識別子は、以下のNixOSのパッケージリストの一覧から調べて書いていきます。「python311Packages.pip」やら「python311Packages.uvicorn」やらが識別として存在します。

NixOS Search

ちなみに、「NixOS」という文字が見えたので、そーいうOS上で動いているのかと思ったのですが、それは違うらしいです。

このProject IDXというIDEは、「NixOS」というシステムを組み込んでいるわけではなく、①広く利用されていて、②キャッシュが効いて速い、「Nixパッケージ」のシステムを取り入れているということらしいです。つまり、みんなが使いやすくてスピーディに開発できるように「Nix」を採用したっぽいですね。(そうか、NIXはNginx用のファイルを作る時にも使うのか。)

How we use Nix on Project IDX
Over here on Project IDX, we like to work fast. A big part of that means making sure we’re building the tools we need to...

idx

envは飛ばしておいて、次に鬼門となるidxフィールドです。

このidxフィールドが最も設定する部分が広いです。開発していくWebアプリをプレビューするのかどうかもここに書いていきます。

idxフィールドでは主に4種類にフィールドが分かれています。

  • extensions・・・IDEで予めインストールしたい拡張機能をリスト形式で書きます。
  • workspace・・・ワークスペースを作ったり開いた時に実行したいことを書きます。
  • previews・・・・プレビューを表示するための事柄を書きます。

extensionsフィールドでは、拡張機能のパッケージの識別子を完全修飾された拡張機能 ID のリストで書いていきます。${publisherId}.${extensionId}みたいな感じです。

エディタの拡張機能の部分で探したり、「Open VSX Registry」から探して記載しています。

Open VSX Registry

workspaceフィールドでは、「onCreate」と「onStart」の2種類のトリガーで行う処理(Bashコマンド)を記述していきます。

「onCreate」はワークスペースが作られる時に参照されるフィールドです。なので、ワークスペースをリビルドする時にはこのフィールドで宣言されている「install」の内容は実行されません。なので、ビルドの仕様がまだ決まっていない時は、「onStart」のフィールドの方の「install」に実行内容を書く方が良さそうです。(ビルドする内容が固まったら、「onCreate」だけに記載して、ワークスペースを再開する時にいちいちコマンドが実行されないようにしても良さそう。)

そして、previewフィールドで、アプリのプレビューをしていくかどうかを決めることが出来ます。「enable = true;」と書けばプレビューできるようになります。しかし、そのenableフィールドの隣にさらにpreviewsフィールドがあって、(idx.preview.previewということです。)その中でプレビューするためのコマンドと、ポート番号などを書いていきます。

Python Flaskアプリを参考に設定していく。

それでは、公式テンプレートのPython Flaskアプリから、構成ファイルをまねていきたいと思います。

Python Flaskテンプレートでは、このような構成になっています。

# To learn more about how to use Nix to configure your environment
# see: https://developers.google.com/idx/guides/customize-idx-env
{ pkgs, ... }: {
  # Which nixpkgs channel to use.
  channel = "stable-24.05"; # or "unstable"
  # Use https://search.nixos.org/packages to find packages
  packages = [ pkgs.python3 ];
  idx = {
    # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
    extensions = [ "ms-python.python" ];
    workspace = {
      # Runs when a workspace is first created with this `dev.nix` file
      onCreate = {
        install =
          "python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt";
        # Open editors for the following files by default, if they exist:
        default.openFiles = [ "README.md" "src/index.html" "main.py" ];
      }; # To run something each time the workspace is (re)started, use the `onStart` hook
    };
    # Enable previews and customize configuration
    previews = {
      enable = true;
      previews = {
        web = {
          command = [ "./devserver.sh" ];
          env = { PORT = "$PORT"; };
          manager = "web";
        };
      };
    };
  };
}

channelは最新版の「stable-24.05」ですね。そして、idxフィールドでは、workspace.onCreatepreviewsが目立ちます。onCreate.installで、Pythonの仮想環境を作ってその中にpip installしていますね。そして、default.openFilesではワークスペースを作成した時に予め最初に開いておくファイルが書いてあります。onCreate.installの部分は拝借しておきましょう。

そして、previews.previewsでは、「./devserver.sh」というものを使ってプレビューを再現しているようです。その中身を見てみましょう。

#!/bin/sh
source .venv/bin/activate
python -m flask --app main run -p $PORT --debug

venvの中に入って、Flaskを起動するようになっていますね。この部分をFastAPIを起動するように編集すれば良さそうです。

最終的なFastAPI用のdev.nix

これが最終的な.idx/dev.nixです。少々長いですが、何が必要なのかどうかが分かっていないので冗長に書いています。

# To learn more about how to use Nix to configure your environment
# see: https://developers.google.com/idx/guides/customize-idx-env
{ pkgs, ... }: {
  channel = "stable-24.05"; # "stable-23.05" or "unstable"
  # Use https://search.nixos.org/packages to  find packages
  packages = [
    pkgs.python3
    pkgs.python311Packages.pip
    pkgs.google-cloud-sdk
    pkgs.python311Packages.fastapi
    pkgs.python311Packages.uvicorn
    pkgs.python311Packages.panel
  ];
  # Sets environment variables in the workspace 
  env = { };

  idx = {
    # search for the extension on https://open-vsx.org/ and use "publisher.id"
    extensions = [
      "ms-python.python"
      "rangav.vscode-thunder-client"
    ];
    workspace = {
      # runs when a workspace is first created with this `dev.nix` file
      # to run something each time the environment is rebuilt, use the `onStart` hook
      onCreate = {
        create-venv = ''
          python -m venv .venv
          source .venv/bin/activate
          pip install --no-cache-dir -r requirements/dev.txt
        '';
      };
      onStart = {
        # install = ''
        #   python -m venv .venv
        #   source .venv/bin/activate
        #   pip install --no-cache-dir -r requirements/dev.txt
        # '';
        # Open editors for the following files by default, if they exist:
        default.openFiles = [ "README.md" "app.py"];
        # default.openFiles = [ "README.md" ];
        # run-server = "./devserver.sh";
        # Example: start a background task to watch and re-build backend code
        # watch-backend = "npm run watch-backend";
      };

    };
    # preview configuration, identical to monospace.json
    previews = {
      enable = true;
      previews = [
        {
          command = [
            "./devserver.sh"
          ];
          env = {
            PORT = "$PORT";
          };
          id = "web";
          manager = "web";
        }
      ];
    };
    # previews = {
    #   enable = true;
    #   previews = [
    #     {
    #       command = [
    #         "./devserver.sh"
    #       ];
    #       env = {
    #         PORT = "$PORT";
    #       };
    #       id = "web";
    #       manager = "web";
    #     }
    #   ];
    # }
  };
}

requirements.txtの中身

現時点で最も怪しいと思っている、requirements.txtの中身です。基本的には、後述のFastAPIをProject IDXで構成する同志の方々のものを真似ていますが、これでPanelのプレビュー画面を再現するまでには至っていません・・・。

annotated-types==0.7.0
anyio==4.7.0
autopep8==2.0.4
click==8.1.7
exceptiongroup==1.2.0
fastapi==0.115.6
h11==0.14.0
idna==3.10
pycodestyle==2.11.1
pydantic==2.10.3
pydantic_core==2.27.1
sniffio==1.3.1
starlette==0.41.3
tomli==2.0.1
typing_extensions==4.12.2
uvicorn>=0.32.1
# Panel
panel[fastapi]>=1.5.2
# Google Cloud
functions_framework>=3.5.0
# Cloud SQL
cloud-sql-python-connector[pg8000]
sqlalchemy>=2.0.36

パッケージ達の概要としてはこんな感じです。HTTP関連のヤツらまでは必須だと思います。

コアとなるパッケージ:

  • fastapi==0.105.0: 高速でモダンなWeb APIフレームワーク。Pythonの型ヒントを利用して、APIの定義、検証、ドキュメント生成を効率的に行えます。
  • uvicorn==0.24.0.post1: ASGI(Asynchronous Server Gateway Interface)サーバー。FastAPIアプリケーションを実行するために使用されます。高速で軽量なサーバーとして知られています。
  • starlette==0.27.0: ASGIツールキットおよびフレームワーク。FastAPIはStarletteをベースに構築されています。ルーティング、ミドルウェア、WebSocketなどの機能を提供します。
  • pydantic==2.5.2: データ検証と設定管理ライブラリ。Pythonの型ヒントを使用して、データの型、制約、変換などを定義できます。FastAPIはPydanticを使用してリクエストボディとレスポンスボディを検証します。
  • pydantic_core==2.14.5: Pydantic 2 のコアバリデーションロジックを提供するライブラリ。パフォーマンス向上のため分離されています。

非同期処理関連:

  • anyio==3.7.1: 非同期プログラミングのためのライブラリ。異なる非同期イベントループ実装(asyncio, trio)を抽象化し、移植性の高いコードを書くことができます。
  • sniffio==1.3.0: 実行時にどの非同期ライブラリ(asyncio, trio)が使用されているかを検出するライブラリ。anyioのようなライブラリで使用されます。

HTTP関連:

  • h11==0.14.0: HTTP/1.1プロトコルを実装した低レベルライブラリ。uvicornなどのASGIサーバーで使用されます。
  • idna==3.6: 国際化ドメイン名(IDNA)を処理するためのライブラリ。URLパーサーなどで使用されます。

型ヒント関連:

  • typing_extensions==4.9.0: Pythonの標準ライブラリのtypingモジュールに含まれていない型ヒントを提供するライブラリ。新しいPythonバージョンで導入された型ヒントを古いバージョンで使用する場合などに利用されます。
  • annotated-types==0.6.0: 型ヒントにアノテーションを追加するためのライブラリ。より詳細な型情報を提供できます。

ユーティリティ:

  • click==8.1.7: コマンドラインインターフェース(CLI)を作成するためのライブラリ。FastAPIアプリケーションでCLIコマンドを定義するために使用できます。
  • tomli==2.0.1: TOMLファイルのパーサー。設定ファイルの読み込みなどに使用できます。

コードフォーマット/静的解析:

  • autopep8==2.0.4: PEP 8スタイルガイドに従ってPythonコードを自動的にフォーマットするツール。
  • pycodestyle==2.11.1: PEP 8スタイルガイドに準拠しているかをチェックする静的解析ツール。

構成時の引っかかりポイント

結局引っかかったままでしたが、途中で遭遇した引っかかりポイントをまとめます。

1. VSX(拡張機能)の再インストール

dev.nixで言うところの、idx.extensionsのフィールドに当たります。この拡張機能のインストール処理は、まあよく分かりません。拡張機能がインストールされたら、エディタ上にインストールされた旨が表示されるものですが、不思議なことに表示されません。なので、実際にはインストールされているのにインストールされていないことになっている。(もしくはインストールされていないのにインストールされていることになっている。)

このアベコベ状態を解消するのは、Project IDXの開発陣の方々にお任せして、使う側としてまず覚えておく必要があることは、idx.extensionsで指定した拡張機能は「ワークスペースを作った時にだけ指定すれば良い。」ということです。

まあ、このdev.nixという構成ファイルはワークスペースを作って開いた後も編集して、ワークスペースを「リビルド」することが出来るのですが、その際にビルド時に元々書いてあったidx.extensionsのフィールドにある拡張子はコメントアウトしましょう。そうしないと、dev.nix内の処理がつっかえてしまいます。「installatoin failed. already installed.」みたいなエラーログだったり、こんなエラーログと共に。

Installing extensions… Updating the extension ‘ms-python.python’ to the version 2024.20.0 Installing extension ‘ms-python.python’ v2024.20.0… Error while installing extension ms-python.python: Can’t install ‘ms-python.python’ extension because it is not compatible with the current version of Code OSS for Cloud Workstations (version 1.89.1). Failed Installing Extensions: ms-python.python

特にバージョン指定をしなくても、ただエディタ上でPythonを使いたいだけなので、とりあえず一度Pythonを入れてしまって、リビルドの際にはコメントアウトです。他の拡張機能も同様ですね。

2. どのパッケージを参照しているんだか分からない。

requirements.txtのパッケージを参照してソースが動いているんだか、packagesフィールドで指定したパッケージを参照して動いているんだかよく分からないんですよね。まあでも、Python Flaskテンプレートを見たら、PythonをNixパッケージで入れたら、あとはpip install -r requirements.txtか・・・。しかし、後述のFastAPIをProject IDXで構成する同志の方々は「pkgs.python311Packages.pip」をパッケージの中に入れている・・・。うーむ、解せん。まだよく分かっていません。

3. プレビュー画面が表示されない。(その1:バージョンが古い)

最大の峠がここでした。未だに越えきれていません。

FastAPIをProject IDXで構成する同志の方々のdev.nixを拝見させていただいて、FastAPIをプレビューできる構成ファイルを見つけたのでここに共有しておきます。

idx-templates/fastapi at main · prakhar1989/idx-templates
Contribute to prakhar1989/idx-templates development by creating an account on GitHub.

この構成ファイルは、「channel = "stable-23.05";」で少し古いのですが、これでビルドするとFastAPIのページをプレビューすることが出来るようになります。これにはかなり助かりました。

しかし、このdev.nixのままでは、FastAPIでPanelを表示するページのプレビューは表示できませんでした。なので、その際に色々奮闘して、奇跡的に成功したdev.nixでは、「channel = "stable-24.05";」になっていたので、おそらくそのバージョンでないとPanelの画面はプレビュー出来ないのでは無いのかと思っています。

それでは、ここから長い長いプレビュー画面を探求する旅路の記録へと参ります。

4. プレビュー画面が表示されない。(その2:Try again実行が早い。)

プレビュー表示されなくなるというのは、「Error starting preview」が表示される状態を指します。この状態は、dev.nixの実行中でエラーが発生しています。なので、dev.nixもしくはそのファイルで参照しているファイルを直してから「Try again」をすると再実行されます。修正部分が正しかったら、ちゃんとプレビューが表示されるようになります。

「Error starting preview」が表示される原因として、pipなどによるパッケージがまだインストールが完了していないことが考えられます。その場合は、インストールが完了するまで待ちましょう。と言っても、いつ完了するのかを確認する方法が分からないので、1分ぐらい待てば確実でしょうか。

奇跡的に成功した環境で、「Error starting preview」が表示された際には、ワークスペースが開いてから少し待っていたら開いたというわけです。

5. プレビュー画面が表示されない。(その3:「Try again」を実行したかどうか。)

これは先程とは逆のパターンですね。「Error starting preview」の文字を見た瞬間にすぐさまリビルドしてしまうパターン。

「Try again」を実行すると、「No Module Named」が表示されたりする時もありますが、待機していると無事正常にレンダリングされるようになったりします。「Try again」してから30秒は待ち続けた方が良いと再度記しておきます。

6. プレビュー画面が表示されない。(その4:「__pycache__」や「.venv」があるかどうか。)

Pythonでpipとかでパッケージをインストールするとディレクトリの中に出てきますよね、「__pycache__」や「.venv」。

それらがあると、自分が今設定した内容が果たして本当に思い描いている環境を再現する至っているのか、それが分からなくなります。

なので、リビルドする際には、「__pycache__」や「.venv」は消しておいた方が良い。環境差異が発生しにくくなります。

7. プレビュー画面が表示されない。(その5:「リビルド=ゼロからやり直す」わけではない。)

これは、コンテナをいつも使っていたりすると引っかかりそうですが・・・。(僕が沼った最大の原因はここかもしれません。)

Project IDXのワークスペースをリビルドすることは、環境をゼロから作り直しているわけではなく、加筆しているような感覚です。つまり、リビルドした後に上手く環境を構築できたと思ってその瞬間のファイルをメモしたら、そのリビルドの前にやったファイルの内容もメモっておく必要があったりするわけです。

なので、リビルドする前にrequirements.txtの中身を消したりしていると、その中身がビルドに必要なものだったりする可能性もあるわけです。僕は、この点を理解していなくて、奇跡的に成功したワークスペースで一体どういう構成ファイルを使ってリビルドしてきたかどうかを忘れてしまったのでした。そして未だに再現できていない・・・。

そのため、新しい環境のビルドを試している間は、自分がビルドに使ったパッケージ等はしっかりメモしておくことが必要です。コンテナを読み込み直したり、VMを再起動しているわけではないんですねえ。(もしくは、Project IDXのHome画面に戻って、ワークスペースの横にある横三点リーダーをクリックして「Reset」で再起動することができます。その分だけ時間が掛かるんですけどね・・・。)

8. Panelのページのプレビュー画面に出てくるエラー出力が貧弱すぎる。

FastAPIの表示に上手くいくと、JSONは間違いなくちゃんと返してくれるようになります。

そんな感じでFastAPIの表示には上手くいくものの、Panelの表示が出来ない時、「Internal Server Error」とだけ表示されます。「Internal Server Error」としか表示されません。

ブラウザを使って中身を見れるわけでも無いので、もうお手上げです。奇跡的に成功したワークスペースを参考に色々とパッケージを揃えていくものの、ずーっと、「Internal Server Error」とだけ表示されます。

まとめ

今回は、Project IDXというクラウドIDEでPanelライブラリとFastAPIフレームワークによるWebアプリの開発で挫折した記事でした。

以下、本記事のまとめです。

  • Project IDXのワークスペースは、.idx/dev.nixというファイルで構成していく。
  • idx.workspaceの「onCreate」の時などに、既に拡張機能をインストールしていたら、「onStart」時にはインストールする必要はない。むしろエラーの原因になるので、既に入っている拡張機能はコメントアウトしておく。
  • ワークスペースが開いて、「Error starting preview」が表示されたら、「Try again」をクリックして30秒ぐらい待っておくと、ちゃんとプレビューが開くことがある。
  • 「__pycache__」や「.venv」によって、プレビューが開くかどうか左右されたりもする。
  • 「リビルド=ゼロからやり直す」わけではない。

Project IDXに関する情報は、以下の公式ページでも確認することが出来ます。

Welcome to Project IDX  |  Google for Developers

なんか公式のトラブルシューティング関連のページもありました。

プロジェクト IDX のトラブルシューティングとよくある質問  |  Project IDX  |  Google for Developers

Project IDXのコミュニティはここにあります。ここでもIDXを使っている開発者達が互いにトラブルシューティングし合っています。

IDX Community
The IDX Community is the place to get your questions answered, hear directly from the team, and share your knowledge.

今回のProject IDXの構成の話って、もしかしてNginxを分かっていないとムズカシイ・・・? くそう、やったことないぞ・・・。(今度NASを組むから齧るかもしれないが。)

クラウドIDE関連記事

その他のクラウドIDE関連の記事を貼っておきます。やっぱり当分はコイツで開発することにします。

Nginx関連の書籍

nginx実践ガイド (impress top gear)

Amazon.co.jp

nginx実践入門 (WEB+DB PRESS plus)

nginx実践入門 (WEB+DB PRESS plus)
nginxを現場で活用するための知識を、実践的なノウハウを交えて解説した書籍です。nginxのインストール方法や基本的な設定方法からはじめ、nginxを利用した「静的コンテンツ配信サーバ」「HTTPSサーバ」「Webアプリケーションサーバ」...

Nginx ポケットリファレンス

Amazon.co.jp

NGINX Unit Cookbook: Recipes for Using a Versatile Open Source Server

Amazon.co.jp

おしまい

リサちゃん
リサちゃん

これからどう化けるかか。

135ml
135ml

とりあえず今回は疲れた・・・

以上になります!

コメント

タイトルとURLをコピーしました