From 748dc9486c51750fbb8025ddce4f55959bc91965 Mon Sep 17 00:00:00 2001 From: Josue <92873227+J-Josu@users.noreply.github.com> Date: Sun, 24 Sep 2023 17:50:07 -0300 Subject: [PATCH] Implemented the dev.py commands as package level commands to simplify usage (#1) * Started to implement the commands as separate package level commands * Added pre-commit hooks * Added more rules to pre-commit * Refactored dev.py commands to package level stand alone commands * Removed config from from pre-commit * Fixed bugs and typos on new commands * Better type checking * Fixed local-install command and updated readme with new changes --- .gitignore | 1 + .pre-commit-config.yaml | 25 + .vscode/settings.json | 9 +- README.md | 160 +++---- flask_livetw/__main__.py | 1 - flask_livetw/build_app.py | 112 +++++ flask_livetw/cli.py | 251 ---------- flask_livetw/config.py | 431 ++++++++++++++++++ flask_livetw/dev_server.py | 363 +++++++++++++++ flask_livetw/initialize.py | 318 +++++++++++++ flask_livetw/local_install.py | 387 ++++++++++++++++ flask_livetw/main.py | 297 ++---------- flask_livetw/{static => resources}/dev.py | 1 + flask_livetw/{static => resources}/global.css | 0 .../{static => resources}/layout.html | 0 .../{static => resources}/live_reload.js | 0 .../{static => resources}/tailwind.config.js | 0 flask_livetw/util.py | 13 +- poetry.lock | 7 - pyproject.toml | 22 +- 20 files changed, 1781 insertions(+), 617 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 flask_livetw/build_app.py delete mode 100644 flask_livetw/cli.py create mode 100644 flask_livetw/config.py create mode 100644 flask_livetw/dev_server.py create mode 100644 flask_livetw/initialize.py create mode 100644 flask_livetw/local_install.py rename flask_livetw/{static => resources}/dev.py (99%) rename flask_livetw/{static => resources}/global.css (100%) rename flask_livetw/{static => resources}/layout.html (100%) rename flask_livetw/{static => resources}/live_reload.js (100%) rename flask_livetw/{static => resources}/tailwind.config.js (100%) delete mode 100644 poetry.lock diff --git a/.gitignore b/.gitignore index 555451d..15c8501 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ # poetry dist/ poetry.toml +poetry.lock .pytest_cache/ # others diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..343c7c4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: 'v4.4.0' + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: no-commit-to-branch + +- repo: https://github.com/pycqa/isort + rev: '5.12.0' + hooks: + - id: isort + +- repo: https://github.com/psf/black + rev: '23.9.1' + hooks: + - id: black + +- repo: https://github.com/pycqa/flake8 + rev: '6.1.0' + hooks: + - id: flake8 + args: ['--extend-ignore=E203,E501,W503'] + stages: [pre-push] diff --git a/.vscode/settings.json b/.vscode/settings.json index 983fc2e..925ea1e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,11 +3,9 @@ "editor.defaultFormatter": "ms-python.black-formatter", "editor.tabSize": 4 }, - "black-formatter.args": [ - "--line-length=79" - ], "files.exclude": { - "**/__pycache__": true + "**/__pycache__": true, + "**/.venv": true }, "files.insertFinalNewline": true, "files.trimFinalNewlines": true, @@ -15,8 +13,5 @@ "flake8.args": [ "--ignore=E501,W503" ], - "isort.args": [ - "--profile=black" - ], "python.analysis.typeCheckingMode": "strict" } diff --git a/README.md b/README.md index acc41f2..2e721c9 100644 --- a/README.md +++ b/README.md @@ -1,141 +1,143 @@ # Flask live tailwindcss -A simple package for adding a dev server to your flask app that automatically compiles your tailwindcss of the templates on save and reloads your browser to sync the changes. +A simple package for adding a dev server to your flask app that automatically compiles the tailwindcss of the templates on file save and triggers a browser reload to sync the changes on the fly. -> **Note:** This package is intended to use with [poetry](https://python-poetry.org/). If you are not using poetry, you can still use this package by installing the dependencies manually. - -## Integrate with poetry - -### Installation +## Installation ```bash +# using poetry poetry add --group=dev flask-livetw + +# using pip +pip install flask-livetw ``` -### Initialization -Simply go to your project root folder, run the following command and follow along the steps. +## Initialization + +To start using this package, simply go to your project root folder, run the following command and follow along the steps. ```bash -poetry run flask-livetw +# using poetry +poetry shell +flask-livetw init + +# using pip +"activate your virtual environment like you normally do" +flask-livetw init ``` -> **Note 1:** If want to skip the questions, you can use the `-Y` or `--yes` flag. +> **Note 1:** To skip requirements check use the `-Y` or `--yes` flag. > -> **Note 2:** If you want to use default values for the setup, you can use the `-D` or `--default` flag. -> -> **Note 3:** You can use the `-h` or `--help` flag to see the available options. - -## Integrate with pip +> **Note 2:** To use default values for the initialization use the `-D` or `--default` flag. -### Installation +### Default values -```bash -pip install flask-livetw -``` +```py +FLASK_ROOT = "src" -### Initialization +STATIC_FOLDER = "src/static" -Simply go to your project root folder, run the following command and follow along the steps. +TEMPLATE_FOLDER = "src/templates" +TEMPLATE_GLOB = "src/templates/**/*.html" +ROOT_LAYOUT_FILE = "src/templates/layout.html" -```bash -python -m flask_livetw -``` +LIVE_RELOAD_FILE = "src/static/.dev/live_reload.js" -After the initialization, you need to install the dependencies manually. +GLOBALCSS_FILE = "src/static/.dev/global.css" +TAILWIND_FILE = "src/static/.dev/tailwind.css" +MINIFIED_TAILWIND_FILE = "src/static/tailwind_min.css" -```bash -pip install pytailwindcss python-dotenv websockets +UPDATE_GITIGNORE = False ``` +Example as file system tree: +```txt +project_root +├── src +│ ├── static +│ │ ├── .dev +│ │ │ ├── global.css +│ │ │ ├── live_reload.js +│ │ │ └── tailwind.css +│ │ ├── tailwind_min.css +│ │ ... +│ └── templates +│ ├── layout.html +│ ... +├── .gitignore +├── pyproject.toml +... +``` -## Usage -### Development +## Commands -When developing your app, you can use the following command to start the dev server. +In order to use the commands, you need to activate your virtual environment first. ```bash -./dev.py dev -``` +# using poetry +poetry shell -> **Note:** You can use the `-h` or `--help` flag to see the available options. +# using pip +"activate your virtual environment like you normally do" +``` -### Building +Each command has its own help page, you can use the `-h` or `--help` flag to see the available options. -When you are done developing, you can use the following command to build your app. +### dev ```bash -./dev.py build +flask-livetw dev ``` -> **Note:** You can use the `-h` or `--help` flag to see the available options. - +By default the command starts: -## Default values +- a flask server in debug mode +- a live reload websocket server +- a tailwindcss in watch mode -### Package cli +### build -```py -DEFAULT_FLASK_ROOT = "src" +Builds the tailwindcss of the templates as a single css file. -DEFAULT_STATIC_FOLDER = "src/static" +```bash +flask-livetw build +``` -DEFAULT_TEMPLATE_FOLDER = "src/templates" -DEFAULT_TEMPLATE_GLOB = "src/templates/**/*.html" +By default the builded tailwindcss file will be minified. -DEFAULT_ROOT_LAYOUT_FILE = "src/templates/layout.html" -DEFAULT_LIVE_RELOAD_FILE = "src/static/.dev/live_reload.js" -DEFAULT_GLOBALCSS_FILE = ".dev/global.css" -DEFAULT_TWCSS_FILE = "src/static/.dev/tailwindcss.css" -DEFAULT_MINIFIED_TWCSS_FILE = "src/static/tailwindcss_min.css" +### local-install -DEFAULT_UPDATE_GITIGNORE = False +```bash +flask-livetw local-install ``` -Example as file system tree: - -```txt -project_root -├── src -│ ├── static -│ │ ├── .dev -│ │ │ ├── global.css -│ │ │ ├── live_reload.js -│ │ │ └── tailwindcss.css -│ │ ├── tailwindcss_min.css -│ │ ... -│ └── templates -│ ├── layout.html -│ ... -├── .gitignore -├── dev.py -├── pyproject.toml -... -``` +This command creates a local script that mimics the `flask-livetw` command and installs the necessary dependencies. -### Dev server +After the installation, you can use the `dev` and `build` commands as follows: -```py -LRWS_HOST = "127.0.0.1" -LRWS_PORT = 5678 -TW_INPUT_PATH = "package_test/static/.dev/global.css" -TW_OUTPUT_PATH = "src/static/.dev/tailwindcss.css" -TW_OUTPUT_PATH_BUILD = "src/static/tailwindcss_min.css" +```bash +./dev.py dev +./dev.py build ``` + ## Contributing Contributions are welcome, feel free to submit a pull request or an issue. -## Credits + +## Packages used - [pytailwindcss](https://github.com/timonweb/pytailwindcss) - [python-dotenv](https://github.com/theskumar/python-dotenv) +- [tomli](https://github.com/hukkin/tomli) - [websockets](https://github.com/python-websockets/websockets) + ## License [MIT](./LICENSE) diff --git a/flask_livetw/__main__.py b/flask_livetw/__main__.py index 2253097..c82292c 100644 --- a/flask_livetw/__main__.py +++ b/flask_livetw/__main__.py @@ -1,5 +1,4 @@ from flask_livetw.main import main - if __name__ == "__main__": raise SystemExit(main()) diff --git a/flask_livetw/build_app.py b/flask_livetw/build_app.py new file mode 100644 index 0000000..341594e --- /dev/null +++ b/flask_livetw/build_app.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import argparse +import dataclasses +import shlex +import subprocess +from typing import Sequence + +from flask_livetw.config import Config +from flask_livetw.util import Term, pkgprint + +MINIFY_ON_BUILD = True + + +@dataclasses.dataclass +class BuildConfig: + input: str + output: str + minify: bool + + +def minify_tailwind(config: BuildConfig) -> int: + input_arg = f"-i {config.input}" + + output_arg = f"-o {config.output}" + + minify_arg = "--minify" if config.minify else "" + + command = f"tailwindcss {input_arg} {output_arg} {minify_arg}" + + pkgprint("Minifying tailwindcss for production...") + + build_result = subprocess.run(shlex.split(command)) + + if build_result.returncode != 0: + pkgprint(f"Tailwind build for production {Term.R}fail{Term.END}") + return build_result.returncode + + pkgprint(f"Tailwind build for production {Term.G}ready{Term.END}") + return build_result.returncode + + +def build(cli_args: argparse.Namespace) -> int: + config = Config.from_pyproject_toml() + + build_config = BuildConfig( + input=cli_args.input or config.full_globalcss_file, + output=cli_args.output or config.full_tailwind_minified_file, + minify=cli_args.minify, + ) + + return minify_tailwind(build_config) + + +def add_command_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "-i", + "--input", + dest="input", + type=str, + help="Input path, accepts glob patterns.", + ) + parser.add_argument( + "-o", "--output", dest="output", type=str, help="Output path." + ) + build_minify_group = parser.add_mutually_exclusive_group() + build_minify_group.add_argument( + "--minify", dest="minify", action="store_true", help="Minify output." + ) + build_minify_group.add_argument( + "--no-minify", + dest="minify", + action="store_false", + help="Do not minify output.", + ) + parser.set_defaults(minify=MINIFY_ON_BUILD) + + +def add_command( + subparser: argparse._SubParsersAction[argparse.ArgumentParser], +) -> None: + parser = subparser.add_parser( + name="build", + description=""" + Build the tailwindcss of the project as a single minified css file. + """, + help="Build tailwindcss for production.", + allow_abbrev=True, + formatter_class=argparse.MetavarTypeHelpFormatter, + ) + + add_command_args(parser) + + +def main(args: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description=""" + Build the tailwindcss of the project as a single css file. + """, + allow_abbrev=True, + formatter_class=argparse.MetavarTypeHelpFormatter, + ) + + add_command_args(parser) + + parsed_args = parser.parse_args(args) + + return build(parsed_args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/flask_livetw/cli.py b/flask_livetw/cli.py deleted file mode 100644 index 57e0092..0000000 --- a/flask_livetw/cli.py +++ /dev/null @@ -1,251 +0,0 @@ -import argparse -import dataclasses -import os -import shlex -import subprocess -from typing import Union - -from flask_livetw.util import Term - -DEFAULT_FLASK_ROOT = "src" - -DEFAULT_STATIC_FOLDER = "static" - -DEFAULT_TEMPLATE_FOLDER = "templates" -DEFAULT_TEMPLATE_GLOB = "**/*.html" - -DEFAULT_ROOT_LAYOUT_FILE = "layout.html" -DEFAULT_LIVE_RELOAD_FILE = ".dev/live_reload.js" -DEFAULT_GLOBALCSS_FILE = ".dev/global.css" -DEFAULT_TWCSS_FILE = ".dev/tailwindcss.css" -DEFAULT_MINIFIED_TWCSS_FILE = "tailwindcss_min.css" - -DEFAULT_UPDATE_GITIGNORE = False - - -PKG_PREFIX = f"{Term.M}[livetw]{Term.END}" - - -@dataclasses.dataclass -class Config: - gitignore: bool - - flask_root: Union[str, None] - - static_folder: str - full_static_folder: str - - templates_folder: str - full_templates_folder: str - templates_glob: str - full_templates_glob: str - - root_layout_file: str - full_root_layout_file: str - live_reload_file: str - full_live_reload_file: str - globalcss_file: str - full_globalcss_file: str - twcss_file: str - full_twcss_file: str - minified_twcss_file: str - full_minified_twcss_file: str - - -def create_cli() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser( - description="Mods a Flask app to use TailwindCSS in a dev server like manner.", - allow_abbrev=True, - formatter_class=argparse.MetavarTypeHelpFormatter, - ) - - parser.add_argument( - "-Y", - "--yes", - dest="all_yes", - action="store_true", - default=False, - help="answer yes to all requirements checks", - ) - - parser.add_argument( - "-D", - "--default", - dest="default", - action="store_true", - default=False, - help="use default values for all options", - ) - - parser.add_argument( - "--gi", - "--gitignore", - dest="gitignore", - action="store_true", - default=DEFAULT_UPDATE_GITIGNORE, - help=f"update .gitignore to exclude dev related files (default: {DEFAULT_UPDATE_GITIGNORE})", - ) - - parser.add_argument( - "--fr", - "--flask-root", - dest="flask_root", - type=str, - help="flask app root path (relative to cwd)", - ) - - return parser - - -def pkgprint(*values: object, end: str = "\n", sep: str = " ") -> None: - print(f"{PKG_PREFIX}", *values, end=end, sep=sep) - - -def check_requirements() -> int: - pkgprint("Checking requirements...") - cwd = os.getcwd() - pkgprint(f"Current working directory: {Term.C}{cwd}{Term.END}") - continue_install = Term.confirm(f"{PKG_PREFIX} Is this your project root?") - - if not continue_install: - pkgprint("Change cwd and start again. Modding canceled") - return 1 - - python_cmd = shlex.split("python --version") - python_cmd_result = subprocess.run( - python_cmd, shell=True, capture_output=True - ) - - if python_cmd_result.returncode != 0: - Term.error("python --version failed, is python installed?") - return python_cmd_result.returncode - - version = python_cmd_result.stdout.decode("utf-8").strip() - if version != "Python 3.8.10": - pkgprint(f"python --version: {Term.C}{version}{Term.END}") - - continue_install = Term.confirm( - f"{PKG_PREFIX} Continue with this version?" - ) - if not continue_install: - pkgprint("Change python version and start again. Modding canceled") - return 1 - - return 0 - - -def get_config(args: argparse.Namespace) -> Config: - if args.default: - return Config( - gitignore=DEFAULT_UPDATE_GITIGNORE, - flask_root=DEFAULT_FLASK_ROOT, - static_folder=DEFAULT_STATIC_FOLDER, - full_static_folder=f"{DEFAULT_FLASK_ROOT}/{DEFAULT_STATIC_FOLDER}", - templates_folder=DEFAULT_TEMPLATE_FOLDER, - full_templates_folder=f"{DEFAULT_FLASK_ROOT}/{DEFAULT_TEMPLATE_FOLDER}", - templates_glob=DEFAULT_TEMPLATE_GLOB, - full_templates_glob=f"{DEFAULT_FLASK_ROOT}/{DEFAULT_TEMPLATE_FOLDER}/{DEFAULT_TEMPLATE_GLOB}", - root_layout_file=DEFAULT_ROOT_LAYOUT_FILE, - full_root_layout_file=f"{DEFAULT_FLASK_ROOT}/{DEFAULT_TEMPLATE_FOLDER}/{DEFAULT_ROOT_LAYOUT_FILE}", - live_reload_file=DEFAULT_LIVE_RELOAD_FILE, - full_live_reload_file=f"{DEFAULT_FLASK_ROOT}/{DEFAULT_STATIC_FOLDER}/{DEFAULT_LIVE_RELOAD_FILE}", - globalcss_file=DEFAULT_GLOBALCSS_FILE, - full_globalcss_file=f"{DEFAULT_FLASK_ROOT}/{DEFAULT_STATIC_FOLDER}/{DEFAULT_GLOBALCSS_FILE}", - twcss_file=DEFAULT_TWCSS_FILE, - full_twcss_file=f"{DEFAULT_FLASK_ROOT}/{DEFAULT_STATIC_FOLDER}/{DEFAULT_TWCSS_FILE}", - minified_twcss_file=DEFAULT_MINIFIED_TWCSS_FILE, - full_minified_twcss_file=f"{DEFAULT_FLASK_ROOT}/{DEFAULT_STATIC_FOLDER}/{DEFAULT_MINIFIED_TWCSS_FILE}", - ) - - gitignore = True if args.gitignore else False - - Term.blank() - - flask_root = args.flask_root - if flask_root is None: - flask_root = Term.ask_dir( - f"{PKG_PREFIX} Flask app root (relative to {Term.C}cwd/{Term.END}) [{DEFAULT_FLASK_ROOT}] ", - default=DEFAULT_FLASK_ROOT, - ) - - static_folder = Term.ask_dir( - f"{PKG_PREFIX} Static folder (relative to {Term.C}cwd/{flask_root}/{Term.END}) [{DEFAULT_STATIC_FOLDER}] ", - flask_root, - DEFAULT_STATIC_FOLDER, - ) - - templates_folder = Term.ask_dir( - f"{PKG_PREFIX} Templates folder (relative to {Term.C}cwd/{flask_root}/{Term.END}) [{DEFAULT_TEMPLATE_FOLDER}] ", - flask_root, - DEFAULT_TEMPLATE_FOLDER, - ) - - templates_glob = ( - Term.ask( - f"{PKG_PREFIX} Templates glob (relative to {Term.C}cwd/{flask_root}/{templates_folder}/{Term.END}) [{DEFAULT_TEMPLATE_GLOB}] ", - ) - or DEFAULT_TEMPLATE_GLOB - ) - - root_layout_file = ( - Term.ask( - f"{PKG_PREFIX} Root layout file (relative to {Term.C}cwd/{flask_root}/{templates_folder}/{Term.END}) [{DEFAULT_ROOT_LAYOUT_FILE}] ", - ) - or DEFAULT_ROOT_LAYOUT_FILE - ) - - live_reload_file = ( - Term.ask( - f"{PKG_PREFIX} Live reload file (relative to {Term.C}cwd/{flask_root}/{static_folder}/{Term.END}) [{DEFAULT_LIVE_RELOAD_FILE}] ", - ) - or DEFAULT_LIVE_RELOAD_FILE - ) - - globalcss_file = ( - Term.ask( - f"{PKG_PREFIX} Global css file (relative to {Term.C}cwd/{flask_root}/{static_folder}/{Term.END}) [{DEFAULT_GLOBALCSS_FILE}] ", - ) - or DEFAULT_GLOBALCSS_FILE - ) - - twcss_file = ( - Term.ask( - f"{PKG_PREFIX} TailwindCSS file (relative to {Term.C}cwd/{flask_root}/{static_folder}/{Term.END}) [{DEFAULT_TWCSS_FILE}] ", - ) - or DEFAULT_TWCSS_FILE - ) - - minified_twcss_file = ( - Term.ask( - f"{PKG_PREFIX} Minified TailwindCSS file (relative to {Term.C}cwd/{flask_root}/{static_folder}/{Term.END}) [{DEFAULT_MINIFIED_TWCSS_FILE}] ", - ) - or DEFAULT_MINIFIED_TWCSS_FILE - ) - - if not flask_root or flask_root == ".": - full_static_folder = static_folder - full_templates_folder = templates_folder - else: - full_static_folder = f"{flask_root}/{static_folder}" - full_templates_folder = f"{flask_root}/{templates_folder}" - - return Config( - gitignore=gitignore, - flask_root=flask_root, - static_folder=static_folder, - full_static_folder=full_static_folder, - templates_folder=templates_folder, - full_templates_folder=full_templates_folder, - templates_glob=templates_glob, - full_templates_glob=f"{full_templates_folder}/{templates_glob}", - root_layout_file=root_layout_file, - full_root_layout_file=f"{full_templates_folder}/{root_layout_file}", - live_reload_file=live_reload_file, - full_live_reload_file=f"{full_static_folder}/{live_reload_file}", - globalcss_file=globalcss_file, - full_globalcss_file=f"{full_static_folder}/{globalcss_file}", - twcss_file=twcss_file, - full_twcss_file=f"{full_static_folder}/{twcss_file}", - minified_twcss_file=minified_twcss_file, - full_minified_twcss_file=f"{full_static_folder}/{minified_twcss_file}", - ) diff --git a/flask_livetw/config.py b/flask_livetw/config.py new file mode 100644 index 0000000..9e99058 --- /dev/null +++ b/flask_livetw/config.py @@ -0,0 +1,431 @@ +from __future__ import annotations + +import dataclasses +import re +from typing import Any + +import tomli + +from flask_livetw.util import PKG_PP, Term + +DEFAULT_FLASK_ROOT = "src" + +DEFAULT_FOLDER_STATIC = "static" + +DEFAULT_FOLDER_TEMPLATE = "templates" +DEFAULT_TEMPLATES_GLOB = "**/*.html" + +DEFAULT_FILE_ROOT_LAYOUT = "layout.html" +DEFAULT_FILE_LIVE_RELOAD = ".dev/live_reload.js" +DEFAULT_FILE_GLOBALCSS = ".dev/global.css" +DEFAULT_FILE_TAILWIND = ".dev/tailwind.css" +DEFAULT_FILE_MINIFIED_TAILWIND = "tailwind_min.css" + +DEFAULT_LIVERELOAD_HOST = "127.0.0.1" +DEFAULT_LIVERELOAD_PORT = 5678 + +DEFAULT_FLASK_HOST = None +DEFAULT_FLASK_PORT = None +DEFAULT_FLASK_EXCLUDE_PATTERNS = [] + + +def get_pyproject_toml(base_dir: str | None = None) -> dict[str, Any] | None: + path = ( + f"{base_dir.strip()}/pyproject.toml" + if base_dir and base_dir.strip() + else "pyproject.toml" + ) + try: + with open(path, "rb") as f: + return tomli.load(f) + except FileNotFoundError: + Term.info(f"Could not find pyproject.toml at '{path}'") + return None + except tomli.TOMLDecodeError as e: + Term.warn(f"Malformed pyproject.toml: {e}") + return None + + +@dataclasses.dataclass +class Config: + flask_root: str + + static_folder: str + full_static_folder: str + + templates_folder: str + full_templates_folder: str + + templates_glob: str + full_templates_glob: str + root_layout_file: str + full_root_layout_file: str + + live_reload_file: str + full_live_reload_file: str + + globalcss_file: str + full_globalcss_file: str + tailwind_file: str + full_tailwind_file: str + tailwind_minified_file: str + full_tailwind_minified_file: str + + live_reload_host: str + live_reload_port: int + + flask_host: str | None + flask_port: int | None + flask_exclude_patterns: list[str] | None + + @staticmethod + def from_dict_with_defaults( + src_dict: dict[str, Any], base_dir: str | None = None + ) -> Config: + flask_root = src_dict.get("flask_root", DEFAULT_FLASK_ROOT) + static_folder = src_dict.get("static_folder", DEFAULT_FOLDER_STATIC) + templates_folder = src_dict.get( + "templates_folder", DEFAULT_FOLDER_TEMPLATE + ) + + if isinstance(base_dir, str) and base_dir != "": + base_dir = base_dir.rstrip("/") + full_static_folder = f"{base_dir}/{flask_root}/{static_folder}" + full_templates_folder = ( + f"{base_dir}/{flask_root}/{templates_folder}" + ) + else: + full_static_folder = f"{flask_root}/{static_folder}" + full_templates_folder = f"{flask_root}/{templates_folder}" + + templates_glob = src_dict.get("templates_glob", DEFAULT_TEMPLATES_GLOB) + root_layout_file = src_dict.get( + "root_layout_file", DEFAULT_FILE_ROOT_LAYOUT + ) + live_reload_file = src_dict.get( + "live_reload_file", DEFAULT_FILE_LIVE_RELOAD + ) + globalcss_file = src_dict.get("globalcss_file", DEFAULT_FILE_GLOBALCSS) + tailwind_file = src_dict.get("tailwind_file", DEFAULT_FILE_TAILWIND) + tailwind_minified_file = src_dict.get( + "tailwind_minified_file", DEFAULT_FILE_MINIFIED_TAILWIND + ) + live_reload_host = src_dict.get( + "live_reload_host", DEFAULT_LIVERELOAD_HOST + ) + live_reload_port = src_dict.get( + "live_reload_port", DEFAULT_LIVERELOAD_PORT + ) + flask_host = src_dict.get("flask_host", DEFAULT_FLASK_HOST) + flask_port = src_dict.get("flask_port", DEFAULT_FLASK_PORT) + flask_exclude_patterns = src_dict.get( + "flask_exclude_patterns", DEFAULT_FLASK_EXCLUDE_PATTERNS + ) + + config_dict: dict[str, Any] = { + "flask_root": flask_root, + "static_folder": static_folder, + "full_static_folder": full_static_folder, + "templates_folder": templates_folder, + "full_templates_folder": full_templates_folder, + "templates_glob": templates_glob, + "full_templates_glob": f"{full_templates_folder}/{templates_glob}", + "root_layout_file": root_layout_file, + "full_root_layout_file": f"{full_templates_folder}/{root_layout_file}", + "live_reload_file": live_reload_file, + "full_live_reload_file": f"{full_static_folder}/{live_reload_file}", + "globalcss_file": globalcss_file, + "full_globalcss_file": f"{full_static_folder}/{globalcss_file}", + "tailwind_file": tailwind_file, + "full_tailwind_file": f"{full_static_folder}/{tailwind_file}", + "tailwind_minified_file": tailwind_minified_file, + "full_tailwind_minified_file": f"{full_static_folder}/{tailwind_minified_file}", + "live_reload_host": live_reload_host, + "live_reload_port": live_reload_port, + "flask_host": flask_host, + "flask_port": flask_port, + "flask_exclude_patterns": flask_exclude_patterns, + } + + return Config(**config_dict) + + @staticmethod + def default(base_dir: str | None = None) -> Config: + return Config.from_dict_with_defaults({}, base_dir) + + @staticmethod + def try_from_pyproject_toml(base_dir: str | None = None) -> Config | None: + if base_dir is None or base_dir == "": + base_dir = "." + else: + base_dir = base_dir.rstrip("/") + + pyproject = get_pyproject_toml(base_dir) + if pyproject is None: + return None + + tool_config = pyproject.get("tool") + if tool_config is None: + return None + + flask_livetw_config = tool_config.get("flask-livetw") + if flask_livetw_config is None: + return None + + return Config.from_dict_with_defaults(flask_livetw_config, base_dir) + + @staticmethod + def from_pyproject_toml(base_dir: str | None = None) -> Config: + """Errors are ignored and default values are used instead""" + if base_dir is None or base_dir == "": + base_dir = "." + + pyproject = get_pyproject_toml(base_dir) + if pyproject is None: + pyproject = {} + + config = pyproject.get("tool", {}) + if not isinstance( + config, dict + ): # pyright: ignore[reportUnnecessaryIsInstance] + config: dict[str, Any] = {} + + config = config.get("flask-livetw", {}) + if not isinstance( + config, dict + ): # pyright: ignore[reportUnnecessaryIsInstance] + config = {} + + return Config.from_dict_with_defaults(config, base_dir) + + +def add_field( + pyproject: dict[str, Any], + key: str, + new_config: Config, + default: Any, + acc: str, +) -> str: + value = pyproject.get(key, getattr(new_config, key)) + if value == default: + return acc + + value_type = type(value) + if value_type == str: + return f'{acc}\n{key} = "{value}"' + + return f"{acc}\n{key} = {value}" + + +def update_pyproject_toml(config: Config) -> int: + try: + with open("pyproject.toml", "rb") as f: + pyproject = tomli.load(f) + except FileNotFoundError: + Term.info("Could not find pyproject.toml") + pyproject = {} + except tomli.TOMLDecodeError as e: + Term.info(f"Malformed pyproject.toml: {e}") + Term.info("Verify that the file is valid TOML") + return 1 + + ppconfig = pyproject.get("tool", {}).get("flask-livetw", {}) + + new_config = """\n[tool.flask-livetw]""" + new_config = add_field( + ppconfig, "flask_root", config, DEFAULT_FLASK_ROOT, new_config + ) + new_config = add_field( + ppconfig, "static_folder", config, DEFAULT_FOLDER_STATIC, new_config + ) + new_config = add_field( + ppconfig, + "templates_folder", + config, + DEFAULT_FOLDER_TEMPLATE, + new_config, + ) + new_config = add_field( + ppconfig, "templates_glob", config, DEFAULT_TEMPLATES_GLOB, new_config + ) + new_config = add_field( + ppconfig, + "root_layout_file", + config, + DEFAULT_FILE_ROOT_LAYOUT, + new_config, + ) + new_config = add_field( + ppconfig, + "live_reload_file", + config, + DEFAULT_FILE_LIVE_RELOAD, + new_config, + ) + new_config = add_field( + ppconfig, "globalcss_file", config, DEFAULT_FILE_GLOBALCSS, new_config + ) + new_config = add_field( + ppconfig, "tailwind_file", config, DEFAULT_FILE_TAILWIND, new_config + ) + new_config = add_field( + ppconfig, + "tailwind_minified_file", + config, + DEFAULT_FILE_MINIFIED_TAILWIND, + new_config, + ) + new_config = add_field( + ppconfig, + "live_reload_host", + config, + DEFAULT_LIVERELOAD_HOST, + new_config, + ) + new_config = add_field( + ppconfig, + "live_reload_port", + config, + DEFAULT_LIVERELOAD_PORT, + new_config, + ) + new_config = add_field( + ppconfig, "flask_host", config, DEFAULT_FLASK_HOST, new_config + ) + new_config = add_field( + ppconfig, "flask_port", config, DEFAULT_FLASK_PORT, new_config + ) + new_config = add_field( + ppconfig, + "flask_exclude_patterns", + config, + DEFAULT_FLASK_EXCLUDE_PATTERNS, + new_config, + ) + new_config += "\n" + + try: + with open("pyproject.toml", "r") as f: + pyproject_toml = f.read() + if "[tool.flask-livetw]" in pyproject_toml: + pyproject_toml = re.sub( + r"\[tool\.flask-livetw\][^[]*", new_config, pyproject_toml + ) + else: + pyproject_toml += new_config + + with open("pyproject.toml", "w") as f: + f.write(pyproject_toml) + except FileNotFoundError: + Term.info("Could not find pyproject.toml") + Term.info("Creating pyproject.toml...") + with open("pyproject.toml", "w") as f: + f.write(new_config) + + return 0 + + +def ask_project_layout(app_source: str | None = None) -> Config: + flask_root = app_source + if flask_root is None or flask_root == "": + flask_root = Term.ask_dir( + f"{PKG_PP} Flask app root (relative to {Term.C}cwd/{Term.END}) [{DEFAULT_FLASK_ROOT}] ", + default=DEFAULT_FLASK_ROOT, + ) + + static_folder = Term.ask_dir( + f"{PKG_PP} Static folder (relative to {Term.C}cwd/{flask_root}/{Term.END}) [{DEFAULT_FOLDER_STATIC}] ", + flask_root, + DEFAULT_FOLDER_STATIC, + ) + + templates_folder = Term.ask_dir( + f"{PKG_PP} Templates folder (relative to {Term.C}cwd/{flask_root}/{Term.END}) [{DEFAULT_FOLDER_TEMPLATE}] ", + flask_root, + DEFAULT_FOLDER_TEMPLATE, + ) + + templates_glob = ( + Term.ask( + f"{PKG_PP} Templates glob (relative to {Term.C}cwd/{flask_root}/{templates_folder}/{Term.END}) [{DEFAULT_TEMPLATES_GLOB}] ", + ) + or DEFAULT_TEMPLATES_GLOB + ) + + root_layout_file = ( + Term.ask( + f"{PKG_PP} Root layout file (relative to {Term.C}cwd/{flask_root}/{templates_folder}/{Term.END}) [{DEFAULT_FILE_ROOT_LAYOUT}] ", + ) + or DEFAULT_FILE_ROOT_LAYOUT + ) + + live_reload_file = ( + Term.ask( + f"{PKG_PP} Live reload file (relative to {Term.C}cwd/{flask_root}/{static_folder}/{Term.END}) [{DEFAULT_FILE_LIVE_RELOAD}] ", + ) + or DEFAULT_FILE_LIVE_RELOAD + ) + + globalcss_file = ( + Term.ask( + f"{PKG_PP} Global css file (relative to {Term.C}cwd/{flask_root}/{static_folder}/{Term.END}) [{DEFAULT_FILE_GLOBALCSS}] ", + ) + or DEFAULT_FILE_GLOBALCSS + ) + + tailwind_file = ( + Term.ask( + f"{PKG_PP} TailwindCSS file (relative to {Term.C}cwd/{flask_root}/{static_folder}/{Term.END}) [{DEFAULT_FILE_TAILWIND}] ", + ) + or DEFAULT_FILE_TAILWIND + ) + + tailwind_minified_file = ( + Term.ask( + f"{PKG_PP} Minified TailwindCSS file (relative to {Term.C}cwd/{flask_root}/{static_folder}/{Term.END}) [{DEFAULT_FILE_MINIFIED_TAILWIND}] ", + ) + or DEFAULT_FILE_MINIFIED_TAILWIND + ) + + if not flask_root or flask_root == ".": + full_static_folder = static_folder + full_templates_folder = templates_folder + else: + full_static_folder = f"{flask_root}/{static_folder}" + full_templates_folder = f"{flask_root}/{templates_folder}" + + return Config( + flask_root=flask_root, + static_folder=static_folder, + full_static_folder=full_static_folder, + templates_folder=templates_folder, + full_templates_folder=full_templates_folder, + templates_glob=templates_glob, + full_templates_glob=f"{full_templates_folder}/{templates_glob}", + root_layout_file=root_layout_file, + full_root_layout_file=f"{full_templates_folder}/{root_layout_file}", + live_reload_file=live_reload_file, + full_live_reload_file=f"{full_static_folder}/{live_reload_file}", + globalcss_file=globalcss_file, + full_globalcss_file=f"{full_static_folder}/{globalcss_file}", + tailwind_file=tailwind_file, + full_tailwind_file=f"{full_static_folder}/{tailwind_file}", + tailwind_minified_file=tailwind_minified_file, + full_tailwind_minified_file=f"{full_static_folder}/{tailwind_minified_file}", + live_reload_host=DEFAULT_LIVERELOAD_HOST, + live_reload_port=DEFAULT_LIVERELOAD_PORT, + flask_host=DEFAULT_FLASK_HOST, + flask_port=DEFAULT_FLASK_PORT, + flask_exclude_patterns=DEFAULT_FLASK_EXCLUDE_PATTERNS, + ) + + +def main() -> int: + config = Config.from_pyproject_toml() + print(config) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/flask_livetw/dev_server.py b/flask_livetw/dev_server.py new file mode 100644 index 0000000..a5959e6 --- /dev/null +++ b/flask_livetw/dev_server.py @@ -0,0 +1,363 @@ +from __future__ import annotations + +import argparse +import asyncio +import concurrent.futures +import dataclasses +import datetime +import json +import shlex +import subprocess +from typing import Sequence, Set + +import websockets.legacy.protocol as ws_protocol +import websockets.server as ws_server + +from flask_livetw.config import Config +from flask_livetw.util import Term, pkgprint + +FLASK_BASE_EXCLUDE_PATTERNS = ("*/**/dev.py",) + +LR_CONNECTIONS: Set[ws_server.WebSocketServerProtocol] = set() + + +async def handle_connection(websocket: ws_server.WebSocketServerProtocol): + LR_CONNECTIONS.add(websocket) + try: + await websocket.wait_closed() + finally: + LR_CONNECTIONS.remove(websocket) + + +async def live_reload_server(host: str, port: int): + async with ws_server.serve(handle_connection, host, port) as server: + pkgprint( + f"Live reload {Term.G}ready{Term.END} on {Term.C}ws://{host}:{Term.BOLD}{port}{Term.END}" + ) + + await server.wait_closed() + + pkgprint(f"Live reload {Term.G}closed{Term.END}") + + +def handle_tailwind_output(process: subprocess.Popen[bytes]): + if process.stdout is None: + return + + for line in iter(process.stdout.readline, b""): + if process.poll() is not None: + break + + if line.startswith(b"Done"): + ws_protocol.broadcast( + LR_CONNECTIONS, + json.dumps( + { + "type": "TRIGGER_FULL_RELOAD", + "data": datetime.datetime.now().isoformat(), + } + ), + ) + + print(f'{Term.C}[twcss]{Term.END} {line.decode("utf-8")}', end="") + + +def handle_flask_output(process: subprocess.Popen[bytes]): + if process.stdout is None: + return + + for line in iter(process.stdout.readline, b""): + if process.poll() is not None: + break + + print(f'{Term.G}[flask]{Term.END} {line.decode("utf-8")}', end="") + + +@dataclasses.dataclass +class DevConfig: + no_live_reload: bool + live_reload_host: str + live_reload_port: int + + no_flask: bool + flask_host: str | None + flask_port: int | None + flask_mode: str + flask_exclude_patterns: Sequence[str] | None + + no_tailwind: bool + tailwind_input: str | None + tailwind_output: str + tailwind_minify: bool + + +async def dev_server(config: DevConfig): + def live_reload_coroutine(): + if config.no_live_reload or config.no_tailwind: + return None + + host = config.live_reload_host + port = config.live_reload_port + + return live_reload_server(host, port) + + def tailwind_cli_executor( + loop: asyncio.AbstractEventLoop, + pool: concurrent.futures.ThreadPoolExecutor, + ): + if config.no_tailwind: + return None + + input_arg = "" + if config.tailwind_input is not None: + input_arg = f"-i {config.tailwind_input}" + + output_arg = f"-o {config.tailwind_output}" + + minify_arg = "--minify" if config.tailwind_minify else "" + + cmd = f"tailwindcss --watch {input_arg} {output_arg} {minify_arg}" + + process = subprocess.Popen( + shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + + return loop.run_in_executor(pool, handle_tailwind_output, process) + + def flask_server_executor( + loop: asyncio.AbstractEventLoop, + pool: concurrent.futures.ThreadPoolExecutor, + ): + if config.no_flask: + return None + + host_arg = "" + if config.flask_host is not None: + host_arg = f"--host {config.flask_host}" + + port_arg = "" + if config.flask_port is not None: + port_arg = f"--port {config.flask_port}" + + debug_arg = "--debug" if config.flask_mode == "debug" else "" + + exclude_patterns: list[str] = list(FLASK_BASE_EXCLUDE_PATTERNS) + if config.flask_exclude_patterns is not None: + exclude_patterns.extend(config.flask_exclude_patterns) + + exclude_patterns_arg = ( + f"--exclude-patterns {';'.join(exclude_patterns)}" + ) + + cmd = f"\ + flask run {host_arg} {port_arg} {debug_arg} {exclude_patterns_arg}" + + process = subprocess.Popen( + shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + + return loop.run_in_executor(pool, handle_flask_output, process) + + loop = asyncio.get_running_loop() + + with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool: + maybe_future_like = ( + live_reload_coroutine(), + tailwind_cli_executor(loop, pool), + flask_server_executor(loop, pool), + ) + + futures = ( + future for future in maybe_future_like if future is not None + ) + + pkgprint("Starting dev server...") + + _ = await asyncio.gather(*futures, return_exceptions=True) + + +def dev(cli_args: argparse.Namespace) -> int: + project_config = Config.try_from_pyproject_toml() + if project_config is None: + pkgprint( + "Project config not found. Dev server not started.", + ) + pkgprint( + "Try checking your current working directory or running 'flask-livetw init' to configure the project." + ) + return 1 + + no_live_reload = cli_args.no_live_reload + live_reload_host = ( + cli_args.live_reload_host or project_config.live_reload_host + ) + live_reload_port = ( + cli_args.live_reload_port or project_config.live_reload_port + ) + + no_flask = cli_args.no_flask + flask_host = cli_args.flask_host or project_config.flask_host + flask_port = cli_args.flask_port or project_config.flask_port + flask_mode = cli_args.flask_mode + flask_exclude_patterns = ( + cli_args.flask_exclude_patterns + or project_config.flask_exclude_patterns + ) + + no_tailwind = cli_args.no_tailwind + tailwind_input = ( + cli_args.tailwind_input or project_config.full_globalcss_file + ) + tailwind_output = ( + cli_args.tailwind_output or project_config.full_tailwind_file + ) + tailwind_minify = cli_args.tailwind_minify + + dev_config = DevConfig( + no_live_reload=no_live_reload, + live_reload_host=live_reload_host, + live_reload_port=live_reload_port, + no_flask=no_flask, + flask_host=flask_host, + flask_port=flask_port, + flask_mode=flask_mode, + flask_exclude_patterns=flask_exclude_patterns, + no_tailwind=no_tailwind, + tailwind_input=tailwind_input, + tailwind_output=tailwind_output, + tailwind_minify=tailwind_minify, + ) + + asyncio.run(dev_server(dev_config)) + return 0 + + +def add_command_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "--no-live-reload", + dest="no_live_reload", + action="store_true", + default=False, + help="Disable live reload server.", + ) + parser.add_argument( + "-lrh", + "--live-reload-host", + dest="live_reload_host", + type=str, + help="Hostname for live reload server.", + ) + parser.add_argument( + "-lrp", + "--live-reload-port", + dest="live_reload_port", + type=int, + help="Port for live reload server.", + ) + + parser.add_argument( + "--no-flask", + dest="no_flask", + action="store_true", + default=False, + help="Disable flask server.", + ) + parser.add_argument( + "-fh", + "--flask-host", + dest="flask_host", + type=str, + help="Hostname for flask server.", + ) + parser.add_argument( + "-fp", + "--flask-port", + dest="flask_port", + type=int, + help="Port for flask server.", + ) + parser.add_argument( + "-fm", + "--flask-mode", + dest="flask_mode", + choices=("debug", "no-debug"), + default="debug", + help="If debug mode is enabled, the flask server will be started with --debug flag. Default: debug.", + ) + parser.add_argument( + "--flask-exclude-patterns", + dest="flask_exclude_patterns", + type=str, + nargs="+", + help="File exclude patterns for flask server. Base: */**/dev.py", + ) + + parser.add_argument( + "--no-tailwind", + dest="no_tailwind", + action="store_true", + default=False, + help="Disable tailwindcss generation. If tailwindcss is disabled the live reload server will not be started.", + ) + parser.add_argument( + "-ti", + "--tailwind-input", + dest="tailwind_input", + type=str, + help="Input path for global css file. Includes glob patterns.", + ) + parser.add_argument( + "-to", + "--tailwind-output", + dest="tailwind_output", + type=str, + help="Output path for the generated css file.", + ) + parser.add_argument( + "-tm", + "--tailwind-minify", + dest="tailwind_minify", + action="store_true", + default=False, + help="Enables minification of the generated css file.", + ) + + +def add_command( + subparser: argparse._SubParsersAction[argparse.ArgumentParser], +) -> None: + parser = subparser.add_parser( + name="dev", + description=""" + Extended dev mode for flask apps. + By default runs the flask app in debug mode, + tailwindcss in watch mode and live reload server. + """, + help="Run a development server.", + allow_abbrev=True, + formatter_class=argparse.MetavarTypeHelpFormatter, + ) + + add_command_args(parser) + + +def main(args: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description=""" + Extended dev mode for flask apps. + By default runs the flask app in debug mode, + tailwindcss in watch mode and live reload server. + """, + allow_abbrev=True, + formatter_class=argparse.MetavarTypeHelpFormatter, + ) + + add_command_args(parser) + + parsed_args = parser.parse_args(args) + + return dev(parsed_args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/flask_livetw/initialize.py b/flask_livetw/initialize.py new file mode 100644 index 0000000..fd6a27d --- /dev/null +++ b/flask_livetw/initialize.py @@ -0,0 +1,318 @@ +from __future__ import annotations + +import argparse +import os +import re +from typing import Sequence + +from flask_livetw.config import ( + Config, + ask_project_layout, + update_pyproject_toml, +) +from flask_livetw.util import Term, load_resource, pkgprint + +LIVE_RELOAD_SCRIPT = load_resource("live_reload.js") +TAILWIND_CONFIG = load_resource("tailwind.config.js") +GLOBAL_CSS = load_resource("global.css") +LAYOUT_TEMPLATE = load_resource("layout.html") + + +def generate_tailwind_config(content_glob: str) -> str: + return TAILWIND_CONFIG.content.replace( + "{content_glob_placeholder}", content_glob + ) + + +def add_content_glob(config: str, content_glob: str) -> str | None: + content_glob_re = re.compile(r"content:\s*\[([^\]]*)\]") + re_match = content_glob_re.search(config) + if re_match is None: + return None + + existing_globs = re_match.group(1) + content_start, content_end = re_match.span(1) + prev = config[:content_start] + next = config[content_end:] + if existing_globs.strip() == "": + new_globs = f"'{content_glob}'," + else: + no_new_line = existing_globs.lstrip("\n") + space = " " * (len(no_new_line) - len(no_new_line.lstrip())) + existing_globs = no_new_line.rstrip(", \t\n") + if space == "": + new_globs = f"\n {existing_globs},\n '{content_glob}',\n " + else: + new_globs = f"\n{existing_globs},\n{space}'{content_glob}',\n " + + config = f"{prev}{new_globs}{next}" + config = config.rstrip() + "\n" + return config + + +def generate_live_reload_template( + live_reload_file: str, tailwind_file: str, tailwind_min_file: str +) -> str: + return ( + """ + {% if config.DEBUG %} + + + {% else %} + + {% endif %} +""" + ).strip("\n") + + +def generate_layout_template( + live_reload_file: str, tailwind_file: str, tailwind_min_file: str +) -> str: + return LAYOUT_TEMPLATE.content.replace( + "{live_reload_template_placeholder}", + generate_live_reload_template( + live_reload_file, tailwind_file, tailwind_min_file + ), + ) + + +def configure_tailwind(content_glob: str) -> int: + Term.blank() + pkgprint("Configuring tailwindcss...") + + if os.path.exists(TAILWIND_CONFIG.name): + Term.info("Detected existing configuration file") + Term.info("Updating tailwindcss configuration file...") + + with open(TAILWIND_CONFIG.name, "r") as f: + existing_config = f.read() + + config = add_content_glob(existing_config, content_glob) + if config is None: + Term.info("No content config found in existing tailwind.config.js") + Term.info(f"Manually add '{content_glob}' to your content config.") + return -1 + + with open(TAILWIND_CONFIG.name, "w") as f: + f.write(config) + + pkgprint("Tailwindcss configured") + return 0 + + config = generate_tailwind_config(content_glob) + + with open(TAILWIND_CONFIG.name, "w") as f: + f.write(config) + + pkgprint("Tailwindcss configured") + return 0 + + +def generate_files( + live_reload_file: str, + globalcss_file: str, +) -> None: + Term.blank() + pkgprint("Generating files...") + + try: + with open(live_reload_file, "w") as f: + f.write(LIVE_RELOAD_SCRIPT.content) + except FileNotFoundError: + os.makedirs(os.path.dirname(live_reload_file), exist_ok=True) + with open(live_reload_file, "w") as f: + f.write(LIVE_RELOAD_SCRIPT.content) + + try: + with open(globalcss_file, "w") as f: + f.write(GLOBAL_CSS.content) + except FileNotFoundError: + os.makedirs(os.path.dirname(globalcss_file), exist_ok=True) + with open(globalcss_file, "w") as f: + f.write(GLOBAL_CSS.content) + + pkgprint("Files generated") + + +def update_layout( + root_layout_file: str, + live_reload_file: str, + tailwind_file: str, + tailwind_min_file: str, +) -> int: + Term.blank() + pkgprint("Updating layout...") + + try: + with open(root_layout_file, "+r") as f: + layout = f.read() + if "" not in layout: + Term.error( + "Root layour is malformed, the tag is missing. \ + Please check your root layout file." + ) + return 1 + + layout = layout.replace( + "", + generate_live_reload_template( + live_reload_file, tailwind_file, tailwind_min_file + ) + + "\n", + ) + f.seek(0) + f.write(layout) + f.truncate() + + pkgprint("Root layout file updated") + return 0 + except FileNotFoundError as e: + Term.warn(e) + os.makedirs(os.path.dirname(root_layout_file), exist_ok=True) + with open(root_layout_file, "w") as f: + f.write( + generate_layout_template( + live_reload_file, tailwind_file, tailwind_min_file + ) + ) + + pkgprint("Root layout file created") + return 0 + + +def update_gitignore(static_folder: str, tailwind_file: str) -> None: + Term.blank() + pkgprint("Updating .gitignore...") + + content = f""" +# flask-livetw +{static_folder}/{tailwind_file} +""" + try: + with open(".gitignore", "a") as f: + f.write(content) + except FileNotFoundError: + Term.info("Missing .gitignore file, creating one...") + with open(".gitignore", "w") as f: + f.write(content) + + pkgprint(".gitignore updated") + + +def initialize(config: Config, gitignore: bool) -> int: + Term.blank() + pkgprint("Initializing flask-livetw 🚀...") + + Term.blank() + pkgprint("Updating pyproject.toml...") + code = update_pyproject_toml(config) + if code != 0: + return code + + tailwind_code = configure_tailwind(config.full_templates_glob) + if tailwind_code > 0: + return tailwind_code + + generate_files( + config.full_live_reload_file, + config.full_globalcss_file, + ) + + code = update_layout( + config.full_root_layout_file, + config.live_reload_file, + config.tailwind_file, + config.tailwind_minified_file, + ) + if code != 0: + return code + + if gitignore: + update_gitignore(config.full_static_folder, config.tailwind_file) + + Term.blank() + + if tailwind_code == 0: + pkgprint("Initialization completed ✅") + return 0 + + pkgprint("Initialization almost completed") + pkgprint( + "Remember to add the content glob to your tailwind.config.js manually" + ) + pkgprint(f"Glob: '{config.full_templates_glob}'") + + return 0 + + +def init(cli: argparse.Namespace) -> int: + # project_config = Config.from_pyproject_toml() + + if cli.default: + init_config = Config.default() + else: + pkgprint("Describe your project layout:") + init_config = ask_project_layout() + + return initialize(init_config, gitignore=cli.gitignore) + + +def add_command_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "-D", + "--default", + dest="default", + action="store_true", + default=False, + help="use default values for all options", + ) + + parser.add_argument( + "--gi", + "--gitignore", + dest="gitignore", + action="store_true", + default=False, + help="update .gitignore to exclude dev related files", + ) + + +def add_command( + subparser: argparse._SubParsersAction[argparse.ArgumentParser], +) -> None: + parser = subparser.add_parser( + name="init", + description=""" + Initialize flask-livetw for the project. + Adds the configuration to pyproject.toml and creates the necessary files. + """, + help="Initialize flask-livetw for the project.", + allow_abbrev=True, + formatter_class=argparse.MetavarTypeHelpFormatter, + ) + + add_command_args(parser) + + +def main(args: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Initialize flask-livetw in the current working directory.", + allow_abbrev=True, + formatter_class=argparse.MetavarTypeHelpFormatter, + ) + + add_command_args(parser) + + parsed_args = parser.parse_args(args) + + return init(parsed_args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/flask_livetw/local_install.py b/flask_livetw/local_install.py new file mode 100644 index 0000000..8e532b1 --- /dev/null +++ b/flask_livetw/local_install.py @@ -0,0 +1,387 @@ +from __future__ import annotations + +import argparse +import os +import re +import shlex +import subprocess +from typing import Sequence + +from flask_livetw.config import Config, ask_project_layout +from flask_livetw.util import PKG_PP, Term, load_resource, pkgprint + +DEV_DEPENDENCIES = "pytailwindcss python-dotenv websockets" + + +LIVE_RELOAD_SCRIPT = load_resource("live_reload.js") +DEV_SCRIPT = load_resource("dev.py") +TAILWIND_CONFIG = load_resource("tailwind.config.js") +GLOBAL_CSS = load_resource("global.css") +LAYOUT_TEMPLATE = load_resource("layout.html") + + +def generate_tw_config(content_glob: str) -> str: + return TAILWIND_CONFIG.content.replace( + "{content_glob_placeholder}", content_glob + ) + + +def generate_dev_script( + globalcss_file: str, twcss_file: str, minified_twcss_file: str +) -> str: + return ( + DEV_SCRIPT.content.replace( + "{tailwind_input_placeholder}", globalcss_file + ) + .replace("{tailwind_output_placeholder}", twcss_file) + .replace("{minified_tailwind_output_placeholder}", minified_twcss_file) + ) + + +def generate_live_reload_template( + live_reload_file: str, twcss_file: str, minified_twcss_file: str +) -> str: + return ( + """ + {% if config.DEBUG %} + + + {% else %} + + {% endif %} +""" + ).strip("\n") + + +def generate_layout_template( + live_reload_file: str, twcss_file: str, minified_twcss_file: str +) -> str: + return LAYOUT_TEMPLATE.content.replace( + "{live_reload_template_placeholder}", + generate_live_reload_template( + live_reload_file, twcss_file, minified_twcss_file + ), + ) + + +def install_dev_dependencies() -> int: + Term.blank() + pkgprint("Installing required dev dependencies...") + + poetry_cmd = shlex.split(f"poetry add --group=dev {DEV_DEPENDENCIES}") + + try: + _ = subprocess.run(poetry_cmd, shell=True, check=True) + except subprocess.CalledProcessError as e: + Term.error(e) + Term.info( + "Dev dependencies installation failed, please install them manually" # noqa: E501 + ) + return -1 + + pkgprint("Dev dependencies installed") + return 0 + + +def configure_tailwind(content_glob: str) -> int: + Term.blank() + pkgprint("Configuring tailwindcss...") + + if os.path.exists(TAILWIND_CONFIG.name): + Term.info("Detected existing configuration file") + Term.info("Updating tailwindcss configuration file...") + + with open(TAILWIND_CONFIG.name, "r+t") as f: + config = f.read() + content_glob_re = re.compile(r"content:\s*\[([^\]]*)\]") + re_match = content_glob_re.search(config) + if re_match is None: + Term.info( + "No content config found in existing tailwind.config.js" + ) + Term.info("Add the following glob to the content config:") + Term.info(f"'{content_glob}',") + return -1 + + existing_globs = re_match.group(1) + content_start, content_end = re_match.span(1) + prev = config[:content_start] + next = config[content_end:] + if existing_globs.strip() == "": + new_globs = f"'{content_glob}'," + else: + no_new_line = existing_globs.lstrip("\n") + space = " " * (len(no_new_line) - len(no_new_line.lstrip())) + existing_globs = no_new_line.rstrip(", \t\n") + if space == "": + new_globs = ( + f"\n {existing_globs},\n '{content_glob}',\n " + ) + else: + new_globs = ( + f"\n{existing_globs},\n{space}'{content_glob}',\n " + ) + + config = f"{prev}{new_globs}{next}" + config = config.rstrip() + "\n" + f.seek(0) + f.write(config) + f.truncate() + + Term.info("Tailwindcss configuration file updated") + pkgprint("Tailwindcss configured") + return 0 + + with open(TAILWIND_CONFIG.name, "w") as f: + f.write(generate_tw_config(content_glob)) + + Term.info("Tailwindcss configuration file created") + pkgprint("Tailwindcss configuration complete") + return 0 + + +def generate_files( + live_reload_file: str, + globalcss_file: str, + twcss_file: str, + minified_twcss_file: str, +) -> None: + with open(DEV_SCRIPT.name, "w") as f: + f.write( + generate_dev_script( + globalcss_file, twcss_file, minified_twcss_file + ) + ) + + try: + with open(live_reload_file, "w") as f: + f.write(LIVE_RELOAD_SCRIPT.content) + except FileNotFoundError: + os.makedirs(os.path.dirname(live_reload_file), exist_ok=True) + with open(live_reload_file, "w") as f: + f.write(LIVE_RELOAD_SCRIPT.content) + + with open(globalcss_file, "w") as f: + f.write(GLOBAL_CSS.content) + + +def update_layout( + root_layout_file: str, + live_reload_file: str, + twcss_file: str, + minified_twcss_file: str, +) -> int: + try: + with open(root_layout_file, "+r") as f: + layout = f.read() + if "" not in layout: + Term.error( + "Root layour is malformed, the tag is missing. \ + Please check your root layout file." + ) + return 1 + + layout = layout.replace( + "", + generate_live_reload_template( + live_reload_file, twcss_file, minified_twcss_file + ) + + "\n", + ) + f.seek(0) + f.write(layout) + f.truncate() + return 0 + except FileNotFoundError as e: + Term.warn(e) + os.makedirs(os.path.dirname(root_layout_file), exist_ok=True) + with open(root_layout_file, "w") as f: + f.write( + generate_layout_template( + live_reload_file, twcss_file, minified_twcss_file + ) + ) + + return 0 + + +def update_gitignore(static_folder: str, twcss_file: str) -> None: + content = f""" +# flask-livetw +{static_folder}/{twcss_file} +""" + try: + with open(".gitignore", "a") as f: + f.write(content) + except FileNotFoundError: + Term.info("Missing .gitignore file, creating one...") + with open(".gitignore", "w") as f: + f.write(content) + + +def check_requirements() -> int: + pkgprint("Checking requirements...") + cwd = os.getcwd() + pkgprint(f"Current working directory: {Term.C}{cwd}{Term.END}") + continue_install = Term.confirm(f"{PKG_PP} Is this your project root?") + + if not continue_install: + pkgprint("Change cwd and start again. Modding canceled") + return 1 + + python_cmd = shlex.split("python --version") + python_cmd_result = subprocess.run( + python_cmd, shell=True, capture_output=True + ) + + if python_cmd_result.returncode != 0: + Term.error("python --version failed, is python installed?") + return python_cmd_result.returncode + + version = python_cmd_result.stdout.decode("utf-8").strip() + if version != "Python 3.8.10": + pkgprint(f"python --version: {Term.C}{version}{Term.END}") + + continue_install = Term.confirm( + f"{PKG_PP} Continue with this version?" + ) + if not continue_install: + pkgprint("Change python version and start again. Modding canceled") + return 1 + + return 0 + + +def local_install(args: argparse.Namespace) -> int: + if not args.all_yes: + code = check_requirements() + if code != 0: + return code + + config = Config.try_from_pyproject_toml() + if config is None: + config = ask_project_layout() + + Term.blank() + pkgprint("Installing flask-livetw as local script 🚀...") + + dependancies_code = install_dev_dependencies() + if dependancies_code > 0: + return dependancies_code + + tailwind_code = configure_tailwind(config.full_templates_glob) + if tailwind_code > 0: + return tailwind_code + + generate_files( + config.full_live_reload_file, + config.full_globalcss_file, + config.full_tailwind_file, + config.full_tailwind_minified_file, + ) + + code = update_layout( + config.full_root_layout_file, + config.live_reload_file, + config.full_tailwind_file, + config.full_tailwind_minified_file, + ) + if code != 0: + return code + + if args.gitignore: + update_gitignore(config.full_static_folder, config.tailwind_file) + + Term.blank() + + if dependancies_code == 0 and tailwind_code == 0: + pkgprint("Local install completed 🎉") + return 0 + + pkgprint("Local install almost completed") + + if dependancies_code != 0: + pkgprint("Remember to install the missing dev dependencies manually") + pkgprint(f"Dependancies: {DEV_DEPENDENCIES}") + + if tailwind_code != 0: + pkgprint( + "Remember to add the content glob to your tailwind.config.js manually" + ) + pkgprint(f"Glob: '{config.full_templates_glob}'") + + return 0 + + +def add_command_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + "-Y", + "--yes", + dest="all_yes", + action="store_true", + default=False, + help="answer yes to all requirements checks", + ) + + parser.add_argument( + "-D", + "--default", + dest="default", + action="store_true", + default=False, + help="use default values for all options", + ) + + parser.add_argument( + "--gi", + "--gitignore", + dest="gitignore", + action="store_true", + default=False, + help="update .gitignore to exclude dev related files", + ) + + +def add_command( + subparser: argparse._SubParsersAction[argparse.ArgumentParser], +) -> None: + parser = subparser.add_parser( + name="local-install", + description=""" + Install flask-livetw as a local script + (adds dev dependencies, configures tailwindcss, + adds dev script and updates root layout file). + """, + help="Install flask-livetw as a local script.", + allow_abbrev=True, + formatter_class=argparse.MetavarTypeHelpFormatter, + ) + + add_command_args(parser) + + +def main(args: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description=""" + Install flask-livetw as a local script + (adds dev dependencies, configures tailwindcss, + adds dev script and updates root layout file). + """, + allow_abbrev=True, + formatter_class=argparse.MetavarTypeHelpFormatter, + ) + add_command_args(parser) + + parsed_args = parser.parse_args(args) + + return local_install(parsed_args) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/flask_livetw/main.py b/flask_livetw/main.py index dae9ce4..b6c1066 100644 --- a/flask_livetw/main.py +++ b/flask_livetw/main.py @@ -1,289 +1,48 @@ -#!/usr/bin/env python +from __future__ import annotations -import os -import re -import shlex -import subprocess +import argparse +from typing import Sequence -from flask_livetw.cli import ( - check_requirements, - create_cli, - get_config, - pkgprint, -) -from flask_livetw.util import Term, load_resource +from flask_livetw import build_app, dev_server, initialize, local_install -DEV_DEPENDENCIES = "pytailwindcss python-dotenv websockets" - -LIVE_RELOAD_SCRIPT = load_resource("live_reload.js") -DEV_SCRIPT = load_resource("dev.py") -TAILWIND_CONFIG = load_resource("tailwind.config.js") -GLOBAL_CSS = load_resource("global.css") -LAYOUT_TEMPLATE = load_resource("layout.html") - - -def generate_tw_config(content_glob: str) -> str: - return TAILWIND_CONFIG.content.replace( - "{content_glob_placeholder}", content_glob +def create_cli() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="flask-livetw", + description="CLI for flask-livetw commands.", + allow_abbrev=True, ) - - -def generate_dev_script( - globalcss_file: str, twcss_file: str, minified_twcss_file: str -) -> str: - return ( - DEV_SCRIPT.content.replace( - "{tailwind_input_placeholder}", globalcss_file - ) - .replace("{tailwind_output_placeholder}", twcss_file) - .replace("{minified_tailwind_output_placeholder}", minified_twcss_file) + subparsers = parser.add_subparsers( + title="commands", + dest="command", + required=True, ) + dev_server.add_command(subparsers) -def generate_live_reload_template( - live_reload_file: str, twcss_file: str, minified_twcss_file: str -) -> str: - return ( - """ - {% if config.DEBUG %} - - - {% else %} - - {% endif %} -""" - ).strip("\n") - - -def generate_layout_template( - live_reload_file: str, twcss_file: str, minified_twcss_file: str -) -> str: - return LAYOUT_TEMPLATE.content.replace( - "{live_reload_template_placeholder}", - generate_live_reload_template( - live_reload_file, twcss_file, minified_twcss_file - ), - ) - - -def install_dev_dependencies() -> int: - Term.blank() - pkgprint("Installing required dev dependencies...") - - poetry_cmd = shlex.split(f"poetry add --group=dev {DEV_DEPENDENCIES}") + build_app.add_command(subparsers) - try: - _ = subprocess.run(poetry_cmd, shell=True, check=True) - except subprocess.CalledProcessError as e: - Term.error(e) - Term.info( - "Dev dependencies installation failed, please install them manually" # noqa: E501 - ) - return -1 + initialize.add_command(subparsers) - pkgprint("Dev dependencies installed") - return 0 - - -def configure_tailwind(content_glob: str) -> int: - Term.blank() - pkgprint("Configuring tailwindcss...") - - if os.path.exists(TAILWIND_CONFIG.name): - Term.info("Detected existing configuration file") - Term.info("Updating tailwindcss configuration file...") - - with open(TAILWIND_CONFIG.name, "r+t") as f: - config = f.read() - content_glob_re = re.compile(r"content:\s*\[([^\]]*)\]") - re_match = content_glob_re.search(config) - if re_match is None: - Term.info( - "No content config found in existing tailwind.config.js" - ) - Term.info("Add the following glob to the content config:") - Term.info(f"'{content_glob}',") - return -1 - - existing_globs = re_match.group(1) - content_start, content_end = re_match.span(1) - prev = config[:content_start] - next = config[content_end:] - if existing_globs.strip() == "": - new_globs = f"'{content_glob}'," - else: - no_new_line = existing_globs.lstrip("\n") - space = " " * (len(no_new_line) - len(no_new_line.lstrip())) - existing_globs = no_new_line.rstrip(", \t\n") - if space == "": - new_globs = ( - f"\n {existing_globs},\n '{content_glob}',\n " - ) - else: - new_globs = ( - f"\n{existing_globs},\n{space}'{content_glob}',\n " - ) - - config = f"{prev}{new_globs}{next}" - config = config.rstrip() + "\n" - f.seek(0) - f.write(config) - f.truncate() - - Term.info("Tailwindcss configuration file updated") - pkgprint("Tailwindcss configured") - return 0 - - with open(TAILWIND_CONFIG.name, "w") as f: - f.write(generate_tw_config(content_glob)) - - Term.info("Tailwindcss configuration file created") - pkgprint("Tailwindcss configuration complete") - return 0 + local_install.add_command(subparsers) + return parser -def generate_files( - live_reload_file: str, - globalcss_file: str, - twcss_file: str, - minified_twcss_file: str, -) -> None: - with open(DEV_SCRIPT.name, "w") as f: - f.write( - generate_dev_script( - globalcss_file, twcss_file, minified_twcss_file - ) - ) - - try: - with open(live_reload_file, "w") as f: - f.write(LIVE_RELOAD_SCRIPT.content) - except FileNotFoundError: - os.makedirs(os.path.dirname(live_reload_file), exist_ok=True) - with open(live_reload_file, "w") as f: - f.write(LIVE_RELOAD_SCRIPT.content) - - with open(globalcss_file, "w") as f: - f.write(GLOBAL_CSS.content) - - -def update_layout( - root_layout_file: str, - live_reload_file: str, - twcss_file: str, - minified_twcss_file: str, -) -> int: - try: - with open(root_layout_file, "+r") as f: - layout = f.read() - if "" not in layout: - Term.error( - "Root layour is malformed, the tag is missing. \ - Please check your root layout file." - ) - return 1 - - layout = layout.replace( - "", - generate_live_reload_template( - live_reload_file, twcss_file, minified_twcss_file - ) - + "\n", - ) - f.seek(0) - f.write(layout) - f.truncate() - return 0 - except FileNotFoundError as e: - Term.warn(e) - os.makedirs(os.path.dirname(root_layout_file), exist_ok=True) - with open(root_layout_file, "w") as f: - f.write( - generate_layout_template( - live_reload_file, twcss_file, minified_twcss_file - ) - ) - - return 0 - - -def update_gitignore(static_folder: str, twcss_file: str) -> None: - content = f""" -# flask-livetw -{static_folder}/{twcss_file} -""" - try: - with open(".gitignore", "a") as f: - f.write(content) - except FileNotFoundError: - Term.info("Missing .gitignore file, creating one...") - with open(".gitignore", "w") as f: - f.write(content) - - -def main() -> int: - cli_args = create_cli().parse_args() - - if not cli_args.all_yes: - code = check_requirements() - if code != 0: - return code - - config = get_config(cli_args) - - pkgprint("Modding your project... 🚀") - - dependancies_code = install_dev_dependencies() - if dependancies_code > 0: - return dependancies_code - - tailwind_code = configure_tailwind(config.full_templates_glob) - if tailwind_code > 0: - return tailwind_code - - generate_files( - config.full_live_reload_file, - config.full_globalcss_file, - config.full_twcss_file, - config.full_minified_twcss_file, - ) - - code = update_layout( - config.full_root_layout_file, - config.live_reload_file, - config.twcss_file, - config.minified_twcss_file, - ) - if code != 0: - return code - - if config.gitignore: - update_gitignore(config.full_static_folder, config.twcss_file) - Term.blank() +def main(args: Sequence[str] | None = None) -> int: + parsed_args = create_cli().parse_args(args) - if dependancies_code == 0 and tailwind_code == 0: - pkgprint("Modding complete ✅") - return 0 + if parsed_args.command == "dev": + return dev_server.dev(parsed_args) - pkgprint("Modding almost completed") + if parsed_args.command == "build": + return build_app.build(parsed_args) - if dependancies_code != 0: - pkgprint("Remember to install the missing dev dependencies manually") - pkgprint(f"Dependancies: {DEV_DEPENDENCIES}") + if parsed_args.command == "init": + return initialize.init(parsed_args) - if tailwind_code != 0: - pkgprint( - "Remember to add the content glob to your tailwind.config.js manually" - ) - pkgprint(f"Glob: '{config.full_templates_glob}'") + if parsed_args.command == "local-install": + return local_install.local_install(parsed_args) return 0 diff --git a/flask_livetw/static/dev.py b/flask_livetw/resources/dev.py similarity index 99% rename from flask_livetw/static/dev.py rename to flask_livetw/resources/dev.py index 6e8b8fa..e2d6fe2 100644 --- a/flask_livetw/static/dev.py +++ b/flask_livetw/resources/dev.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +from __future__ import annotations import argparse import asyncio diff --git a/flask_livetw/static/global.css b/flask_livetw/resources/global.css similarity index 100% rename from flask_livetw/static/global.css rename to flask_livetw/resources/global.css diff --git a/flask_livetw/static/layout.html b/flask_livetw/resources/layout.html similarity index 100% rename from flask_livetw/static/layout.html rename to flask_livetw/resources/layout.html diff --git a/flask_livetw/static/live_reload.js b/flask_livetw/resources/live_reload.js similarity index 100% rename from flask_livetw/static/live_reload.js rename to flask_livetw/resources/live_reload.js diff --git a/flask_livetw/static/tailwind.config.js b/flask_livetw/resources/tailwind.config.js similarity index 100% rename from flask_livetw/static/tailwind.config.js rename to flask_livetw/resources/tailwind.config.js diff --git a/flask_livetw/util.py b/flask_livetw/util.py index 5a7b8e1..a30d7bf 100644 --- a/flask_livetw/util.py +++ b/flask_livetw/util.py @@ -1,10 +1,14 @@ +from __future__ import annotations + import dataclasses import os import platform from typing import Callable, Union +PKG_PPN = "livetw" + STATIC_PATH = os.path.join( - os.path.dirname(os.path.realpath(__file__)), "static" + os.path.dirname(os.path.realpath(__file__)), "resources" ) @@ -122,3 +126,10 @@ def ask_dir( Term.error(f"'{full_path}' is not a dir") dir = input(message).strip() + + +PKG_PP = f"{Term.M}[{PKG_PPN}]{Term.END}" + + +def pkgprint(*values: object, end: str = "\n", sep: str = " ") -> None: + print(f"{PKG_PP}", *values, end=end, sep=sep) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 74f4afe..0000000 --- a/poetry.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. -package = [] - -[metadata] -lock-version = "2.0" -python-versions = "^3.8.10" -content-hash = "14b1c8005ea3066a881421bbb7ded56abea61e98ad9228d3340cd85898765255" diff --git a/pyproject.toml b/pyproject.toml index e03b9e2..8d293f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "flask-livetw" -version = "0.2.0" -description = "A simple package that enables live tailwindcss reloading for a template based app in flask" +version = "0.3.0" +description = "A simple package that enables livereload and tailwindcss for flask templates." license = "MIT" authors = ["J-Josu "] readme = "README.md" @@ -25,8 +25,26 @@ flask-livetw = "flask_livetw.main:main" [tool.poetry.dependencies] python = "^3.8.10" +pytailwindcss = "^0.2.0" +websockets = "^11.0.3" +tomli = "^2.0.1" + + +[tool.poetry.group.dev.dependencies] +pre-commit = "^3.4.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + + +[tool.black] +line-length = 79 + +[tool.isort] +profile = "black" +line_length = 79 + +[tool.pyright] +reportPrivateUsage = false