ちゃなべの備忘録

ほぼ備忘録です。

TypeChallengeをやった_Easy編【備忘録】

はじめに

型パズルをやった備忘録を残しまする

環境整え

これ入れた。

marketplace.visualstudio.com

あとはフォルダ指定して、やるだけ。

解いた

4-Pick (Easy)

問題

組み込みの型ユーティリティPick<T, K>を使用せず、TからKのプロパティを抽出する型を実装します。

例えば:

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}
思考

ふむふむ。どうやるんやろ。

なんか、& 使えそうだけど、全然わからん。

ブルベリ本見てたら使えそうなやつあった。それがmapped types。これ使ってみるか。

そして、T型から型を取得すればいい。

あとは型引数の部分型を指定してやる。

↓こうなった。

type MyPick<T extends object, K extends keyof T> = {
  [KKey in K]: T[KKey]
}

んでこれは正解っぽい。

7-Readonly (Easy)

問題

組み込みの型ユーティリティReadonly<T>を使用せず、T のすべてのプロパティを読み取り専用にする型を実装します。実装された型のプロパティは再割り当てできません。

例えば:

interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property
思考

さっきの問題と似てるくね?またmapped types使う。

そして、あとはreadonlyつけるだけかな。

readonlyはkey名の前につけるの注意。

type MyReadonly<T> = {
  readonly [TKey in keyof T]: T[TKey]
}

11-Tuple to Object (Easy)

問題

タプルを受け取り、その各値のkey/valueを持つオブジェクトの型に変換する型を実装します。

例えば:

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}
思考

さっきから同じ感じじゃない? これもmapped typesよな。

そしてーーー、mapped typeのkey部分({ [P in K]: T }のK部分)にはユニオン型が必要。

なので、タプル→ユニオンに変換できれば良い。 調べたら、Tuple[number]ってやれば良さそう。

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [TKey in T[number]]: TKey
}

11-Tuple to Object (Easy)

問題

配列Tを受け取り、その最初のプロパティの型を返すFirst<T>を実装します。

例えば:

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3
思考

ほう。これはーーーtypeofの出番か? でもなさそう。普通にこの記述してみたけど、中身が入ってない時はneverを返せって言われる。

type First<T extends any[]> = T[0]

じゃあconditional types使って中身がない時はnever返しますか?(半ギレ)明示的に。 あーーだとしても型引数の配列の長さを知りたいな。lengthプロパティの取り方を調べて。

type First<T extends any[]> = T["length"] extends 0 ? never : T[0]

なんかneverのパターンをconditional types 使うのがちょっと綺麗じゃないけどいいんかな。

解答見たけどconditional types使ってたな。てか T extends []っていう使い方おもろすぎ。あと可変長タプル型使うやつ。

18-Length of Tuple (Easy)

問題

タプルTを受け取り、そのタプルの長さを返す型Length<T>を実装します。

例えば:

type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

type teslaLength = Length<tesla>  // expected 4
type spaceXLength = Length<spaceX> // expected 5
思考

だからさっきやったって。 ↓で終わり。lengthプロパティを呼び出す。

type Length<T extends readonly any[]> = T['length']

43-Exclude (Easy)

問題

組み込みの型ユーティリティExclude <T, U>を使用せず、Uに割り当て可能な型をTから除外する型を実装します。

例えば:

type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
思考

たぶん、mapped types とconditional typesを組み合わせるんかな?いや違うか。

最終的にユニオン型で返さないといけないよな、ユニオン型の引き算ってなんやねん。まぁそれがExcludeの本質なんだけど。

含むかどうかはextendsとかでわかるけど。

...すまん、わからんかった。

(答えを見る時間)

そんな使い方できるのーーー!conditional types。

記事みてみた。

qiita.com

ほえーー覚えとこう。

189-Awaited (Easy)

問題

Promise ライクな型が内包する型をどのように取得すればよいでしょうか。

例えば:Promise<ExampleType>という型がある場合、どのようにして ExampleType を取得すればよいでしょうか。

type ExampleType = Promise<string>

type Result = MyAwaited<ExampleType> // string
思考

これだめだ、たぶんガチで知らないやつ。試行錯誤が無駄なやつ、だって知らないとわからない問題ぽいから。

だから答え見るわ

(答えを見る時間)

へーーーー!!! infer便利やなぁ。これあれば、<>に囲まれた型を取得できるなぁ。

あと PromiseLike。Promiseのオブジェクトの条件がthenが入っていること。だけなのは衝撃。

268-If (Easy)

問題

条件値CCが truthy である場合の戻り値の型TCが falsy である場合の戻り値の型Fを受け取るIfを実装します。 条件値Ctruefalseのどちらかであることが期待されますが、TF は任意の型をとることができます。

例えば:

type A = If<true, 'a', 'b'>; // expected to be 'a'
type B = If<false, 'a', 'b'>; // expected to be 'b'
思考

肌感、めっちゃ余裕じゃね?conditional types使えば良さそう。

type If<C extends boolean, T, F> = C extends true ? T : F

え、解けたw

533-Concat (Easy)

問題

JavaScriptArray.concat関数を型システムに実装します。この型は 2 つの引数を受け取り、受け取ったイテレータの要素を順に含む新しい配列を返します。

例えば:

type Result = Concat<[1], [2]>; // expected to be [1, 2]
思考

んーー?すげぇ実装っぽいけどこれ型システムだけでできるんだ?? まぁあれか、タプル型を返すからいいのか、あくまで型だもんな。

当て勘で↓を書いてみたら当たった。ほぼ実装やん。

type Concat<T extends readonly any[], U extends readonly any[]> = [...T, ...U]

898-Includes (Easy)

問題

JavaScriptArray.include関数を型システムに実装します。この型は、2 つの引数を受け取り、truefalseを出力しなければなりません。

例えば:

