Skip to content

Commit 929fb46

Browse files
committed
feat: health check, install binaries, font etc.
1 parent 1fa603e commit 929fb46

File tree

10 files changed

+160
-37
lines changed

10 files changed

+160
-37
lines changed

.github/workflows/tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ jobs:
4242
# python -m pip install --upgrade pip
4343
uv pip install -r deps/requirements_dev.txt
4444
uv pip install -e .
45+
bash scripts/install_binaries.sh
4546
- name: Run pytest
4647
run: |
4748
set +e # Do not exit shell on pytest failure
@@ -107,6 +108,7 @@ jobs:
107108
# python -m pip install --upgrade pip
108109
uv pip install -r deps/requirements_dev.txt
109110
uv pip install -e .
111+
bash scripts/install_binaries.sh
110112
- name: Run doctest
111113
run: |
112114
set +e # Do not exit shell on pytest failure

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222
## 돌려 보기
2323

2424
1. (Optional) `pip3 install --user uv` 해서 pip 대신 `uv pip` 사용하면 더 빠름.
25-
2. `uv pip install -r deps/requirements.txt`, `uv pip install -e .`으로 dependencies 및 mlproject 패키지 설치
25+
2. `uv pip install -r deps/requirements.txt`, `uv pip install -e .`, `bash scripts/install_binaries.sh` 으로 dependencies 및 mlproject 패키지 설치
2626
3. template.env 파일을 .env로 복사한 후 token 등 내용 수정.
27-
4. `python tools/examples/color_logging_main.py` 실행해보기. 로깅 내용은 `data/logs` 폴더 안에 기록됨.
28-
5. `uv pip install -r deps/requirements_dev.txt` 으로 pytest 등 개발자용 패키지도 설치가능
29-
6. `pytest` 커맨드로 테스트 실행해보기.
27+
4. `python -m mlproject.health` 실행해서 환경 설정이 잘 되었는지 확인.
28+
5. `python tools/examples/color_logging_main.py` 실행해보기. 로깅 내용은 `data/logs` 폴더 안에 기록됨.
29+
6. `uv pip install -r deps/requirements_dev.txt` 으로 pytest 등 개발자용 패키지도 설치가능
30+
7. `pytest` 커맨드로 테스트 실행해보기.
3031
- `python scripts/run_doctest.py` 커맨드로 doctest 실행해보기.
31-
7. `import mlproject; print(mlproject.__version__)` 해보면 `0.1.0+4.g75bbed7.dirty` 이런식으로 나옴.
32+
8. `import mlproject; print(mlproject.__version__)` 해보면 `0.1.0+4.g75bbed7.dirty` 이런식으로 나옴.
3233
- 0.1.0 버전 이후 4개의 커밋이란 뜻. 그리고 커밋되지 않은 수정사항이 있는 상태이면 dirty버전임.
3334

3435
## 파일 설명

scripts/health.py

Lines changed: 0 additions & 8 deletions
This file was deleted.

