
NyanQL プロジェクトで遊ぶ
NyanQL(にゃんくる)プロジェクトの最初のプロダクトが公開されましたので、少し遊んでみました。なおこの Note では NyanQL とはなんぞやという詳しい説明はしません。
https://github.com/NyanQL/NyanQL
NyanQL はツールチェーン間で受け渡されるデータフォーマットを JSON に限定し、永続データモデルを SQL で操作する、シンプルなビルディングブロックを提供することを目指すプロジェクトです。
気軽にウェブアプリを構成できるようにするのが目的の一つです。
現在リリースされている NyanQLサーバーはデータの管理を担当します。
他にも UI や 業務ロジックを提供するプロダクトのリリースが予定されていますが、現在(2024/07/31)の時点ではデータ管理の部分を引き受ける基本的な NyanQL サーバーが公開されています。
上記 GitHub を clone すると、ソースコードの他に既にビルド済みのバイナリ(NyanQL_*)や、サンプルDB、サンプル設定ファイル(api.json, config.json)などがダウンロードされます。
とりあえず、何も考えずに実行しているプラットフォームに合ったバイナリをダブルクリックします。
NyanQL_Linux_amd64 (x86 Linux用)
NyanQL_Mac (Apple Sillicon Mac用)
NyanQL_Windows.exe (x86 Windows 用)
NyanQL サーバーが立ち上がったら
https://localhost:8443/?api=list
にアクセスしてみてください。
初回は Basic 認証で ID と パスワードを聞かれますので
neko, nyan
を入力します(この辺の情報は README に書いてあります)
するとブラウザの画面内に以下のような JSON が表示されます。
[{"date":"2024-07-01","day_of_week":2,"has_registered":0},{"date":"2024-07-02","day_of_week":3,"has_registered":0},{"date":"2024-07-03","day_of_week":4,"has_registered":0},{"date":"2024-07-04","day_of_week":5,"has_registered":0},{"date":"2024-07-05","day_of_week":6,"has_registered":0},{"date":"2024-07-06","day_of_week":7,"has_registered":0},{"date":"2024-07-07","day_of_week":1,"has_registered":0},{"date":"2024-07-08","day_of_week":2,"has_registered":0},{"date":"2024-07-09","day_of_week":3,"has_registered":0},{"date":"2024-07-10","day_of_week":4,"has_registered":0},{"date":"2024-07-11","day_of_week":5,"has_registered":0},{"date":"2024-07-12","day_of_week":6,"has_registered":0},{"date":"2024-07-13","day_of_week":7,"has_registered":0},{"date":"2024-07-14","day_of_week":1,"has_registered":0},{"date":"2024-07-15","day_of_week":2,"has_registered":0},{"date":"2024-07-16","day_of_week":3,"has_registered":0},{"date":"2024-07-17","day_of_week":4,"has_registered":0},{"date":"2024-07-18","day_of_week":5,"has_registered":0},{"date":"2024-07-19","day_of_week":6,"has_registered":0},{"date":"2024-07-20","day_of_week":7,"has_registered":0},{"date":"2024-07-21","day_of_week":1,"has_registered":0},{"date":"2024-07-22","day_of_week":2,"has_registered":0},{"date":"2024-07-23","day_of_week":3,"has_registered":0},{"date":"2024-07-24","day_of_week":4,"has_registered":0},{"date":"2024-07-25","day_of_week":5,"has_registered":0},{"date":"2024-07-26","day_of_week":6,"has_registered":0},{"date":"2024-07-27","day_of_week":7,"has_registered":0},{"date":"2024-07-28","day_of_week":1,"has_registered":0},{"date":"2024-07-29","day_of_week":2,"has_registered":0},{"date":"2024-07-30","day_of_week":3,"has_registered":0},{"date":"2024-07-31","day_of_week":4,"has_registered":1}]
これは正しい動作です。NyanQLは単なるビルデイングブロックですので、これに適当なUIなどを用意してあげるのは、エンジニアの仕事です。NyanQL はこうしたビルディングブロック間の受け渡しを JSON に限定することで、単純さを実現しようとしています。
とはいえ、このままではちょっと寂しいので簡単な UI を用意してあげることにしましょう。生成AIの Claude 3.5 Sonnet を使うことにします。
まず最初に非常に雑ですが、以下のようなプロンプトを投げてみました。
なお streamlit というのは python のフレームワークです。
アクセスすると JSON 形式のレスポンスを返す API にアクセスする streamlit アプリを作りたい。
API は
https://localhost:8443/?api=target_month_list&year=2024&month=7
にアクセスすると(basic認証, neko, nyan)を行い
以下のような JSON が返される
** ここに実際に返された JSON のコードをそのまま貼り付ける **
生成されたコードを app.py というファイルに保存して
streamlit run app.py
と実行します。こうするとこの streamlit サーバーにアクセスしたページが表示されます。streamlit サーバー側で API とのやりとりが行われますので、利用者は単に GUI を見るだけです。
この段階の各コンポーネントの関係を簡単な図で示しましょう。

少し複雑に見えますが、実際に行ったことは claude 2.5 sonnet に JSON の実際のサンプルを入力して app.py を生成して貰った位です。
さて、これを実行して生成されたコードは、Basic 認証を使った API のアクセスには成功しましたが、出力形式が望み通りのものではありませんでした。まあ何も指定していないので当然なのですが … 以下のような画面になりました。

一応データは取られていますが、もっと見やすくするためにはカレンダー形式にしたいところです。そこで次に以下のようなプロンプトを指定しました。
返される JSON は指定した月の1ヶ月分のカレンダーです。 返されたデータをカレンダー形式で表示して下さい。
タイトルは「スタンプ記録」としてください。
カレンダー表示の部分、曜日は一番上だけに表示するようにしてください。