type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`
思考

うーん、condditional typesなのはそうじゃないか。 あとは、タプル型でもらったのをユニオン型にして部分型か判断?

んでこうなった↓ だけどこれ不正解ね

type Includes<T extends readonly any[], U> = U extends T[number] ? true : false;

わかんねーー正解を見る。

(正解見てる)

めっちゃなるほど。infer使うのか。いやーーーinferいいな。 inferとconditional typesの合わせ技めっちゃ便利やん。

3057-Push (Easy)

問題

Array.pushジェネリックバージョンを実装します。

例えば:

type Result = Push<[1, 2], '3'> // [1, 2, '3']
思考

なんかーーinferでいけそうだな。 やってみるか、、いけた。

type Push<T extends any[], U> = T extends [...infer Rest] ? [...Rest, U] : never

んーーけど、infer使いたいがためにconditional typesやっているだけで、neverは定義しなくていいんだよな。 答えも見てみるか。

www そりゃそうだわwww

type Push<T extends unknown[], U> = [...T, U]

3060-Unshift (Easy)

問題

Array.unshiftの型バージョンを実装します。

例えば:

type Result = Unshift<[1, 2], 0> // [0, 1, 2,]
思考

...なめすぎでは? ほぼ前問と同じ。

type Unshift<T extends unknown[], U> = [U, ...T]

3312-Parameters (Easy)

問題

組み込みの型ユーティリティParameters<T>を使用せず、Tからタプル型を構築する型を実装します。

例えば:

const foo = (arg1: string, arg2: number): void => {}

type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]
思考

これもinferでいけそう。 やってみるか、、、いけた。なんなんinfer

type MyParameters<T extends (...args: any[]) => any> = T extends (...args: infer Args) => any ? [...Args] : false

解答見てみたら、そもそも展開する必要なかったわ。 そうなのか、タプル型になるのか、inferは。

おわりに

よしーーーEasyモードは終わり。 次はMedium編。

「あれ?前回何やったっけ?」をなくす拡張機能【備忘録】

はじめに

いまこのようにhatenablogを書いているのはいくつか理由があります。
「アウトプットすることで、自身の理解の向上に繋げる」. 「誰かの参考になって欲しい」. など。

そして、「やったことを記録することで、前回どこまでやったかをするに思い出すようにしている」 というのもあります。

しかしこれ、googleの検索履歴とgithubのログからAIに分析してもらって、「前回ここまでやったよーー!」といってもらえるようにできないかな。と思いました。じゃあ作ってみましょう。

前提理解

  • googleの検索履歴からデータを取得して、それをchatGPTのAPIに流して学習させて、概要を表示する
  • もし「isDeveloper」をオンにしたら、githubのcommitメッセージ履歴からも判断させる→commitメッセージモード的な?
  • もし「readCode」をオンにしたら、githubのcommit履歴からも判断させる→codeモード的な?
  • 多分鬼門はgoogleの検索履歴データをちゃんと取得できるかどうか。→これがもしchrome拡張機能しか無理なら、それで作ろう。

調査

どうやらこんなAPIがあるっぽい。

developer.chrome.com

これでサイトの検索履歴が調べられる

あとはChatGPTのAPIはあるだろ。

openai.com

あとgithubAPIもある

qiita.com

いろいろ揃ってはいる。
ホントはこれらを使って事前に「ChatGPTでちゃんとやったことのまとめができるのか」をテストしないといけないと思うんだけど、もうめんどいからやんない。

簡単な拡張機能を作ってみよう

これを参考に簡単なものを作ってみましょう。

yuki.world

まずはアイコンが出てくるだけ

最初にfolerを作って、以下のファイルを作った。階層はこんな感じ。

daily-auto-summary/
└── src/
    ├── manifest.json
    └── popup.html
{
    "name": "HELLO WORLD",
    "description": "chrome拡張機能の基礎",
    "version": "0.0.1",
    "manifest_version": 3,
    "action": {
        "default_popup": "popup.html"
    }
}
<html>
  <body>
    <h1>HELLO WORLD</h1>
  </body>
</html>

じゃあこのfolerをupしてみよう。

chrome拡張機能のページに移動。

(僕はBraveを使っているので、URLはこれですが、それぞれのブラウザの拡張機能のページに行ってください。)

brave://extensions/

これを押して、さっき作成したフォルダの /src を選択する。

すると、できてる!

これの拡張機能をピンドメして、教えてみると、簡単なポップアップが出るようになりました。

h1タグのテキストを取得して表示する

じゃあページのテキストから取得してポップアップに表示してみる。

最終的にこんな感じ。(詳しいことは参考ページに書いてあるから見てみて)

daily-auto-summary/
└── src/
    ├── content.js
    ├── manifest.json
    ├── popup.html
    └── popup.js
{
  "name": "HELLO WORLD",
  "description": "chrome拡張機能の基礎",
  "version": "0.0.1",
  "manifest_version": 3,
  "action": {
    "default_popup": "popup.html"
  },
  "content_scripts": [
    {
      "js": ["content.js"],
      "matches": ["https://qiita.com/*"]
    }
  ]
}
<html>
  <body>
    <h1>HELLO WORLD</h1>
    <input type="text" id="title">
    <button id="btn">text change</button>
    <button id="btn_get">get</button>
    <script src="popup.js"></script>
  </body>
</html>
document.getElementById('btn').addEventListener('click', () => {
  document.querySelector('h1').textContent = document.getElementById('title').value;
})

document.getElementById('btn_get').addEventListener('click', () => {
  chrome.tabs.query( {active:true, currentWindow:true}, (tabs) => {
    chrome.tabs.sendMessage(tabs[0].id, {message: 'getname'}, (content) => {
      if(!content){
        alert('Cannot Get! Try Reload First!');
        return;
      }
      document.getElementById('title').value = content
    });
  });
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  let title = document.getElementsByTagName('h1')[0].textContent
  sendResponse(title);
});

これでQiitaの詳細ページのh1タグを取得できるよ。

chromeAPIで検索履歴を取得する

じゃあやってみよーー

まずは、chrome.historyを使えるようにするために、manifest.jsonを修正

+   "permissions": [
+     "history"
+   ],

これで使えるようになったらしい。

次に履歴を表示するためにpopup.htmlを修正

+     <button id="getHistory">getHistory</button>
+     <div>
+       <ul id="historyBox">
+       </ul>
+     </div>

じゃあpopup.jsでchrome.historyを呼び出す。 以下の記述を追加

document.getElementById('getHistory').addEventListener("click", async() => {
  const now = new Date();

  const text = "";
  const startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0).getTime();
  const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0).getTime();
  const maxResults = 10;
  const historyItems = await chrome.history.search({
    text,
    startTime,
    endTime,
    maxResults,
  });

  console.log(historyItems);

  const ul = document.getElementById('historyBox');
  historyItems.forEach((item) => {
    const li = document.createElement('li');
    li.setAttribute("id", item.id);
    li.setAttribute("key", item.id);
    li.innerHTML = item.title;
    ul.appendChild(li);
  })
})

おおーこれを反映すると取得することができた。

履歴見られるの恥ずかしいね。

GithubAPIでcommit履歴を取得する

手順は以下のように。

  1. アクセストークンを手動で取得して、そのアクセストークンをもとに自身のアカウントのすべてのレポジトリの、すべてのコミット履歴を取得できるか確認
  2. アクセストークンをOAuthなどで取得できるように修正

すべてのコミット履歴を取得する。

まず手動でアクセストークンを発行する。手順は以下。(ChatGPTで取得したけど、これでできることを確認済み)

  • GitHubにログインし、右上のユーザーアイコンをクリックします。
  • ドロップダウンメニューから「Settings」を選択します。
  • 左側のパネルから「Developer settings」をクリックします。
  • 「Personal access tokens」をクリックします。
  • 「Generate new token」ボタンをクリックします。
  • 「Note」に適当な名前を付け、有効期間を設定します。その後、必要な権限を選択します。この場合、「repo」の全ての権限を選択します。
  • 最後に「Generate token」ボタンをクリックします。新しいページに表示されたトークンをメモしてください。このトークンは再度表示されません。

これを使って、1. プライベートレポジトリを取得できるか 2. プライベートレポジトリのコミット履歴を取得できるか を確認する。

# さっき取得したトークンを変数に代入
$ token="your_personal_access_token"

# 1. プライベートレポジトリのみ取得
$ curl -H "Authorization: token $token" https://api.github.com/user/repos | jq -r '.[] | select(.private == true) | .name'

プライベートレポジトリ1
プライベートレポジトリ2
...

# 2. プライベートレポジトリのコミット履歴
$ curl -H "Authorization: token $token" https://api.github.com/repos/{owner}/{プライベートレポジトリ名}/commits

コミット履歴

# 5つのコミットのコミット名だけ取得
$ curl -H "Authorization: Bearer $token" https://api.github.com/repos/ayumu-1212/dnn_of_crystal_grain_diameter_distributional/commits\?per_page\=5 | jq -r '.[] | .commit.message'

できたので、これを拡張機能上でできるようにする。

<html>
  <body>
    <h1>HELLO WORLD</h1>
    <input type="text" id="title">
    <button id="btn">text change</button>
    <button id="btn_get">getH1</button>
+    <button id="getHistory">getHistory</button>
    <button id="getCommits">getCommits</button>
    <div>
      <ul id="historyBox">
      </ul>
    </div>
+    <div>
+      <ul id="commitsBox">
+      </ul>
+    </div>
    <script src="popup.js"></script>
  </body>
</html>
// 追加
document.getElementById('getCommits').addEventListener("click", async() => {

  const token="sample_token"

  const maxResults = 5;
  const commitItems = []

  await fetch(`https://api.github.com/repos/ayumu-1212/dnn_of_crystal_grain_diameter_distributional/commits?per_page=${maxResults}`, {
    headers: {
      Authorization: `token ${token}`
    }
  })
  .then(response => response.json())
  .then(data => {
    data.forEach(d => {
      commitItems.push(d.commit.message)
    });
  })
  .catch(error => console.error(error));

  const ul = document.getElementById('commitsBox');
  commitItems.forEach((item, index) => {
    const li = document.createElement('li');
    li.setAttribute("id", `commit-${index}`);
    li.setAttribute("key", `commit-${index}`);
    li.innerHTML = item;
    ul.appendChild(li);
  })
})

