تبدیل فایل های IPYNB به HTML و استقرار خودکار در github-pages (بخش اول)

تو این پست قراره با نوشتن اسکریپت پایتون فایل های با فرمت ipynb رو به HTML تبدیل کنیم و به صورت خودکار با استفاده از github actions تو github pages منتشرش کنیم.

فایل های با فرمت ipynb

فایل IPYNB یک سند نوت بوکه که توسط jupyter notebook ایجاد شده، یک محیط محاسباتی تعاملی که به ما تو دستکاری و تجزیه و تحلیل داده ها با استفاده از پایتون کمک می کنه.

اکشن های github

به کمک ویژگی github actions خیلی از کارا رو مثل تست کد یا دیپلوی کردن کد جدید یا تو مورد ما، تبدیل فایل ها و انتقالشون به github pages رو به صورت خودکار میشه انجام داد.


اسکریپتی که قراره بنویسیم رو میتونین از مخزن اصلی گیت هاب ایرانی پای بردارید.


ایمپورت ماژول های استفاده شده

import os
import pathlib
import shutil
import sys
from concurrent.futures import ThreadPoolExecutor

تو این قسمت ماژول های مورد نیاز رو ایمپورت میکنیم. همشون از لایبرری های استاندارد و built-in پایتون هستن، پس نیازی به نصب چیزی نیست.

تعریف متغیر های گلوبال

_DOCS_PATH = 'docs'
_PROJECTS_PATH = 'projects'
_IPYNB_GLOB = '**/*.ipynb'
_GIT_MAIN_BRANCH_NAME = 'main'
_GIT_EMAIL = 'iranipy@users.noreply.github.com'
_GIT_USERNAME = 'iranipy'
_GIT_COMMIT_MESSAGE = 'new rendered docs added by ipynb_docs_renderer action'
_CONVERTED_FILE_FORMAT = 'html'
_CONVERT_COMMAND = f'jupyter nbconvert --to {_CONVERTED_FILE_FORMAT}'
_DEPENDENCIES = ['jupyterlab', 'nbconvert']

اینا متغیر هایی هستن که ادامه کار بهشون نیاز داریم و مقدارشون تغییری نمیکنه. مقادیر رو به خواست خودتون تغییر بدین، مثلا مقدار GIT_EMAIL_ رو به ایمیل حساب خودتون تغییر بدین. دلیل استفاده از حروف بزرگ و آندرلاین قبل اسمشون اینه که میخوایم نشون بدیم ثابت (constant) هستن و خصوصی (private) اما از اونجایی که تو پایتون این ویژگی ها به صورت داخلی و تو سطح مفسر (interpreter) وجود نداره، این موضوع فقط به صورت قرار دادیه؛ یعنی شما هرجای کد که بخواید میتونید مقدارشون رو عوض یا تو هر ماژولی ایمپورتشون کنین. ولی خب نکنین :)

ماژول های جدیدی که تو این بخش به عنوان وابستگی (dependency) دیده میشه nbconvert و jupyterlab هستن که نیازی به نصبشون نیست و در ادامه بهشون میرسیم.

نصب پیش نیاز ها روی ماشین

def _install_dependencies():
    with ThreadPoolExecutor() as executer:
        executer.map(os.system, [f'pip install {d}' for d in _DEPENDENCIES])

وظیفه این تابع نصب پیش نیاز های برنامه (اعضای لیست DEPENDENCIES_) تو محیط اجرای برنامه ست.

از کلاس ThreadPoolExecutor استفاده میکنیم. متغیر executer یک instance از ThreadPoolExecutor حساب میشه. (آشنایی بیشتر با context manager)

تو خط سوم از یکی از متد های ThreadPoolExecutor به اسم map استفاده کردیم. عملکرد این متد دقیقا مثل تابع map خود پایتونه. یعنی یک تابع و یک iterable به عنوان پارامتر میگیره و اون تابع رو روی تک تک اعضای اون iterable اجرا میکنه ولی فرقش اینه که هر عضو اون iterable رو تو یک رشته (Thread) جداگونه اجرا می کنه. تابعی که اینجا استفاده شده system از کتابخونه os هستش. این تابع دستوری که به شکل رشته (string) به عنوان یک پارامتر بهش میدید رو تو یک subshell اجرا میکنه.

تبدیل ipynb به HTML و انتقال به GitHub Pages

def _render_ipynb(ipynb: list):
    directory, file = os.path.split(ipynb)
    rendered_file = f'{os.path.splitext(file)[0]}.{_CONVERTED_FILE_FORMAT}'
    rendered_file_path = os.path.join(directory, rendered_file)
    os.system(f'{_CONVERT_COMMAND} {ipynb.absolute()}')
    shutil.move(rendered_file_path, os.path.join(_DOCS_PATH, rendered_file))

def _run_rendering():
    IPYNB_DOCS = list(pathlib.Path(_PROJECTS_PATH).glob(_IPYNB_GLOB))
    with ThreadPoolExecutor() as executer:
        executer.map(_render_ipynb, IPYNB_DOCS)