ここで、今日(2024/07/31)のスタンプを押してみましょう。README によれば、以下のアクセスで「本日分のスタンプ」が押される筈です。
https://localhost:8443/?api=stamp
上のアクセスをしたのち、再び streamlit の画面に戻ると以下のようになっていることが確認できます。

確かに 31 日のところにチェックマークが入っていますね。
もちろん https://localhost:8443/?api=stamp の API を呼び出すボタンを上の streamlit アプリに追加することも難しくないでしょう。
以下のプロンプトをさらに投入しました。
本日分のスタンプを押すためのボタン「スタンプ!」を追加してください。 ボタンを押すと https://localhost:8443/?api=stamp が呼び出されて、本日分のスタンプが登録されます。
この結果画面は以下のようなものになりました。

一見、良さそうですが、ここで「スタンプ!」を押すとデータが重複してしまうので「スタンプの登録に失敗しました」というエラーが表示されてしまいます。まあそれでも結果的に役目は果たしているのですが、気持ちはよくありません。ということで事前に本日分が登録済かどうかをチェックして、まだ登録されていない場合は実際に登録することにします。
新しく以下のプロンプトを投入します。
現在の stamp_today で単純に登録しようとすると、既に登録されている場合にエラーになってしまうことがわかりました。そこでまず
https://localhost:8443/?api=check
で今日のスタンプが登録済かどうかを確認します。 上記の API を呼ぶと今日の日付で登録されたスタンプの数が返されますので 返ってきた JSONが
[{"today_count":0}]
のときだけ、実際にスタンプの登録を行うようにしてください。
これで、上手く動作するようになりました。
ここで登録した記録は、NyanQL_* のバイナリを立ち上げた場所にある stamps.db の中に既に永続化されています。すなわち、とても簡単な Web アプリ(スタンプ登録、スタンプ履歴参照)がこれで完成したことになります。
NyanQL プロジェクトをさわっていると、WebAPI 時代のシェルプログラミングをしているような気がしてきます。この先 UI や ビジネスロジック(ビジネスロジックは Javascript で記述)を定義できるビルディングブロックも登場する予定のようですので、楽しみにしています。
最後に生成した streamlit のソースコードを載せておきます。
import streamlit as st
import requests
import pandas as pd
from datetime import datetime, timedelta
from requests.auth import HTTPBasicAuth
import urllib3
# SSL証明書の検証を無効にする(開発環境用、本番環境では注意が必要)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def fetch_data(year, month):
url = f"https://localhost:8443/?api=target_month_list&year={year}&month={month}"
try:
response = requests.get(url, auth=HTTPBasicAuth('neko', 'nyan'), verify=False)
response.raise_for_status()
data = response.json()
return data
except requests.RequestException as e:
st.error(f"APIリクエストエラー: {e}")
return None
def create_calendar(df, year, month):
# 日本語の曜日
weekdays = ["月", "火", "水", "木", "金", "土", "日"]
# カレンダーのヘッダー
st.write(f"### {year}年{month}月")
# 曜日の行を表示
cols = st.columns(7)
for i, day in enumerate(weekdays):
cols[i].write(f"**{day}**")
# 月の最初の日を取得
first_day = datetime(year, month, 1)
# 月の最初の日の曜日(0:月曜日, 6:日曜日)
first_weekday = first_day.weekday()
# カレンダーの各週を生成
current_day = first_day - timedelta(days=first_weekday)
while current_day.month <= month:
cols = st.columns(7)
for i in range(7):
with cols[i]:
if current_day.month == month:
date_str = current_day.strftime("%Y-%m-%d")
day_data = df[df['date'] == date_str]
if not day_data.empty:
registered = "✅" if day_data['has_registered'].values[0] == 1 else "❌"
st.markdown(f"**{current_day.day}** \n{registered}", unsafe_allow_html=True)
else:
st.markdown(f"**{current_day.day}**", unsafe_allow_html=True)
else:
st.empty()
current_day += timedelta(days=1)
if current_day.month > month:
break
def check_today_stamp():
url = "https://localhost:8443/?api=check"
try:
response = requests.get(url, auth=HTTPBasicAuth('neko', 'nyan'), verify=False)
response.raise_for_status()
data = response.json()
return data[0]['today_count'] == 0
except requests.RequestException as e:
st.error(f"スタンプ確認エラー: {e}")
return False
def stamp_today():
if check_today_stamp():
url = "https://localhost:8443/?api=stamp"
try:
response = requests.get(url, auth=HTTPBasicAuth('neko', 'nyan'), verify=False)
response.raise_for_status()
st.success("本日のスタンプを押しました!")
except requests.RequestException as e:
st.error(f"スタンプの登録に失敗しました: {e}")
else:
st.warning("本日のスタンプは既に登録されています。")
def main():
st.title("スタンプ記録")
# スタンプボタンを追加
if st.button("スタンプ!"):
stamp_today()
# 年と月の入力
col1, col2 = st.columns(2)
with col1:
year = st.number_input("年を入力してください", min_value=2000, max_value=2100, value=datetime.now().year)
with col2:
month = st.number_input("月を入力してください", min_value=1, max_value=12, value=datetime.now().month)
if st.button("カレンダーを表示"):
data = fetch_data(year, month)
if data:
# データをDataFrameに変換
df = pd.DataFrame(data)
df['date'] = pd.to_datetime(df['date'])
# カレンダー形式で表示
create_calendar(df, year, month)
# 登録状況の集計
registered_count = df['has_registered'].sum()
total_days = len(df)
st.write(f"### スタンプ記録サマリー")
st.write(f"スタンプ済み日数: {registered_count}/{total_days}")
st.progress(registered_count / total_days)
if __name__ == "__main__":
main()