これで、拡張機能を開いて、ボタンを押すと。

いいですねぇ、プライベートレポジトリのcommit履歴を取得できています。

アクセストークンをOAuthとかで取得する

これはFirebaseログイン機能を実装して、そこでアクセストークンを発行すれば可能になりそう。

Firebaseでプロジェクトを作成して、Authenticationを開始する。

Sign-in-methodでGithubを選択し、表示されたCallbackURLをコピーする。

GithubのSettings / Developer Settingsを開き、OAuth Appsで[New OAuth App]を押す。

必要な情報を入力し、Authorization callback URLと表示されているところに、FirebaseでコピーしたURLを貼り付けて、完了する。

すると、クライアントIDクライアントシークレットが表示されるので、Firebaseに入力して[保存]をする。

次にブラウザの拡張機能の設定ページに行き、IDをコピーする。

Firebase / Authentication / Settingを開き、[ドメインの追加]を押す。

そこに先程コピーした拡張機能のIDを使って、 chrome-extension://CHROME_EXTENSION_ID と入力して、[追加]する。

と、ここまで作ってみて、「拡張機能を作るのに、テンプレートなしだときつい」という結論に至った。よって、chrome拡張機能を作成するテンプレートを使う。

plasmoの利用

ちょっと探してみたが、なんかこれしかなさそうだった。

www.plasmo.com

ちょっと使ってみよう。

ayumu1212.hatenablog.com

いいねぇ、使ってみたけど悪くない。

あとこれも読んだ。いいね。

zenn.dev

この段階でわかったことをまとめる。

  • manifestのバージョンによって書き方が結構違う (参考:Zenn)
  • plasmoが問題かと思ったけど、manifestバージョンが3になったことでfirebaseとの互換性がゴミになってる。
That worked. Thank you. For other people that might have this problem, Manifest v3 prohibits the loading external scripts in the script-src directive - all external scripts must be included in the application itself.
引用元:stack overflow

akita, gomen

参照

  • chromeのボイラーテンプレート

github.com

  • manifest ver3の書き方

qiita.com

  • chromeからのログイン機能(公式)

cloud.google.com

  • firebaseでのGithubのログイン機能実装

firebase.google.com

ただただ最近の技術のキャッチアップ【備忘録】

はじめに

以下のことをキャッチアップしました

  • tRPC
  • gRPC
  • hono
  • deno
  • bun
  • GraphQL

詳しくは後日書くので、今日はどういう記事を読んだかだけ貼っとく。。許して。。

キャッチアップ

tRPC

これよんだ

qiita.com

zenn.dev

gRPC

これよんだ

qiita.com

deno

これよんだ

qiita.com

hono

これよんだ

zenn.dev

bun

これよんだ

GraphQL

これよんだ

zenn.dev

ISUCON本を見ながら書いたやつら【備忘録】

はじめに

これを読みながら勉強している備忘録

https://amzn.asia/d/6bd2FtXamzn.asia

1-2章

読んだ、2章で Prometheus を紹介していたが、実際に触ってみたいのであとで戻ってこよう。

3章

private-isuの準備

これを用いながら、いろいろチューニングしていく。

github.com

dockerで立ち上げたいんだけど、なんかうまく行かない。

→ 立ち上がってたわ。portがないところだった。

次に、nginxのログの設定を終わらせて、ログの解析に入る。

alpとabの準備と実行

alpというものを使うらしいが、localにinstallしなくなかったので、新しくcontainerを立ち上げてそこに入れた。

# 以下を追加
  bench:
    build: bench/
    container_name: isu-bench
    volumes:
      - ./logs/nginx:/app/src/logs/nginx
    working_dir: /app/src
    tty: true
FROM alpine:latest

# alpのinstall
RUN apk add curl unzip
RUN curl -L -O https://github.com/tkuchiki/alp/releases/download/v1.0.21/alp_linux_amd64.zip
RUN unzip alp_linux_amd64.zip
RUN mv alp /usr/bin/alp && rm -rf alp_linux_amd64.zip

# abのinstall
RUN apk add apache2-utils

これで、立ち上げてやってみる。

