前回、prisma-client-pythonに入門しました。
しかし、どうやってNodeが動いているのかちょっとわからなかったので調べてみました。
Nodeの実態は?
現状、グローバルにprismaが使える状態ではないため、普通にprismaコマンドを実行しても実行できません。
% prisma
zsh: command not found: prisma
しかし、前回作ったパッケージ上であればprismaコマンドは実行できます。
% poetry run prisma
This command is only intended to be invoked internally. Please run the following instead:
prisma <command>
e.g.
prisma generate
説明を見るかぎり、ここで実行されているprismaは本家のCLIのラッパーとして実装されているようです。
Prisma Client Python exposes a CLI interface which wraps the Prisma CLI. This works by downloading a Node binary, if you don’t already have Node installed on your machine, installing the CLI with npm and running the CLI using Node.
和訳:Prisma Client Python は、Prisma CLI をラップする CLI インターフェイスを公開します。これは、マシンにまだ Node がインストールされていない場合は、Node バイナリをダウンロードし、npm を使用して CLI をインストールし、Node を使用して CLI を実行することで機能します。
既にNodeはインストールされているため、Nodeバイナリのインストールはされていないようですが、 グローバルにprisma CLIが実行できないので、どこからこのprismaが呼び出されているのかがよく分かりません。
とりあえず実態がどこにあるのか探しましょう。
% poetry run which prisma
/home/username/Project/study/prisma-client-python-playground/.venv/bin/prisma
どうやら仮想パッケージの依存関係の中にいるようです。
このバイナリはコードのどこから呼び出されているかは、リポジトリのsetup.pyを見たら分かりました。
#...
entry_points={
'console_scripts': [
'prisma=prisma.cli:main', # Prisma CLIの呼び出しはこれ
'prisma-client-py=prisma.cli:main',
],
'prisma': [],
},
#...
じゃあこのprisma.cli:main
はどこか調べたら、ここにありました。
prisma-client-py/src/prisma/cli/cli.py at main · RobertCraigie/prisma-client-py · GitHub
def main(
args: Optional[List[str]] = None,
use_handler: bool = True,
do_cleanup: bool = True,
) -> NoReturn:
if args is None:
args = sys.argv
with setup_logging(use_handler), cleanup(do_cleanup):
if len(args) > 1:
if args[1] == 'py':
cli.main(args[2:], prog_name='prisma py')
else:
sys.exit(prisma.run(args[1:]))
else:
if not os.environ.get('PRISMA_GENERATOR_INVOCATION'):
error(
'This command is only intended to be invoked internally. '
'Please run the following instead:',
exit_=False,
)
click.echo('prisma <command>')
click.echo('e.g.')
click.echo('prisma generate')
sys.exit(1)
Generator.invoke()
# mypy does not recognise sys.exit as a NoReturn for some reason
raise SystemExit(0)
else
の中には、先程サブコマンドを打たずに実行した際の出力内容がそのまま書かれていますね。
% poetry run prisma
This command is only intended to be invoked internally. Please run the following instead:
prisma <command>
e.g.
prisma generate
最初の分岐の次も分岐していて、サブコマンドでpy
コマンドが使われているかどうかで分岐していますね。
py
コマンドではない場合、prismaモジュールを実行しています。
prisma-client-python特有のコマンド
ここでサラッと出てきたpy
サブコマンドについても見ていきましょう。
Command Line - Prisma Client Python
2023-08-15現時点で以下の3つのコマンドがあります。
prisma py generate
schema.prisma
ファイルを変えずに設定を変えてgenerateする際に実行する
prisma py version
- prisma-client-pythonが使用する依存関係の各バージョンなどを表示
prisma py fetch
- バイナリのダウンロードを行う
py fecth
だけ分かりづらいんですが、これは依存して使用しているNodeとRustのバイナリを指定する際に利用するコマンドです。(この辺は自分もふわっと理解しているくらいです。。)
Binaries - Prisma Client Python
prismaモジュールの実態
少し話が逸れましたが、py
サブコマンド以外はprismaモジュールから実行しているというところまで確認しました。
prismaモジュールの中身を確認しましょう。
(気になる部分にコメントを入れています。)
from __future__ import annotations
import os
import sys
import json
import logging
import subprocess
from pathlib import Path
from typing import Any, List, Optional, Dict, NamedTuple
import click
from ._node import node, npm # nodeの実態?
from .. import config
from ..errors import PrismaError
log: logging.Logger = logging.getLogger(__name__)
DEFAULT_PACKAGE_JSON: dict[str, Any] = {
'name': 'prisma-binaries',
'version': '1.0.0',
'private': True,
'description': 'Cache directory created by Prisma Client Python to store Prisma Engines',
'main': 'node_modules/prisma/build/index.js',
'author': 'RobertCraigie',
'license': 'Apache-2.0',
}
def run(
args: List[str],
check: bool = False,
env: Optional[Dict[str, str]] = None,
) -> int:
log.debug('Running prisma command with args: %s', args)
# 環境変数を設定
default_env = {
**os.environ,
'PRISMA_HIDE_UPDATE_MESSAGE': 'true',
'PRISMA_CLI_QUERY_ENGINE_TYPE': 'binary',
}
env = {**default_env, **env} if env is not None else default_env
# TODO: ensure graceful termination
# nodeのエントリーポイントとして使用するjsを生成
entrypoint = ensure_cached().entrypoint
# nodeによるコマンドの実行
process = node.run(
str(entrypoint),
*args,
env=env,
check=check,
stdout=sys.stdout,
stderr=sys.stderr,
)
if args and args[0] in {'--help', '-h'}:
click.echo(click.style('Python Commands\n', bold=True))
click.echo(
' '
+ 'For Prisma Client Python commands run '
+ click.style('prisma py --help', bold=True)
)
return process.returncode
class CLICache(NamedTuple):
cache_dir: Path
entrypoint: Path
# キャッシュディレクトリ内のpackage.jsonを定義
DEFAULT_PACKAGE_JSON: dict[str, Any] = {
'name': 'prisma-binaries',
'version': '1.0.0',
'private': True,
'description': 'Cache directory created by Prisma Client Python to store Prisma Engines',
'main': 'node_modules/prisma/build/index.js',
'author': 'RobertCraigie',
'license': 'Apache-2.0',
}
def ensure_cached() -> CLICache:
# 設定からキャッシュディレクトリを取得(pathlib.Path型)
cache_dir = config.binary_cache_dir
# キャッシュディレクトリ内のprismaからエントリーポイントとなるjsファイルを指定
entrypoint = cache_dir / 'node_modules' / 'prisma' / 'build' / 'index.js'
if not cache_dir.exists():
cache_dir.mkdir(parents=True)
# npm`が他の場所を探さないように、ダミーの `package.json` ファイルを作成する必要がある。
#
# もし別の `package.json` ファイルが見つかったら、キャッシュディレクトリではなく、そこに `prisma` パッケージがインストールされる。
package = cache_dir / 'package.json'
if not package.exists():
package.write_text(json.dumps(DEFAULT_PACKAGE_JSON))
# エントリーポイントが存在しない場合、Prisma CLIをbinary_cache_dirにインストールする
if not entrypoint.exists():
click.echo('Installing Prisma CLI')
try:
proc = npm.run(
'install',
f'prisma@{config.prisma_version}',
cwd=config.binary_cache_dir,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if proc.returncode != 0:
click.echo(
f'An error ocurred while installing the Prisma CLI; npm install log: {proc.stdout.decode("utf-8")}'
)
proc.check_returncode()
except Exception:
# npm install` を実行すべきかどうかをチェックするために、既存のエントリーポイントを使っているので、もし `npm install` の実行が失敗したら、それが存在しないことを確認する必要がある。
# そうしないと壊れてしまう。
# https://github.com/RobertCraigie/prisma-client-py/issues/705
if entrypoint.exists():
try:
entrypoint.unlink()
except Exception:
pass
raise
# 上記の処理を行った上でなおエントリーポイントが存在しない場合のエラー
if not entrypoint.exists():
raise PrismaError(
f'CLI installation appeared to complete but the expected entrypoint ({entrypoint}) could not be found.'
)
return CLICache(cache_dir=cache_dir, entrypoint=entrypoint)
つまり、キャッシュディレクトリにprisma-client-pythonで使用する依存関係があり、そこのprismaのエントリーポイントjsを使用してnodeを実行しているということになります。
補足:pathlib.Path
を使うと/
記号を特殊な使い方ができます。
import pathlib
a = pathlib.Path("a")
a / "b" / "c" # PosixPath('a/b/c')
キャッシュディレクトリは、実装を見るとデフォルトでホームディレクトリから決まった位置にあるようです。
class Config(DefaultConfig):
binary_cache_dir: Path = Field(env='PRISMA_BINARY_CACHE_DIR')
@classmethod
def from_base(cls, config: DefaultConfig) -> Config:
if config.binary_cache_dir is None:
config.binary_cache_dir = (
config.home_dir
/ '.cache'
/ 'prisma-python'
/ 'binaries'
/ config.prisma_version
/ config.expected_engine_version
)
return cls.parse_obj(config.dict())
見てみると、確かにありました。
構成は以下のようになっています。
% tree -a -L 2
.
├── node_modules
│ ├── .bin
│ ├── .package-lock.json
│ ├── @prisma
│ └── prisma
├── package-lock.json
└── package.json
prisma-client-jsとprisma-client-pythonの併用はどうする?
前回のように、prisma-dbml-generatorを使用したいケースに対応するには、キャッシュディレクトリ内にある依存関係に追加する必要があります。
ただデフォルト設定のままだと、隠しディレクトリに作成するようになっており、Dockerなどで持ち回るときに不便です。
そこで、独自のgeneratorなどを追加する際はキャッシュディレクトリ設定を変更しましょう。
前回から引き続きprisma-client-python-playground
を使用します。
キャッシュディレクトリを指定するには、pyproject.toml
に以下の設定を追加します。
[tool.prisma]
binary_cache_dir = '.'
今回はpyproject.toml
を配置したプロジェクトのルートディレクトリを指定しています。
package.json
はこのようになっています。
prisma-dbml-generatorの依存性は既に追加済みです。
{
"name": "my-prisma-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"prisma": "^4.15.0",
"prisma-dbml-generator": "^0.10.0"
},
"dependencies": {
"@prisma/client": "^4.15.0"
}
}
schema.prisma
もprisma-dbml-generatorが走るようにしましょう。
generator dbml {
provider = "prisma-dbml-generator"
}
この状態でprisma-client-pythonからprismaコマンドを実行します。
% poetry run prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Error: Generator "prisma-dbml-generator" failed:
/bin/sh: 1: prisma-dbml-ge
/ '.cache'
/ 'prisma-python'
/ 'binaries'nerator: not found
くそー!!エラーか!!!
直接環境変数を指定してもだめでした。
% PRISMA_BINARY_CACHE_DIR=$(pwd) poetry run prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Error: Generator "prisma-dbml-generator" failed:
/bin/sh: 1: prisma-dbml-generator: not found
デバッグモードで試してみると、キャッシュディレクトリとしてはちゃんとこのプロジェクトのディレクトリが認識されているみたいです。
% PRISMA_BINARY_CACHE_DIR=$(pwd) PRISMA_PY_DEBUG=1 poetry run prisma generate
[DEBUG ] prisma.cli.prisma: Running prisma command with args: ['generate']
[DEBUG ] prisma.cli._node: Checking if nodejs-bin is installed
[DEBUG ] prisma.cli._node: Checking for global target binary: node
[DEBUG ] prisma.cli._node: Found global binary at: /home/username/.anyenv/envs/nodenv/shims/node
[DEBUG ] prisma.cli._node: node version check exited with code 0
[DEBUG ] prisma.cli._node: node version check output: v17.9.0
[DEBUG ] prisma.cli._node: node version check returning (17, 9)
[DEBUG ] prisma.cli._node: Using global node binary at /home/username/.anyenv/envs/nodenv/shims/node
[DEBUG ] prisma.cli._node: Attempting to preprend /home/username/.anyenv/envs/nodenv/shims to the PATH
[DEBUG ] prisma.cli._node: Using PATH environment variable: /home/username/.anyenv/envs/nodenv/shims:/home/username/Project/study/prisma-client-python-playground/.venv/bin:/home/username/.anyenv/envs/pyenv/versions/3.8.12/bin:/home/username/.anyenv/envs/pyenv/libexec:/home/username/.anyenv/envs/pyenv/plugins/python-build/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/username/.gcloud/google-cloud-sdk/bin:/home/username/.odrive-agent/bin:/home/username/.poetry/bin:/home/username/.anyenv/envs/rbenv/shims:/home/username/.anyenv/envs/rbenv/bin:/home/username/.anyenv/envs/rbenv/bin:/home/username/.anyenv/envs/pyenv/shims:/home/username/.anyenv/envs/pyenv/bin:/home/username/.anyenv/envs/nodenv/shims:/home/username/.anyenv/envs/nodenv/bin:/home/username/.anyenv/envs/goenv/bin:/home/username/.anyenv/bin:/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:/home/username/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin:/home/username/.anyenv/envs/goenv/shims
[DEBUG ] prisma.cli._node: Executing binary at /home/username/.anyenv/envs/nodenv/shims/node with args: ('/home/username/Project/study/prisma-client-python-playground/node_modules/prisma/build/index.js', 'generate')
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
prisma:GeneratorProcess /bin/sh: 1: prisma-dbml-generator: not found +0ms
prisma:GeneratorProcess child exited with code 127 on signal null +2ms
Error: Generator "prisma-dbml-generator" failed:
/bin/sh: 1: prisma-dbml-generator: not found
…まぁ、あくまでprsma-dbml-generator
なんかはサードパーティのジェネレーターなわけで、公式にサポートしてるものじゃないですからね。。
ここで生成できなくても仕方ないか…
まとめ
prisma-client-pythonがだいぶ理解できました。
ただ、一部のジェネレーターが含まれているとビルドできなくなると言った点が残念でした。。
しかしまぁ一応、やり方次第で如何様にもできるのでこの辺はそう日間的にならなくてもいいかもしれませんね。