diff --git a/.gitignore b/.gitignore index c2d7493..c3751c1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,3 @@ # vscode .history/ .vscode/ - - -# python cache -__pycache__/ - - -# mypy cache -.mypy_cache/ \ No newline at end of file diff --git a/Flow.Launcher.Plugin.PythonTemplate/.buildignore b/Flow.Launcher.Plugin.PythonTemplate/.buildignore new file mode 100644 index 0000000..8daa537 --- /dev/null +++ b/Flow.Launcher.Plugin.PythonTemplate/.buildignore @@ -0,0 +1,12 @@ +# folder +.git/* +.github/* +.vscode/* +.history/* +*/__pycache__/* +build/* + +# file +.gitignore +.gitattributes +.buildignore diff --git a/Flow.Launcher.Plugin.PythonTemplate/.github/workflows/release.yml b/Flow.Launcher.Plugin.PythonTemplate/.github/workflows/release.yml new file mode 100644 index 0000000..7cbcaeb --- /dev/null +++ b/Flow.Launcher.Plugin.PythonTemplate/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: Build and Publish + +on: + push: + tags: + - "v*" + +jobs: + build-publish: + name: Build and Publish + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: "3.x" + + - name: Set up Environment + run: | + python -m pip install --upgrade pip + python commands.py setup-env + + - name: Install Dependencies to Local + run: python commands.py setup-env-to-lib + + - name: Compile all languages + run: python commands.py compile + + - name: Update 'plugin.json' information + run: python commands.py gen-plugin-info + + - name: Remove Python file artifacts + run: python commands.py clean-pyc + + - name: Pack plugin to a zip file + run: python commands.py build + + - name: Create GitHub Release + id: create_release + uses: actions/create-release@v1 + env: + # This token is provided by Actions + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + draft: false + prerelease: false + + - name: Get Asset name + run: | + export PKG=$(ls build/ | grep zip) + set -- $PKG + echo "name=$1" >> $GITHUB_ENV + + - name: Upload Release (zip) to GitHub + id: upload-release-asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: build/${{ env.name }} + asset_name: ${{ env.name }} + asset_content_type: application/zip diff --git a/Flow.Launcher.Plugin.PythonTemplate/.gitignore b/Flow.Launcher.Plugin.PythonTemplate/.gitignore index e69de29..a28b021 100644 --- a/Flow.Launcher.Plugin.PythonTemplate/.gitignore +++ b/Flow.Launcher.Plugin.PythonTemplate/.gitignore @@ -0,0 +1,7 @@ +# build +lib/ +build/ + +# python cache +__pycache__/ +.mypy_cache/ diff --git a/Flow.Launcher.Plugin.PythonTemplate/commands.py b/Flow.Launcher.Plugin.PythonTemplate/commands.py index 5432812..73b98f8 100644 --- a/Flow.Launcher.Plugin.PythonTemplate/commands.py +++ b/Flow.Launcher.Plugin.PythonTemplate/commands.py @@ -2,13 +2,94 @@ import json import os +from textwrap import dedent +from typing import List import click -from plugin import (ICON_PATH, PLUGIN_ACTION_KEYWORD, PLUGIN_AUTHOR, - PLUGIN_EXECUTE_FILENAME, PLUGIN_ID, PLUGIN_PROGRAM_LANG, - PLUGIN_URL, __long_description__, __package_name__, - __short_description__, __version__, basedir) +from plugin import ( + ICON_PATH, + PLUGIN_ACTION_KEYWORD, + PLUGIN_AUTHOR, + PLUGIN_EXECUTE_FILENAME, + PLUGIN_ID, + PLUGIN_PROGRAM_LANG, + PLUGIN_URL, + PLUGIN_URL_DOWNLOAD, + PLUGIN_URL_SOURCE_CODE, + PLUGIN_ZIP_NAME, + TRANSLATIONS_PATH, + __long_description__, + __package_name__, + __short_description__, + __version__, + basedir, +) + +# constants +# folder +build_path = basedir / "build" +build_path.mkdir(exist_ok=True) +lib_path = basedir / "lib" +lib_path.mkdir(exist_ok=True) + +# file +build_ignore_path = basedir / ".buildignore" +build_ignore_path.touch() # if no existed, would be created +plugin_info_path = basedir / "plugin.json" +zip_path = build_path / f"{PLUGIN_ZIP_NAME}" + +plugin_infos = { + "ID": PLUGIN_ID, + "ActionKeyword": PLUGIN_ACTION_KEYWORD, + "Name": __package_name__, + "Description": __short_description__, + "Author": PLUGIN_AUTHOR, + "Version": __version__, + "Language": PLUGIN_PROGRAM_LANG, + "Website": PLUGIN_URL, + "IcoPath": ICON_PATH, + "ExecuteFileName": PLUGIN_EXECUTE_FILENAME, + "UrlDownload": PLUGIN_URL_DOWNLOAD, + "UrlSourceCode": PLUGIN_URL_SOURCE_CODE, +} + + +def get_build_ignores(comment: str = "#") -> List[str]: + """ + Ignore file or folder when building a plugin, similar to '.gitignore'. + """ + ignore_list = [] + + with open(build_ignore_path, "r") as f: + for line in f.readlines(): + line = line.strip() + if line and not line.startswith(comment): + ignore_list.append(line) + + return ignore_list + + +def hook_env_snippet(exec_file: str = PLUGIN_EXECUTE_FILENAME) -> str: + """Hook lib folder path to python system environment variable path.""" + + env_snippet = dedent( + f"""\ + import os + import sys + + basedir = os.path.dirname(os.path.abspath(__file__)) + sys.path.append(os.path.join(basedir, "{lib_path.name}")) + """ + ) + + temp_path = build_path / exec_file + entry_src = basedir / exec_file + with open(entry_src, "r") as f_r: + with open(temp_path, "w") as f_w: + f_w.write(env_snippet + f_r.read()) + + return temp_path @click.group() @@ -18,38 +99,39 @@ def translate(): @translate.command() -@click.argument('locale') +@click.argument("locale") def init(locale): """Initialize a new language.""" - if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): - raise RuntimeError('extract command failed') - if os.system('pybabel init -i messages.pot -d plugin/translations -l ' + locale): - raise RuntimeError('init command failed') - os.remove('messages.pot') + if os.system("pybabel extract -F babel.cfg -k _l -o messages.pot ."): + raise RuntimeError("extract command failed") + if os.system(f"pybabel init -i messages.pot -d {TRANSLATIONS_PATH} -l {locale}"): + raise RuntimeError("init command failed") + os.remove("messages.pot") - click.echo('Done.') + click.echo("Done.") @translate.command() def update(): """Update all languages.""" - if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): - raise RuntimeError('extract command failed') - if os.system('pybabel update -i messages.pot -d plugin/translations'): - raise RuntimeError('update command failed') - os.remove('messages.pot') + if os.system("pybabel extract -F babel.cfg -k _l -o messages.pot ."): + raise RuntimeError("extract command failed") + if os.system(f"pybabel update -i messages.pot -d {TRANSLATIONS_PATH}"): + raise RuntimeError("update command failed") + os.remove("messages.pot") - click.echo('Done.') + click.echo("Done.") @translate.command() def compile(): """Compile all languages.""" - if os.system('pybabel compile -d plugin/translations'): - raise RuntimeError('compile command failed') - click.echo('Done.') + if os.system(f"pybabel compile -d {TRANSLATIONS_PATH}"): + raise RuntimeError("compile command failed") + + click.echo("Done.") @click.group() @@ -60,29 +142,90 @@ def plugin(): @plugin.command() def gen_plugin_info(): - """Auto generate the `plugin.json` file for Flow""" + """Auto generate the 'plugin.json' file for Flow.""" + + with open(plugin_info_path, "w") as f: + json.dump(plugin_infos, f, indent=4) + + click.echo("Done.") + + +@plugin.command() +def build(): + "Pack plugin to a zip file." + + zip_path.unlink(missing_ok=True) + + ignore_list = get_build_ignores() + ignore_string = "'" + "' '".join(ignore_list) + "'" + os.system(f"zip -r {zip_path} . -x {ignore_string}") + + entry_src_hooked = hook_env_snippet() + os.system(f"zip -j {zip_path} {entry_src_hooked}") + entry_src_hooked.unlink() + + click.echo("Done.") + + +@click.group() +def env(): + ... + + +@env.command() +def setup_env(): + """ + Set up the environment for the first time. + This installs requirements.txt and requirements-dev.txt + """ + + os.system("pip install -r requirements.txt --upgrade") + os.system("pip install -r requirements-dev.txt --upgrade") + + click.echo("Environment ready to go.") + + +@env.command() +def setup_env_to_lib(): + """Install dependencies to local.""" + + os.system(f"pip install -r requirements.txt -t {lib_path} --upgrade") + + click.echo("Done.") + + +@click.group() +def clean(): + """Clean commands.""" + ... + + +@clean.command() +def clean_build(): + """Remove build artifacts""" + + os.system("rm -fr build/") + click.echo("Done.") - plugin_infos = { - "ID": PLUGIN_ID, - "ActionKeyword": PLUGIN_ACTION_KEYWORD, - "Name": __package_name__.title(), - "Description": __short_description__, - "Author": PLUGIN_AUTHOR, - "Version": __version__, - "Language": PLUGIN_PROGRAM_LANG, - "Website": PLUGIN_URL, - "IcoPath": ICON_PATH, - "ExecuteFileName": PLUGIN_EXECUTE_FILENAME - } - json_path = os.path.join(basedir, 'plugin.json') - with open(json_path, 'w') as f: - json.dump(plugin_infos, f, indent=' '*4) +@clean.command() +def clean_pyc(): + "Remove Python file artifacts" - click.echo('Done.') + os.system(f"find {basedir} -name '*.pyc' -exec rm -f {{}} +") + os.system(f"find {basedir} -name '*.pyo' -exec rm -f {{}} +") + os.system(f"find {basedir} -name '*~' -exec rm -f {{}} +") + click.echo("Done.") -cli = click.CommandCollection(sources=[plugin, translate]) if __name__ == "__main__": + cli = click.CommandCollection( + sources=[ + clean, + env, + plugin, + translate, + ] + ) cli() diff --git a/Flow.Launcher.Plugin.PythonTemplate/plugin/__init__.py b/Flow.Launcher.Plugin.PythonTemplate/plugin/__init__.py index 684d885..cf9e2ec 100644 --- a/Flow.Launcher.Plugin.PythonTemplate/plugin/__init__.py +++ b/Flow.Launcher.Plugin.PythonTemplate/plugin/__init__.py @@ -1,14 +1,27 @@ # -*- coding: utf-8 -*- """ Plugin -===== +====== Introduce the plugin. """ from plugin.settings import ( - GITHUB_USERNAME, ICON_PATH, PLUGIN_ACTION_KEYWORD, PLUGIN_AUTHOR, - PLUGIN_EXECUTE_FILENAME, PLUGIN_ID, PLUGIN_PROGRAM_LANG, PLUGIN_URL, - __long_description__, __package_name__, __short_description__, __version__, - basedir) + ICON_PATH, + PLUGIN_ACTION_KEYWORD, + PLUGIN_AUTHOR, + PLUGIN_EXECUTE_FILENAME, + PLUGIN_ID, + PLUGIN_PROGRAM_LANG, + PLUGIN_URL, + PLUGIN_URL_DOWNLOAD, + PLUGIN_URL_SOURCE_CODE, + PLUGIN_ZIP_NAME, + TRANSLATIONS_PATH, + __long_description__, + __package_name__, + __short_description__, + __version__, + basedir, +) from plugin.ui import Main diff --git a/Flow.Launcher.Plugin.PythonTemplate/plugin/extensions.py b/Flow.Launcher.Plugin.PythonTemplate/plugin/extensions.py index df38abf..eb4b0d1 100644 --- a/Flow.Launcher.Plugin.PythonTemplate/plugin/extensions.py +++ b/Flow.Launcher.Plugin.PythonTemplate/plugin/extensions.py @@ -2,10 +2,13 @@ import gettext -from plugin.settings import LOCAL +from plugin.settings import LOCAL, TRANSLATIONS_PATH # localization translation = gettext.translation( - "messages", "plugin/translations/", languages=[LOCAL]) + "messages", + TRANSLATIONS_PATH, + languages=[LOCAL], +) _l = translation.gettext diff --git a/Flow.Launcher.Plugin.PythonTemplate/plugin/settings.py b/Flow.Launcher.Plugin.PythonTemplate/plugin/settings.py index 02ef341..fbf44fc 100644 --- a/Flow.Launcher.Plugin.PythonTemplate/plugin/settings.py +++ b/Flow.Launcher.Plugin.PythonTemplate/plugin/settings.py @@ -10,15 +10,13 @@ pludir = setting_pyfile.parent basedir = pludir.parent -dotenv_path = basedir / '.env' +dotenv_path = basedir / ".env" if dotenv_path.exists(): load_dotenv(dotenv_path) -ICON_PATH = 'assets/favicon.ico' - # The default value can work, if no user config. -CONFIG = os.getenv('CONFIG', 'default config') +CONFIG = os.getenv("CONFIG", "default config") LOCAL = os.getenv("local", "en") @@ -29,18 +27,26 @@ GITHUB_USERNAME = "USERNAME" -readme_path = os.path.join(basedir, 'README.md') +readme_path = basedir / "README.md" try: __long_description__ = open(readme_path, "r").read() except: __long_description__ = __short_description__ -# other information -PLUGIN_ID = "uuid" +# extensions +TRANSLATIONS_PATH = basedir / "plugin/translations" + +# plugin.json +PLUGIN_ID = "uuid" # could generate via python `uuid` official package ICON_PATH = "assets/favicon.ico" -PLUGIN_ACTION_KEYWORD = "KEYWORD" PLUGIN_AUTHOR = "AUTHOR_NAME" +PLUGIN_ACTION_KEYWORD = "KEYWORD" PLUGIN_PROGRAM_LANG = "python" -PLUGIN_URL = f"https://github.com/{GITHUB_USERNAME}/{__package_name__}" PLUGIN_EXECUTE_FILENAME = "main.py" +PLUGIN_ZIP_NAME = f"{__package_name__}-{__version__}.zip" +PLUGIN_URL = f"https://github.com/{GITHUB_USERNAME}/{__package_name__}" +PLUGIN_URL_SOURCE_CODE = f"https://github.com/{GITHUB_USERNAME}/{__package_name__}" +PLUGIN_URL_DOWNLOAD = ( + f"{PLUGIN_URL_SOURCE_CODE}/releases/download/v{__version__}/{PLUGIN_ZIP_NAME}" +) diff --git a/Flow.Launcher.Plugin.PythonTemplate/plugin/ui.py b/Flow.Launcher.Plugin.PythonTemplate/plugin/ui.py index 6197a8e..6ee13e6 100644 --- a/Flow.Launcher.Plugin.PythonTemplate/plugin/ui.py +++ b/Flow.Launcher.Plugin.PythonTemplate/plugin/ui.py @@ -13,21 +13,21 @@ class Main(FlowLauncher): def sendNormalMess(self, title: str, subtitle: str): message = copy.deepcopy(RESULT_TEMPLATE) - message['Title'] = title - message['SubTitle'] = subtitle + message["Title"] = title + message["SubTitle"] = subtitle self.messages_queue.append(message) def sendActionMess(self, title: str, subtitle: str, method: str, value: List): # information message = copy.deepcopy(RESULT_TEMPLATE) - message['Title'] = title - message['SubTitle'] = subtitle + message["Title"] = title + message["SubTitle"] = subtitle # action action = copy.deepcopy(ACTION_TEMPLATE) - action['JsonRPCAction']['method'] = method - action['JsonRPCAction']['parameters'] = value + action["JsonRPCAction"]["method"] = method + action["JsonRPCAction"]["parameters"] = value message.update(action) self.messages_queue.append(message) diff --git a/Flow.Launcher.Plugin.PythonTemplate/requirements-dev.txt b/Flow.Launcher.Plugin.PythonTemplate/requirements-dev.txt index 03f4fbc..8a2d292 100644 --- a/Flow.Launcher.Plugin.PythonTemplate/requirements-dev.txt +++ b/Flow.Launcher.Plugin.PythonTemplate/requirements-dev.txt @@ -1,2 +1,3 @@ click babel +python-dotenv \ No newline at end of file