#alpを動かす。
$ alp json --file logs/nginx/access.log
+-------+-----+-----+-----+-----+-----+--------+----------------------+-------+-------+-------+-------+-------+-------+-------+--------+-------------+-------------+-------------+-------------+
| COUNT | 1XX | 2XX | 3XX | 4XX | 5XX | METHOD |         URI          |  MIN  |  MAX  |  SUM  |  AVG  |  P90  |  P95  |  P99  | STDDEV |  MIN(BODY)  |  MAX(BODY)  |  SUM(BODY)  |  AVG(BODY)  |
+-------+-----+-----+-----+-----+-----+--------+----------------------+-------+-------+-------+-------+-------+-------+-------+--------+-------------+-------------+-------------+-------------+
| 1     | 0   | 1   | 0   | 0   | 0   | GET    | /posts/10002         | 0.080 | 0.080 | 0.080 | 0.080 | 0.080 | 0.080 | 0.080 | 0.000  | 1948.000    | 1948.000    | 1948.000    | 1948.000    |
| 1     | 0   | 0   | 1   | 0   | 0   | POST   | /login               | 0.058 | 0.058 | 0.058 | 0.058 | 0.058 | 0.058 | 0.058 | 0.000  | 0.000       | 0.000       | 0.000       | 0.000       |
| 1     | 0   | 1   | 0   | 0   | 0   | GET    | /login               | 0.001 | 0.001 | 0.001 | 0.001 | 0.001 | 0.001 | 0.001 | 0.000  | 1166.000    | 1166.000    | 1166.000    | 1166.000    |
| 2     | 0   | 0   | 2   | 0   | 0   | GET    | /img/ajax-loader.gif | 0.005 | 0.010 | 0.015 | 0.007 | 0.010 | 0.010 | 0.010 | 0.003  | 0.000       | 0.000       | 0.000       | 0.000       |
| 2     | 0   | 0   | 2   | 0   | 0   | GET    | /css/style.css       | 0.023 | 0.028 | 0.051 | 0.026 | 0.028 | 0.028 | 0.028 | 0.003  | 0.000       | 0.000       | 0.000       | 0.000       |
| 2     | 0   | 0   | 2   | 0   | 0   | GET    | /favicon.ico         | 0.006 | 0.006 | 0.012 | 0.006 | 0.006 | 0.006 | 0.006 | 0.000  | 0.000       | 0.000       | 0.000       | 0.000       |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9991.jpg      | 0.002 | 0.016 | 0.027 | 0.007 | 0.016 | 0.016 | 0.016 | 0.006  | 153638.000  | 153645.000  | 614571.000  | 153642.750  |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9988.jpg      | 0.002 | 0.010 | 0.023 | 0.006 | 0.010 | 0.010 | 0.010 | 0.003  | 111647.000  | 111647.000  | 446588.000  | 111647.000  |
| 4     | 0   | 0   | 4   | 0   | 0   | GET    | /js/main.js          | 0.005 | 0.025 | 0.049 | 0.012 | 0.025 | 0.025 | 0.025 | 0.008  | 0.000       | 0.000       | 0.000       | 0.000       |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/10000.png     | 0.036 | 0.094 | 0.245 | 0.061 | 0.094 | 0.094 | 0.094 | 0.023  | 1057874.000 | 1057904.000 | 4231558.000 | 1057889.500 |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9997.jpg      | 0.023 | 0.065 | 0.192 | 0.048 | 0.065 | 0.065 | 0.065 | 0.016  | 176577.000  | 176577.000  | 706308.000  | 176577.000  |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9993.jpg      | 0.006 | 0.035 | 0.071 | 0.018 | 0.035 | 0.035 | 0.035 | 0.011  | 85639.000   | 85646.000   | 342563.000  | 85640.750   |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9994.jpg      | 0.007 | 0.057 | 0.113 | 0.028 | 0.057 | 0.057 | 0.057 | 0.022  | 105271.000  | 105278.000  | 421105.000  | 105276.250  |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9995.jpg      | 0.007 | 0.062 | 0.131 | 0.033 | 0.062 | 0.062 | 0.062 | 0.021  | 123497.000  | 123497.000  | 493988.000  | 123497.000  |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /                    | 0.782 | 1.001 | 3.551 | 0.888 | 1.001 | 1.001 | 1.001 | 0.080  | 34708.000   | 35496.000   | 139629.000  | 34907.250   |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9990.jpg      | 0.004 | 0.007 | 0.022 | 0.005 | 0.007 | 0.007 | 0.007 | 0.001  | 102493.000  | 102501.000  | 409982.000  | 102495.500  |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9989.jpg      | 0.002 | 0.010 | 0.020 | 0.005 | 0.010 | 0.010 | 0.010 | 0.003  | 107584.000  | 107584.000  | 430336.000  | 107584.000  |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9996.jpg      | 0.008 | 0.050 | 0.086 | 0.021 | 0.050 | 0.050 | 0.050 | 0.017  | 375194.000  | 375218.000  | 1500848.000 | 375212.000  |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9986.jpg      | 0.002 | 0.010 | 0.020 | 0.005 | 0.010 | 0.010 | 0.010 | 0.003  | 86701.000   | 86732.000   | 346867.000  | 86716.750   |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9987.jpg      | 0.005 | 0.014 | 0.041 | 0.010 | 0.014 | 0.014 | 0.014 | 0.003  | 367761.000  | 367792.000  | 1471075.000 | 367768.750  |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9985.jpg      | 0.003 | 0.011 | 0.031 | 0.008 | 0.011 | 0.011 | 0.011 | 0.003  | 83357.000   | 83364.000   | 333448.000  | 83362.000   |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9983.jpg      | 0.004 | 0.015 | 0.031 | 0.008 | 0.015 | 0.015 | 0.015 | 0.004  | 98175.000   | 98198.000   | 392754.000  | 98188.500   |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9981.jpg      | 0.002 | 0.010 | 0.020 | 0.005 | 0.010 | 0.010 | 0.010 | 0.003  | 81479.000   | 81493.000   | 325943.000  | 81485.750   |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9982.jpg      | 0.004 | 0.006 | 0.021 | 0.005 | 0.006 | 0.006 | 0.006 | 0.001  | 150023.000  | 150046.000  | 600137.000  | 150034.250  |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9999.jpg      | 0.016 | 0.055 | 0.155 | 0.039 | 0.055 | 0.055 | 0.055 | 0.014  | 90021.000   | 90036.000   | 360099.000  | 90024.750   |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/10001.jpg     | 0.008 | 0.024 | 0.064 | 0.016 | 0.024 | 0.024 | 0.024 | 0.008  | 72217.000   | 72217.000   | 288868.000  | 72217.000   |
| 4     | 0   | 0   | 4   | 0   | 0   | GET    | /js/timeago.min.js   | 0.005 | 0.020 | 0.041 | 0.010 | 0.020 | 0.020 | 0.020 | 0.006  | 0.000       | 0.000       | 0.000       | 0.000       |
| 4     | 0   | 4   | 0   | 0   | 0   | GET    | /image/9998.jpg      | 0.013 | 0.043 | 0.107 | 0.027 | 0.043 | 0.043 | 0.043 | 0.011  | 61716.000   | 61740.000   | 246912.000  | 61728.000   |
| 5     | 0   | 5   | 0   | 0   | 0   | GET    | /image/10002.jpg     | 0.005 | 0.014 | 0.042 | 0.008 | 0.014 | 0.014 | 0.014 | 0.003  | 279915.000  | 279938.000  | 1399613.000 | 279922.600  |
+-------+-----+-----+-----+-----+-----+--------+----------------------+-------+-------+-------+-------+-------+-------+-------+--------+-------------+-------------+-------------+-------------+

# 次に、nginxのコンテナにちょっかい出す。
$ ab -c 1 -n 10 http://isu-nginx/
This is ApacheBench, Version 2.3 <$Revision: 1903618 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking webapp-app-1 (be patient).....done


Server Software:
Server Hostname:        webapp-app-1
Server Port:            8080

Document Path:          /
Document Length:        34641 bytes