تو تابع render_ipynb_ یک ابجکت pathlib.Path به عنوان پارامتر دریافت میشه و عملیات تبدیل فرمت و انتقالش به دایرکتوری مورد نظر (معادل با مقدار DOCS_PATH_) انجام میشه.

  1. تو خط اول با استفاده از os.path.split مسیر فایل و اسم فایل رو به ترتیب به متغیرهای directory و file نسبت دادیم.
  2. تو خط دوم اسم فایل نهایی رو مشخص کردیم. این اسم متشکل از اسم فایلی که در حال حاضر روش کار میکنیم و پسوند html (معادل با مقدار CONVERTED_FILE_FORMAT_ ) هستش.
  3. تو خط سوم مسیر نهایی فایل HTML مورد نظر رو با os.path.join میسازیم. دلیل استفاده از این متد به جای هارد کد کردن اینه که این متد با توجه به سیستم عامل از separator مناسب استفاده میکنه و از ارور جلوگیری میکنه.
  4. تو خط چهارم از تابع system استفاده شده که تو بخش های قبلی عملکردش توضیح داده شد، که اینجا داره دستور معادل با مقدار CONVERT_COMMAND_ رو با یک پارامتر با مقدار حاصل از فراخوانی ipynb.absolute اجرا میکنه. این متد مسیر مطلق فایل رو بر میگردونه.
  5. خط آخر فایل تولید شده خط بالایی رو از مسیر فعلیش برمیداره و به دایرکتوری مقصد (که اینجا docs در نظر گرفته شده) منتقل میکنه. این متد بدون نوشتن اسم فایل هم کار میکنه، ینی اگه فقط اسم دایرکتوری رو مینوشیتم هم درست بود ولی نکته منفی ای که ننوشتن اسم فایل مقصد داره اینه که اگه فایلی با اسم مشابه فایل مبدا وجود داشته باشه به جای بازنویسی کردنش ارور میده.
  6. تابع run_rendering_ دوتا وظیفه داره. اول درست کردن لیستی از تمام فایل های با فرمت ipynb توی دایرکتوری مورد نظر (که اینجا projects در نظر گرفته شده) و تبدیل اونا به pathlib.Path و بعد اجرای تابع render_ipynb_ روی اعضای لیست به صورت همزمان.

پوش کردن تغییرات صورت گرفته روی مخزن (repository) مورد نظر

def _commit_push():
    commands = [
        f'git config --global user.email &quot{_GIT_EMAIL}&quot && git config --global user.name 
&quot{_GIT_USERNAME}&quot',
        'git add .',    f'git commit -m &quot{_GIT_COMMIT_MESSAGE}&quot',
        f'git fetch origin {_GIT_MAIN_BRANCH_NAME}',
        f'git push origin {_GIT_MAIN_BRANCH_NAME}',
     ]
    list(map(os.system, commands))

تو این تابع تمام دستوراتی که برای کامیت و پوش کردن لازم هست رو یک لیست قرار دادیم و با استفاده از map تابع os.system روی اعضای لیست اجرا کردیم. دلیل استفاده از تابع list این هستش که اگه این کارو انجام ندیم تنها اتفاقی که میوفته اینه که تابع map یک map object بهمون میده و هیچکدوم از دستورات رو اجرا نمیکنه. به عبارت دیگه برای اجرا این دستورات باید روی map object، یک iterate بزنیم (به هرشکلی، مهم نیست).

رابط خط فرمان (CLI)

def main():
    task = {
        'install-dependencies': _install_dependencies,
        'render': _run_rendering,
        'commit-push': _commit_push,
    }[sys.argv[1]]
    task()

تو این تابع یک دیکشنری داریم که تو این دیکشنری به هر کدوم از تابع هایی که بالاتر تعریف کردیم یک کلیدواژه اختصاص داده شده. هر کدوم از این کلیدواژه ها که موقع اجرای ماژول به عنوان آرگومان خط فرمان وارد بشه، برنامه همون تابع رو اجرا میکنه؛ در واقع روشیه برای شبیه سازی عبارت swicth-case موجود تو سایر زبان های برنامه نویسی. (sys.argv)

به عنوان مثال، اینجوری میتونیم تابع run_rendering_ رو اجرا کنیم:

python ipynb_docs_renderer.py render

اجرای تابع اصلی

if __name__ == '__main__':
    main()

توضیحات بیشتر در مورد __main__


پایان بخش اول ...

امیدواریم از اولین بخش این پست استفاده کافی رو برده باشید. اگه شما هم مثل ما از خوندن مطالب پایتونی لذت میبرید، خوشحال میشیم ما رو تو شبکه های اجتماعی دنبال و در صورت داشتن انتقاد و پیشنهاد ما رو ازش مطلع کنید. لایک هم یادتون نره :)

برای خوندن بخش دوم کلیک کنید.


حمایت از ما

با دنبال کردن ما در

یا کمک مالی به مبلغ دلخواه از طریق درگاه آیدی پی میتونید از ما حمایت کنید.

وب سایت (به زودی): iranipy.ir