scripts/install_binaries.sh

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env bash
2+
3+
if [[ -n "$CONDA_PREFIX" ]]; then
4+
PREFIX=$CONDA_PREFIX
5+
else
6+
echo "Please activate a conda environment"
7+
exit 1
8+
fi
9+
10+
if command -v mamba &> /dev/null; then
11+
CONDA=mamba
12+
else
13+
CONDA=conda
14+
fi
15+
16+
echo "Installing binaries to $PREFIX"
17+
TEMPDIR=$(mktemp -d)
18+
19+
# Shared objects needed for the cairosvg package
20+
$CONDA install -y -c conda-forge cairo
21+
22+
# Install FiraCode font (for rich export_svg -> cairosvg PDF generation)
23+
if [[ "$OSTYPE" == "darwin"* ]]; then
24+
brew tap homebrew/cask-fonts
25+
brew install font-fira-code
26+
else
27+
FONTDIR="$HOME/.local/share/fonts"
28+
mkdir -p "$FONTDIR"
29+
30+
if [ ! command -v fc-list ] &>/dev/null || ! fc-list | grep -q "FiraCode"; then
31+
echo "FiraCode could not be found. Installing on $FONTDIR"
32+
# NOTE: we need non-NF FiraCode for python rich export_svg -> cairosvg PDF generation
33+
mkdir -p "$TEMPDIR/firacode"
34+
curl -s https://api.github.com/repos/tonsky/FiraCode/releases/latest |
35+
grep "browser_download_url.*.zip" |
36+
cut -d : -f 2,3 |
37+
tr -d \" |
38+
wget -qi - -O "$TEMPDIR/firacode.zip"
39+
unzip "$TEMPDIR/firacode.zip" -d "$TEMPDIR"/firacode
40+
mv "$TEMPDIR"/firacode/ttf/*.ttf "$FONTDIR"
41+
42+
fc-cache -fv
43+
else
44+
echo "FiraCode is already installed"
45+
fi
46+
fi
47+
48+
rm -rf "$TEMPDIR"

src/mlproject/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import os
22
from pathlib import Path
33

4+
from dotenv import load_dotenv
5+
46
from . import _version
57

8+
load_dotenv()
9+
610
__version__ = _version.get_versions()["version"]
711

812
default_log_level = os.environ.get("MLPROJECT_LOG_LEVEL")

src/mlproject/health/__init__.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
"""Check health of the installation."""
22

3+
import logging
34
import os
45

5-
from rich import print
6-
from rich.console import Console
7-
86
from .. import DATA_DIR
7+
from .font import verify_fonts_bool
98
from .slack import check_env as slack_check_env
109
from .slack import check_send_text as slack_check_send_text
1110

12-
console = Console()
11+
logger = logging.getLogger(__name__)
1312

1413

1514
def check_binaries():
@@ -19,16 +18,16 @@ def check_binaries():
1918

2019
def check_env():
2120
"""Check environment variables."""
22-
data_dir = os.environ.get("MLPROJECT_DATA_DIR")
21+
ppmi_data_dir = os.environ.get("MLPROJECT_DATA_DIR")
2322

24-
if data_dir is None or data_dir == "":
25-
print(
26-
"🤢 Please set the environment variable MLPROJECT_DATA_DIR to the path of the data directory."
23+
if ppmi_data_dir is None:
24+
logger.warning(
25+
"🤒 Please set the environment variable MLPROJECT_DATA_DIR to the path of the data directory.\n"
26+
f"Otherwise, the default {DATA_DIR} will be used."
2727
)
28-
print(f"Otherwise, the default {DATA_DIR} will be used.")
2928
return False
3029

31-
print(f"✅ MLPROJECT_DATA_DIR is set to {data_dir}")
30+
logger.info(f"✅ MLPROJECT_DATA_DIR is set to {ppmi_data_dir}")
3231
return True
3332

3433

@@ -39,9 +38,11 @@ def main():
3938
successes.append(slack_check_env())
4039
successes.append(slack_check_send_text())
4140

41+
successes.append(verify_fonts_bool())
42+
4243
if all(successes):
43-
print()
44-
print("💪 You are ready to go!")
44+
logger.info("")
45+
logger.info("💪 You are ready to go!")
4546

4647

4748
if __name__ == "__main__":

src/mlproject/health/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from ..utils import setup_logging
2+
from . import main
3+
4+
if __name__ == "__main__":
5+
setup_logging(output_files=[], file_levels=[])
6+
main()

src/mlproject/health/font.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import logging
2+
import subprocess
3+
4+
logger = logging.getLogger(__name__)
5+
6+
7+
class FontNotInstalledError(Exception):
8+
pass
9+
10+
11+
def verify_fonts_installed():
12+
"""
13+
This function verifies that the FiraCode font is installed because Rich Console.export_svg() uses it.
14+
15+
Actually, the svg itself doesn't need the font but when we convert it to PDF using cairosvg, the font is needed.
16+
"""
17+
cmd = ["fc-match", "FiraCode:style=Regular"]
18+
output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
19+
20+
if not output.decode("utf-8").startswith("FiraCode-Regular"):
21+
raise FontNotInstalledError("FiraCode")
22+
23+
cmd = ["fc-match", "FiraCode:style=Bold"]
24+
output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
25+
26+
if not output.decode("utf-8").startswith("FiraCode-Bold"):
27+
raise FontNotInstalledError("FiraCode")
28+
29+
# log.debug('Courier and Helvetica Neue were found to be installed.')
30+
31+
32+
def verify_fonts_bool():
33+
try:
34+
verify_fonts_installed()
35+
logger.info("✅ FiraCode font is installed.")
36+
return True
37+
except FontNotInstalledError:
38+
logger.error("😡 FiraCode font is not installed.")
39+
return False
40+
41+
42+
if __name__ == "__main__":
43+
verify_fonts_installed()

src/mlproject/health/slack.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,55 @@
1-
# ruff: noqa: T201
1+
import logging
22
import os
33

44
from ..utils.slack.send_only import send_text
55

6+
logger = logging.getLogger(__name__)
67

7-
def check_one_env(env_var):
8+
9+
def check_one_env(env_var, secret=False, secret_show_first_n=0):
810
value = os.environ.get(env_var)
911
if value is None:
10-
print(f"🤢 Please set the environment variable {env_var}.")
12+
logger.error(f"😡 Please set the environment variable {env_var}.")
1113
return False
12-
print(f"✅ {env_var} is set to {value}")
14+
15+
if secret:
16+
if secret_show_first_n > 0:
17+
logger.info(
18+
f"✅ {env_var} is set to {value[:secret_show_first_n]}{'*' * (len(value) - secret_show_first_n)}"
19+
)
20+
else:
21+
logger.info(f"✅ {env_var} is set.")
22+
else:
23+
logger.info(f"✅ {env_var} is set to {value}")
1324
return True
1425

1526

16-
def check_many_env(env_vars):
27+
def check_many_env(env_vars, secret=False, secret_show_first_n=0):
1728
ret = True
1829
for env_var in env_vars:
19-
if not check_one_env(env_var):
30+
if not check_one_env(env_var, secret, secret_show_first_n):
2031
ret = False
2132
return ret
2233

2334

2435
def check_env():
25-
return check_many_env(["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN", "SLACK_CHANNEL_ID"])
36+
secrets_checked = check_many_env(["SLACK_BOT_TOKEN", "SLACK_APP_TOKEN"], True, 5)
37+
normal_checked = check_many_env(["SLACK_CHANNEL_ID"])
38+
39+
return secrets_checked and normal_checked
2640

2741

2842
def check_send_text():
29-
print("🚀 Sending a test message to the Slack channel...")
43+
logger.info("🚀 Sending a test message to the Slack channel...")
3044
response = send_text("💪 Checking health of the Slack bot.")
3145

3246
if response is None:
33-
print("🤢 The response is None.")
47+
logger.error("😡 The response is None.")
3448
return False
3549

3650
if response["ok"]:
37-
print("✅ The message was sent successfully.")
51+
logger.info("✅ The message was sent successfully.")
3852
return True
3953

40-
print(f"🤢 The message was not sent successfully. Response: {response}")
54+
logger.error(f"😡 The message was not sent successfully. Response: {response}")
4155
return False

src/mlproject/utils/slack/send_only.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,24 @@ def send_svg_as_pdf(
233233
channel_id = default_channel_id
234234
assert channel_id is not None
235235

236+
# svg_buf = BytesIO()
236237
pdf_buf = BytesIO()
237238

238239
if isinstance(svg_file, str | bytes):
239240
cairosvg.svg2pdf(bytestring=svg_file, write_to=pdf_buf)
241+
# raise NotImplementedError("cairosvg does not support bytestring")
242+
# str to bytes
243+
# if isinstance(svg_file, str):
244+
# svg_file = svg_file.encode("utf-8")
245+
# svg_buf.write(svg_file)
246+
# svg_buf.seek(0)
247+
# drawing = svg2rlg(svg_buf)
248+
# renderPDF.drawToFile(drawing, pdf_buf)
249+
240250
elif isinstance(svg_file, IOBase):
241-
cairosvg.svg2pdf(file_obj=svg_file, write_to=pdf_buf)
251+
# cairosvg.svg2pdf(file_obj=svg_file, write_to=pdf_buf)
252+
drawing = svg2rlg(svg_file)
253+
renderPDF.drawToFile(drawing, pdf_buf)
242254
else:
243255
raise ValueError(f"Unsupported type {type(svg_file)}")
244256

0 commit comments

Comments
 (0)