Concurrency Level:      1
Time taken for tests:   7.890 seconds
Complete requests:      10
Failed requests:        0
Total transferred:      347370 bytes
HTML transferred:       346410 bytes
Requests per second:    1.27 [#/sec] (mean)
Time per request:       788.973 [ms] (mean)
Time per request:       788.973 [ms] (mean, across all concurrent requests)
Transfer rate:          43.00 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.4      0       1
Processing:   763  789  45.5    781     915
Waiting:      762  788  45.5    780     914
Total:        763  789  45.9    781     916

Percentage of the requests served within a certain time (ms)
  50%    781
  66%    784
  75%    786
  80%    789
  90%    916
  95%    916
  98%    916
  99%    916
 100%    916 (longest request)

$ tail -n 10 logs/nginx/access.log | alp json -o count,method,uri,min,avg,max
+-------+--------+-----+-------+-------+-------+
| COUNT | METHOD | URI |  MIN  |  AVG  |  MAX  |
+-------+--------+-----+-------+-------+-------+
| 10    | GET    | /   | 0.757 | 0.773 | 0.828 |
+-------+--------+-----+-------+-------+-------+

いいね。

ログファイルのローテート

これを準備

#!/bin/sh

mv /var/log/nginx/access.log /var/log/nginx/access.log.`date +%Y%m%d-%H%M%S`

nginx -s reopen

んで、 docker exec で実行すると、nginx上のlogファイルを日付をつけてコピーするようになった。

$ docker exec -it isu-nginx sh /var/log/nginx/rotate.sh

mysqlの解析

mysqldumpslowがないよぉ。

つぎに mysqldumpslowを用いてスロークエリログを読み取ってみようのコーナーだったのだが、M1macのdockerのmysql-imageには入っていなかった。

ということで、imageをこれに変えました、ここにはあったよ。

  mysql:
    image: mysql:8.0-debian
    platform: linux/amd64
    container_name: isu-mysql
.
.
.

これで、以下のコマンドが isu-mysql内でうてるようになった。

$ mysqldumpslow /var/log/mysql/mysql-slow.log

もちろんだけど、このログを出力するのにも設定がいるから、それは本を読もうね。

mysqlに入れないよぉ。

isu-mysql コンテナ内でmysqlにログインできなかったのだが、落ち着いてdocker-compose.yamlをみて、ログイン情報を読もうね。

以下の情報で行けるはずよ。

$ mysql -uroot -proot

4章

k6のinstall

これだけやっかいだった。結論は設定でできます。

services:
  nginx:
    image: nginx:1.24
    container_name: isu-nginx
    volumes:
      - ./etc/nginx/conf.d:/etc/nginx/conf.d
      - ./public:/public
      - ./logs/nginx:/var/log/nginx
    ports:
      - "80:80"
    links:
      - app

  app:
    # Go実装の場合は golang/ PHP実装の場合は php/
    build: ruby/
    container_name: isu-app
    environment:
      ISUCONP_DB_HOST: mysql
      ISUCONP_DB_PORT: 3306
      ISUCONP_DB_USER: root
      ISUCONP_DB_PASSWORD: root
      ISUCONP_DB_NAME: isuconp
      ISUCONP_MEMCACHED_ADDRESS: memcached:11211
    links:
      - mysql
      - memcached
    volumes:
      - ./public:/home/public
    init: true
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 1g

  mysql:
    image: mysql:8.0-debian
    platform: linux/amd64
    container_name: isu-mysql
    environment:
      #- "TZ=Asia/Tokyo"
      - "MYSQL_ROOT_HOST=%"
      - "MYSQL_ROOT_PASSWORD=root"
    volumes:
      - mysql:/var/lib/mysql
      - ./etc/my.cnf:/etc/my.cnf
      - ./sql:/docker-entrypoint-initdb.d
      - ./logs/mysql:/var/log/mysql
    ports:
      - "3306:3306"
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 1g

  memcached:
    image: memcached:1.6

  bench:
    build: bench/
    container_name: isu-bench
    volumes:
      - ./logs/nginx:/app/logs/nginx
      - ./bench/src:/app/src
    working_dir: /app/src
    tty: true

volumes:
  mysql:
FROM debian:bullseye-slim

# alpのinstall
RUN apt update && apt install -y curl unzip
RUN curl -L -O https://github.com/tkuchiki/alp/releases/download/v1.0.21/alp_linux_amd64.zip
RUN unzip alp_linux_amd64.zip
RUN mv alp /usr/bin/alp && rm -rf alp_linux_amd64.zip

# abのinstall
RUN apt install -y apache2-utils

# k6のinstall
RUN curl -L -O https://github.com/grafana/k6/releases/download/v0.47.0/k6-v0.47.0-linux-amd64.tar.gz
RUN tar -zxvf k6-v0.47.0-linux-amd64.tar.gz
RUN mv k6-v0.47.0-linux-amd64/k6 /usr/bin/k6 && rm -rf k6-v0.47.0-linux-amd64 && rm -rf k6-v0.47.0-linux-amd64.tar.gz

結局curlでdownloadしてくるんだけど、バージョンとか環境とかが違うと

「それバイナリファイルじゃねぇから!!!」

って切れ方でバイナリファイル否定されるので注意。

5章

pt-query-digest

なんやら pt-query-digestというもんが必要らしいので、それを入れる

$  apt install percona-toolkit
$  pt-query-digest --version
pt-query-digest 3.2.1

query-digester

これのほうが便利らしいからいれる。 だけどgitを使ってinstallする方法が書かれていたので、それに乗っ取る

$ apt install git

$ cd /usr/src

$ git clone git@github.com:kazeburo/query-digester.git

$ cd query-digester
$ install query-digester /usr/local/bin

これで実行してもmysqlの権限がないよ、と怒られたので、ファイル自体の設定をいじってpasswordを入力するようにする。

- 86      open(my $pipe, '|-', $mysql, @mysqlopt, '--sigint-ignore');
+ 86      open(my $pipe, '|-', $mysql, @mysqlopt, '-p', '--sigint-ignore');

そしたらpasswordを要求されるようになるので入力しようね。

$ query-digester -duration 10
exec mysql to change long_query_time and slow_query_log_file
save slowlog to /tmp/slow_query_20230922114025.log
wait 10 seconds
Enter password:
finished capturing slowlog.
start query-digest
finished pt-query-digest.
digest saved to /tmp/slow_query_20230922114025.digest

fastAPIを触ってみた【備忘録】

はじめに

ハッカソンの準備でFastAPIを触る必要があったので、触ってみました。

参考

fastapi.tiangolo.com

イントロ

さぁやってみんべ。

docker環境

いつものごとく。 とりあえずpython3の環境を構築

FROM python:3.9-slim

WORKDIR /app

(そういえば、docker-compose.yamlじゃなくて compose.yamlでいいらしい(公式推奨)よ!!)

version: "3"

services:
  api:
    build: .
    volumes:
      - ./app:/app
    working_dir: /app
    ports:
      - 8080:8080
    tty: true

諸々install

# pipのバージョンアップ
$ pip install --upgrade pip

# fastapiをinstall
$ pip3 install fastapi

# ローカルウェブサーバー
$ pip3 install "uvicorn[standard]"

fastapiの開始

main.pyを書いてみた。

from typing import Union

from fastapi import FastAPI

app = FastAPI()

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

@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

これをunicornで実行!

$ uvicorn main:app --reload --host 0.0.0.0 --port 8080

すると、

がもう利用可能に!!

すごいね、早いね笑

tutorial

とりあえずチュートリアルはさらっとこうか。

fastapi.tiangolo.com

参考

一番参考 DockerでFastAPIの環境を作ってGETするまで

二番参考 FastAPIでPythonアプリを素早く構築する |Kinsta®

公式 FastAPI

openapiとそれ関連いろいろ実装してみるンゴ (後編)【備忘録】

はじめに

これの続きな?

ayumu1212.hatenablog.com

以下実装

設計(再掲)

Style周りの環境構築

アプリ作っていく前にUIライブラリとcss-in-jsの導入する。

UIライブラリの選定

まぁデザイン力ないからね、こういうの使うほうが速い。

参考

note.com

いろいろあるもんだなぁ。
だけどなんかおしゃれなのあったーー!笑

nextui.org

これ、nextjsとの互換性強そう。

CSS-in-JSの選定

ライブラリ入れるにしても、その土壌がないとだめだわ。

これみてみる。

2022.stateofcss.com

ピックアップするとこれらが気になる。

  • Styled Component
  • CSS Modules
  • Styled JSX
  • vanilla extract
  • Stitches
  • Emotion

どれを使おうかなと悩んだ。
悩んだ記事はこちら

ayumu1212.hatenablog.com

結論は、Stitchesを使ってみようかな。 styled componentsを使おうかと思ったけど、なんかこっちのほうが新しいし、nextUIもこれ使ってるぽいからね。

stitches.dev

Stitchesの導入

公式でnextjsに導入しているやつがあったからこれみてやろう

stitches.dev

まずinstall

# uiレポジトリに導入
$ yarn workspace ui add @stitches/react

# cdsアプリにも導入
$ yarn workspace cds add @stitches/react

次に、stiches.config.tsを作らないと行けないんだけど、たぶんこれ共通の設定をapps/cdsapps/cdmpackages/ui にinstallしたいから、packagesにフォルダを作ろう。

$ mkdir packages/stitches-config
$ cd packages/stitches-config

$ yarn init
yarn init v1.22.19
question name (stitches-config): 
question version (1.0.0): 
question description: package of stiches config files.
question entry point (index.js): 
question repository url: 
question author: 
question license (MIT): 
question private: true
success Saved package.json
Done in 61.99s.

$ cd ../../

$ yarn workspace stitches-config add @stitches/react

できて、編集を加えたファイルがこちら。

{
  "name": "stitches-config",
  "version": "1.0.0",
  "description": "package of stiches config files.",
  "main": "index.ts",
  "private": true,
  "dependencies": {
    "@stitches/react": "^1.2.8"
  }
}

じゃあstitches.config.ts を作る。

import { createStitches } from '@stitches/react'

export const { styled, getCssText } = createStitches({
  theme: {
    fonts: {
      system: 'system-ui',
    },
    colors: {
      hiContrast: 'hsl(206,10%,5%)',
      loContrast: 'white',
    },
    fontSizes: {
      1: '13px',
      2: '15px',
      3: '17px',
    },
  },
})

とりあえずこれにする。 これを、index.tsでexportする。

export { styled, getCssText } from './stitches.config'

そしたらこれをuiのレポジトリで使う。

import * as React from "react"
import { styled } from 'stitches-config'

export const Button = styled('button', {
  fontFamily: '$system',
  color: '$hiContrast',

  variants: {
    size: {
      1: {
        fontSize: '$1',
      },
      2: {
        fontSize: '$2',
      },
      3: {
        fontSize: '$3',
      },
    },
  },
})

使えてそーー! じゃあ、これをcdsレポジトリで使って表示してみる。

import { Button } from "ui"

export default function View() {
  return (
    <div>
      <p>this is cds</p>
      <Button size={3}>ボタン</Button>
    </div>
  )
}

あとSSRの設定もするみたい。 新たにpagesの下に_document.tsxを作っておく

import React from 'react'
import NextDocument, { Html, Head, Main, NextScript } from 'next/document'
import { getCssText } from 'stitches-config'

export default class Document extends NextDocument {
  render() {
    return (
      <Html lang="ja">
        <Head>
          <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

さぁ表示!

完璧か?

NextUIの導入

次はこれの導入。結構わくわくしてる。

$ yarn workspace cds add @nextui-org/react
import React from 'react'
+ import NextDocument, { Html, Head, Main, NextScript, DocumentContext } from 'next/document'
import { getCssText } from 'stitches-config'
+ import { CssBaseline } from "@nextui-org/react";

export default class Document extends NextDocument {
+  static async getInitialProps(ctx: DocumentContext) {
+    const initialProps = await NextDocument.getInitialProps(ctx)
+    return {
+      ...initialProps,
+      styles: React.Children.toArray(initialProps.styles),
+    }
;  }

  render() {
    return (
      <Html lang="ja">
        <Head>
+          {CssBaseline.flush()}
          <style id="stitches" dangerouslySetInnerHTML={{ __html: getCssText() }} />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

Providerの設置。

import { NextUIProvider } from '@nextui-org/react'
import type { AppProps } from "next/app";

export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <NextUIProvider>
      <Component {...pageProps} />
    </NextUIProvider>
  )
}

これで準備ができた。 じゃあやってみるべ!!

- import { Button } from "ui"
+ import { Button } from "@nextui-org/react";

export default function View() {
  return (
    <div>
      <p>this is cds</p>
-      <Button size={3}>ボタン</Button>
+      <Button color="gradient" auto shadow>いい感じのボタン</Button>
    </div>
  )
}

えーーー、、きれぇーーー。。ww

準備オーケー

CDS作成

じゃあついにCDS作っていくか。

一覧画面

ホームページの一覧画面から。コンセプトは「イメージで感じ取るブログ」。本屋の背表紙で買うような感じで。

Cardコンポーネント

まずはCard作るべ。 そのためにuiにNextUIいれる。

あと、このプロジェクトの日付管理はdate-fnsにしよ。cdateも気になるけども。

qiita.com

$ yarn workspace ui add @nextui-org/react date-fns
$ yarn workspace cds add date-fns
import { Button, Card, Col, Row, Text } from "@nextui-org/react"
import { format } from 'date-fns'

type Props = {
  userName: string
  date: Date
  imageUrl: string
  isNew?: boolean
  title: string
}

export const BlogCard = ({
  userName = 'unknown',
  date = new Date(),
  imageUrl = 'https://nextui.org/images/card-example-1.jpeg',
  isNew = false,
  title = 'unknown'
}: Props) => {
  return (
    <Card css={{ w: "100%", h: "400px" }}>
      <Card.Header css={{ position: "absolute", zIndex: 1, top: 5 }}>
        <Col>
          <Text size={12} weight="bold" transform="uppercase" color="#ffffffAA">
            {isNew ? 'New' : ''}
          </Text>
          <Text h3 color="black">
            {title}
          </Text>
        </Col>
      </Card.Header>
      <Card.Body css={{ p: 0 }}>
        <Card.Image
          src={imageUrl}
          width="100%"
          height="100%"
          objectFit="cover"
          alt="Card example background"
        />
      </Card.Body>
      <Card.Footer
        isBlurred
        css={{
          position: "absolute",
          bgBlur: "#ffffff66",
          borderTop: "$borderWeights$light solid rgba(255, 255, 255, 0.2)",
          bottom: 0,
          zIndex: 1,
        }}
      >
        <Row>
          <Col>
            <Text color="#000" size={12}>
              {format(date, 'yyyy/MM/dd')}
            </Text>
            <Text color="#000" size={14}>
              @{userName}
            </Text>
          </Col>
          <Col>
            <Row justify="flex-end">
              <Button flat auto rounded color="secondary">
                <Text
                  css={{ color: "inherit" }}
                  size={12}
                  weight="bold"
                  transform="uppercase"
                >
                  Show
                </Text>
              </Button>
            </Row>
          </Col>
        </Row>
      </Card.Footer>
    </Card>
  )
};

これで、アプリ側から呼ぶとエラーが。

エラー内容と解決策はこれ。

zenn.dev

なんか他のレポジトリからimportするときはこの設定が必要っぽい...?ほんとか?
まぁけどこれは入れた。

module.exports = {
  experimental: {
    externalDir: true,
  },
}

いい感じに表示されてきた。

headerコンポーネント

とりあえずHeaderコンポーネントを作った。

import { Button, Link, Navbar } from "@nextui-org/react"

export const Header = () => {
  return (
    <Navbar isBordered variant='static' color="secondary">
      <Navbar.Content hideIn="xs">
        <Navbar.Link href="#">Features</Navbar.Link>
        <Navbar.Link isActive href="#">Customers</Navbar.Link>
        <Navbar.Link href="#">Pricing</Navbar.Link>
        <Navbar.Link href="#">Company</Navbar.Link>
      </Navbar.Content>
      <Navbar.Content>
        <Navbar.Link color="inherit" href="#">
          Login
        </Navbar.Link>
        <Navbar.Item>
          <Button auto flat as={Link} href="#">
            Sign Up
          </Button>
        </Navbar.Item>
      </Navbar.Content>
    </Navbar>
  )
}

これを全ページで適用したい。どうやら、Layoutというものを定義したほうが良いらしい。

nextjs-ja-translation-docs.vercel.app

なので作ってみた。

import { ReactNode } from 'react';
import { Header } from 'ui'

type Props = {
  children: ReactNode
}

export const Layout = ({ children }: Props) => {
  return (
    <>
      <Header />
      <main>
        {children}
      </main>
    </>
  );
}

んでどうやら子要素に指定するのはReactNodeReactElement があるが、用途が違うらしい。

  • ReactNode : まじで何でもいい。
  • ReactElement : 一個の要素をよこせ。

zenn.dev

んで、作ったLayoutを_app.tsx に配置

import { NextUIProvider, useSSR } from '@nextui-org/react'
import type { AppProps } from "next/app";
+ import { Layout } from '../components/Layout';

export default function MyApp({ Component, pageProps }: AppProps) {
  const { isBrowser } = useSSR()
  return (
    isBrowser && (
      <NextUIProvider>
+      <Layout>
          <Component {...pageProps} />
+      </Layout>
      </NextUIProvider>
    )
  )
}

これでいい感じに表示された。

footerの作成

NextUIにfooterがなかったから、stitchesで作った。
とりあえずこんな感じ。

import { styled } from "stitches-config"
import { theme } from "@nextui-org/react"

export const FOOTER_HEIGHT = '4rem'

export const Footer = () => {
  return (
    <FooterWrapper>
      <FooterContent>
        © Chanabe Corp. All rights reserved.
      </FooterContent>
    </FooterWrapper>
  )
}

const FooterWrapper = styled('footer', {
  position: 'absolute',
  bottom: 0,
  width: '100%',
  flexGrow: 0,
  flexShrink: 0,
})

const FooterContent = styled('div', {
  width: '100%',
  height: FOOTER_HEIGHT,
  lineHeight: FOOTER_HEIGHT,
  textAlign: 'center',
  backgroundColor: theme.colors.blue100,
})

NextUIはthemeも提供していてありがたいなぁ。
デザイナーさんがやるところを全部やってくれる。

nextui.org

これをそのまんまLayoutの一番下にやると、こんな感じに。

みたらわかるようにFooterが上になっちゃう。

Layoutを変えて、コンテンツが画面高さよりも少ない場合も最下部にいくようにしよう

import { ReactNode } from 'react';
import { Header, Footer, FOOTER_HEIGHT } from 'ui'
import { styled, css } from 'stitches-config';
import { theme } from '@nextui-org/react';

type Props = {
  children: ReactNode
}

export const Layout = ({ children }: Props) => {
  return (
    <Container>
      <Header />
      <Main>
        {children}
      </Main>
      <Footer />
    </Container>
  )
}

const Container = styled('div', {
  minHeight: '100vh',
  display: 'flex',
  flexDirection: 'column',
})

const Main = styled('main', {
  flexGrow: 1,
  marginBottom: FOOTER_HEIGHT,
  backgroundColor: theme.colors.gray50
})

flexを使って最下部に持っていった。
完成したのはこんな感じ。

これやりながらわかったんだけど、Stitchesってすごい可変的な引数のとり方( 例:width: $propsWidth ) できないっぽい?マジ?
まぁやらないほうがいいのはわかるし、思想も理解できるけど、ここまでの制限があると後々少し嫌になりそう。

style整え

いろいろstyleを整えて、最終調整

完成したらこんな感じになった。

詳細ページの作成

次は詳細ページを作っていこう。

どんなブログがいいだろうか。「本屋を背表紙で買った人はおそらく作者に興味がない」という想像から、作者情報は文末にしよう。

けど目次は横がいい!(個人的欲求)

あともしできるんだったら、ブログを進んでいくごとに夕焼けになるようにしたい。(なんか思いつき)

...かな?とりあえずここまで作ってみよう。

ページの枠の作成

とりあえずページの枠を作成しよう。

routingはこれを参考に。

nextjs-ja-translation-docs.vercel.app

これでfile作成

import { Text } from "@nextui-org/react";

export default function View() {
  return (
    <Text>詳細ページです</Text>
  )
}

よし、表示もできるっぽい

ざっくりとレイアウト作成

そういえば、「何かを囲う時のclassの接尾字の命名規則」をchatGPTに作ってもらったので記載。

Q. 何かを囲う時は、class名の末尾に何を使うのが適切? A. 何かを囲む要素の場合、一般的には以下のような接尾辞がclass名に使われることが多いです。 1. -wrapper(ラッパー) - 例: content-wrapper 2. -container(コンテナ) - 例: header-container 3. -box(ボックス) - 例: search-box 4. -area(エリア) - 例: footer-area 5. -section(セクション) - 例: banner-section ただし、命名規則はプロジェクトやチームによって異なる場合があるため、一貫性を保つためにコーディング規約やガイドラインを設定することが推奨されます。このような規約やガイドラインに従ってclass名を選択することが適切です。
引用元:ChatGPT
Q. じゃあいま出した5つのうち、最初の3つだけの使用規則を作ってください。 A. 以下に、1-3の接尾辞を使った改訂された命名規則の使用例を示します。 1. -wrapper(ラッパー) 使用例: content-wrapper, image-wrapper 使用目的: 他の要素を包み、レイアウトやスタイリングの目的で使用される。余白やサイズ調整などのスタイルを適用する場合に適している。 2. -container(コンテナ) 使用例: header-container, form-container 使用目的: 関連する要素をグループ化し、共通のスタイリングやレイアウトを適用する。親要素に対してグリッドシステムやフレックスボックスを適用する場合に適している。 3. -box(ボックス) 使用例: search-box, info-box 使用目的: 独立した機能を持つコンポーネントを表現する。そのコンポーネント内で独自のスタイリングやレイアウトが必要な場合に適している。 これらの命名規則は、一貫性を保つためにプロジェクトやチームで共有されるべきです。また、プロジェクトに応じてカスタマイズされることがあります。
引用元:ChatGPT

あとこれめっちゃめちゃ参考になった。

blog.ojisan.io

いやーけど、style関数できないのきつくないかい?選択肢でしかpropsを与えられないのは辛い。imageUrlとか与えたいもん。
無理なのでcssで与えるようにしました。

こんな感じで設定しました。

import { Col, Container, Image, Row, Text, User } from "@nextui-org/react";
import { css, styled } from "stitches-config";

export default function View() {
  return (
    <Container gap={0} md>
      <Row gap={0} justify="center">
        <Col span={2}>
          <BlogThumbnailImageStyled css={{ backgroundImage: `url(https://nextui.org/images/card-example-${6}.jpeg)` }} />
        </Col>
      </Row>
      <Row gap={0} justify="center">
        <Col span={8}>
          <BlogTitleWrapperStyled>
            <Text h2>羅生門</Text>
          </BlogTitleWrapperStyled>
        </Col>
      </Row>
      <Row>
        <Text>
          ある日の暮方の事である。一人の下人げにんが、羅生門らしょうもんの下で雨やみを待っていた。
          「おのれ、どこへ行く。」
          下人は、老婆が死骸につまずきながら、慌てふためいて逃げようとする行手を塞ふさいで、こう罵ののしった。老婆は、それでも下人をつきのけて行こうとする。下人はまた、それを行かすまいとして、押しもどす。二人は死骸の中で、しばらく、無言のまま、つかみ合った。しかし勝敗は、はじめからわかっている。下人はとうとう、老婆の腕をつかんで、無理にそこへ※(「てへん+丑」、第4水準2-12-93)ねじ倒した。丁度、鶏にわとりの脚のような、骨と皮ばかりの腕である。
          「何をしていた。云え。云わぬと、これだぞよ。」
          下人は、老婆をつき放すと、いきなり、太刀の鞘さやを払って、白い鋼はがねの色をその眼の前へつきつけた。けれども、老婆は黙っている。両手をわなわなふるわせて、肩で息を切りながら、眼を、眼球めだまが※(「目+匡」、第3水準1-88-81)まぶたの外へ出そうになるほど、見開いて、唖のように執拗しゅうねく黙っている。これを見ると、下人は始めて明白にこの老婆の生死が、全然、自分の意志に支配されていると云う事を意識した。そうしてこの意識は、今までけわしく燃えていた憎悪の心を、いつの間にか冷ましてしまった。後あとに残ったのは、ただ、ある仕事をして、それが円満に成就した時の、安らかな得意と満足とがあるばかりである。そこで、下人は、老婆を見下しながら、少し声を柔らげてこう云った。
          「己おれは検非違使けびいしの庁の役人などではない。今し方この門の下を通りかかった旅の者だ。だからお前に縄なわをかけて、どうしようと云うような事はない。ただ、今時分この門の上で、何をして居たのだか、それを己に話しさえすればいいのだ。」
          すると、老婆は、見開いていた眼を、一層大きくして、じっとその下人の顔を見守った。※(「目+匡」、第3水準1-88-81)まぶたの赤くなった、肉食鳥のような、鋭い眼で見たのである。それから、皺で、ほとんど、鼻と一つになった唇を、何か物でも噛んでいるように動かした。細い喉で、尖った喉仏のどぼとけの動いているのが見える。その時、その喉から、鴉からすの啼くような声が、喘あえぎ喘ぎ、下人の耳へ伝わって来た。
          「この髪を抜いてな、この髪を抜いてな、鬘かずらにしようと思うたのじゃ。」
          下人は、老婆の答が存外、平凡なのに失望した。そうして失望すると同時に、また前の憎悪が、冷やかな侮蔑ぶべつと一しょに、心の中へはいって来た。すると、その気色けしきが、先方へも通じたのであろう。老婆は、片手に、まだ死骸の頭から奪った長い抜け毛を持ったなり、蟇ひきのつぶやくような声で、口ごもりながら、こんな事を云った。
          「成程な、死人しびとの髪の毛を抜くと云う事は、何ぼう悪い事かも知れぬ。じゃが、ここにいる死人どもは、皆、そのくらいな事を、されてもいい人間ばかりだぞよ。現在、わしが今、髪を抜いた女などはな、蛇を四寸しすんばかりずつに切って干したのを、干魚ほしうおだと云うて、太刀帯たてわきの陣へ売りに往いんだわ。疫病えやみにかかって死ななんだら、今でも売りに往んでいた事であろ。それもよ、この女の売る干魚は、味がよいと云うて、太刀帯どもが、欠かさず菜料さいりように買っていたそうな。わしは、この女のした事が悪いとは思うていぬ。せねば、饑死をするのじゃて、仕方がなくした事であろ。されば、今また、わしのしていた事も悪い事とは思わぬぞよ。これとてもやはりせねば、饑死をするじゃて、仕方がなくする事じゃわいの。じゃて、その仕方がない事を、よく知っていたこの女は、大方わしのする事も大目に見てくれるであろ。」
          老婆は、大体こんな意味の事を云った。
        </Text>
      </Row>
      <Row gap={1}>
        <BlogDetailFooterCss>
          <User src={`https://i.pravatar.cc/150?u=a042581f4e29026024d`} name={`@ayumu-1212`} size='xl' />
          <Text size="$xl">作成日:2023/05/07 15:09</Text>
        </BlogDetailFooterCss>
      </Row>
    </Container>
  )
}

const BlogDetailFooterCss = styled('div', {
  padding: '2rem 1rem',
  display: 'flex',
  flexDirection: 'column',
  gap: '1rem'
})

const BlogThumbnailImageStyled = styled('div', {
  width: '100%',
  paddingBottom: '100%',
  borderRadius: '50%',
  backgroundPosition: 'center center',
  margin: '1rem 0 0'
})

const BlogTitleWrapperStyled = styled('div', {
  width: '100%',
  textAlign: 'center',
  padding: '2rem 0'
})

表示したらこんな感じ。

リファクタリング

ちょっと立ち止まってリファクタリング

Styleコンポーネントからcss propsに。

どうやらStitchesはcss propsも使えるらしい。
そっちがいいので変更。

だけどなんかStyledコンポーネントにしたほうが良さげなものと、CssPropsのほうが良さげのものがあるな。

たぶんこんな感じ。

  • Styledコンポーネント:そのタグ自体に意味がある場合
    • 例:FooterContainer, FooterContent
  • CssProps:そのタグ自体に意味がない場合
    • 例:余白を付けたいがためのStyleなど
prettierの設定

開発していてやりづらい!!
自動で自分の間違いを修正してほしい。

ということで環境作りから。

$ mkdir packages/prettier-config-custom

$ cd packages/prettier-config-custom/

$ yarn init
yarn init v1.22.19
question name (prettier-config-custom): 
question version (1.0.0): 
question description: package of prettier config files.
question entry point (index.js): 
question repository url: 
question author: 
question license (MIT): 
question private: true
success Saved package.json
Done in 32.38s.

$ cd ../..

$ yarn workspace prettier-config-custom add -D prettier

んで、ファイルも生成。こちらを参考。

zenn.dev

module.exports = {
  printWidth: 100,
  tabWidth: 2,
  useTabs: false,
  semi: false,
  singleQuote: true,
  quoteProps: "as-needed",
  jsxSingleQuote: false,
  trailingComma: "es5",
  bracketSpacing: true,
  bracketSameLine: false,
  proseWrap: "always",
  endOfLine: "lf",
  embeddedLanguageFormatting: "off",
}

じゃあこれをappsとuiに導入する。

まず諸々のinstallから。

$ yarn workspace cds add -D prettier eslint-config-prettier
$ yarn workspace ui add -D prettier eslint-config-prettier

突然だけど...

一旦ここまで!!これはWIPにします。

Dockerでpythonの環境を整えるまで【備忘録】

はじめに

表題の通りです。

pythonの環境をDockerで作るのにいろいろ準備が必要だったので、それを備忘録的にまとめました。

設定

docker周り

まずお決まりの2つのファイル

version: '3'
services:
  app:
    build:
      context: .
      args:
        - UID=${UID}
        - GID=${GID}
        - USERNAME=${USERNAME}
    container_name: 'vegetation_analysis_app'
    working_dir: /app
    tty: true
    volumes:
      - .:/app
    user: "${UID}:${GID}"
    command: /bin/bash -c "/app/startup.sh && tail -f /dev/null"
  • ${ } とすることで、envファイルの変数を読み込みます。
  • argsに入れることで、Dockerfileで指定した変数を使えます。
  • command はcontainerの起動方法に関わらず、実行したら終了するので、最後に無言実行する処理を起きます。
    • /dev/null は特別なデバイスファイルで、データを読むと何も出力されない一方、データを書き込むとそのデータは破棄されます。そのため、tail -f /dev/null を実行すると、実際には何も出力されず、コマンドは何も新しいデータが追加されることを待っています(実際には/dev/nullに新しいデータが追加されることはありません)
  • userstartup.sh を実行するために指定しています。実際にはlocalのuserと同じにしているため、localで作成したstartup.shを実行することができます。
FROM python:3

# USER setting
ARG USERNAME
ARG UID
ARG GID
RUN useradd -m -s /bin/bash -u $UID -g $GID $USERNAME

USER root

RUN apt-get update
RUN apt-get -y install locales && \
  localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9
ENV TERM xterm

RUN pip install --upgrade pip
RUN pip install --upgrade setuptools

RUN python -m pip install jupyterlab
  • 公式のimageを利用
  • ARG xx とすることでcompose.yamlで定義した変数を定義できます
  • USER設定、i18n設定、pipの整備を行いました

ipynbファイルをgitで扱うために

ipynbファイルをgitで管理しようとすると、metaデータのせいで、diffが莫大になる問題は昔からあった。

Using IPython / Jupyter Notebooks Under Version Control

解決策として、gitのコミットするたびにmetaデータを消すスクリプトを設置することらしい。

やり方はここにあります。

IPython Notebook で書いたコードをGitで管理するには?

具体的には、以下の2つのファイルを作成し、コマンドを打つだけ。

*.ipynb    filter=dropoutput_ipynb
#! /usr/bin/python

import sys
from IPython.nbformat.current import read, write

json_in = read(sys.stdin, 'json')

for sheet in json_in.worksheets:
    for cell in sheet.cells:
        if "outputs" in cell:
            cell.outputs = []
        if "prompt_number" in cell:
            cell.prompt_number = ''

write(json_in, sys.stdout, 'json')

作ったらこのコマンド

$ chmod +x ./config/ipynb_on_git/ipynb_output_filter.py

$ git config --global core.attributesfile ./config/ipynb_on_git/.gitattributes
$ git config --global filter.dropoutput_ipynb.clean ./config/ipynb_on_git/ipynb_output_filter.py 
$ git config --global filter.dropoutput_ipynb.smudge cat

VSCode拡張機能

ipynbを実行するため(?)にこの拡張機能をいれとこう。

marketplace.visualstudio.com