From bc5ade2154e12f9be77da0e910475ab4b4178c91 Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Fri, 4 Oct 2024 16:20:59 +0400 Subject: [PATCH] chore: use dynamic version field in `pyproject.toml` based on `hatch.build.hooks.vcs` and publish dev packages on pypi for all pushes to the main branch and all pull requests targeting the main branch --- .github/workflows/integration_delivery.yml | 74 ++++++++++------------ .gitignore | 3 + CHANGELOG.md | 2 + pyproject.toml | 16 ++++- setup_scm_schemes.py | 11 ++++ ubo_app/logging.py | 35 +++++++++- 6 files changed, 94 insertions(+), 47 deletions(-) create mode 100644 setup_scm_schemes.py diff --git a/.github/workflows/integration_delivery.yml b/.github/workflows/integration_delivery.yml index bad92462..d55f1449 100644 --- a/.github/workflows/integration_delivery.yml +++ b/.github/workflows/integration_delivery.yml @@ -25,7 +25,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - architecture: x64 - name: Install uv uses: astral-sh/setup-uv@v3 @@ -109,7 +108,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - architecture: x64 - name: Install uv uses: astral-sh/setup-uv@v3 @@ -117,12 +115,8 @@ jobs: enable-cache: true cache-suffix: ubuntu-latest - - name: Create virtualenv - run: | - uv venv --system-site-packages - - name: Lint - run: uv run --frozen poe lint + run: uvx ruff check test: name: Test @@ -151,7 +145,6 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ env.PYTHON_VERSION }} - architecture: x64 - name: Install uv uses: astral-sh/setup-uv@v3 @@ -215,7 +208,6 @@ jobs: name: Setup Python with: python-version: ${{ env.PYTHON_VERSION }} - architecture: x64 - name: Install uv uses: astral-sh/setup-uv@v3 @@ -223,39 +215,35 @@ jobs: enable-cache: true cache-suffix: ubuntu-latest - - name: Create virtualenv - run: | - uv venv --system-site-packages - - name: Extract Version id: extract-version run: | - echo "VERSION=$(uv run --frozen python -c 'from importlib.metadata import version; print(version("ubo_app"))')" >> "$GITHUB_OUTPUT" - echo "VERSION=$(uv run --frozen python -c 'from importlib.metadata import version; print(version("ubo_app"))')" - echo "NAME=$(uv run --frozen python -c 'from importlib.metadata import metadata; print(metadata("ubo_app")["Name"])')" >> "$GITHUB_OUTPUT" - echo "NAME=$(uv run --frozen python -c 'from importlib.metadata import metadata; print(metadata("ubo_app")["Name"])')" + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + echo "VERSION=$(uvx hatch version)" >> "$GITHUB_OUTPUT" + echo "VERSION=$(uvx hatch version)" + echo "NAME=$(uvx hatch project metadata | jq -r .name)" >> "$GITHUB_OUTPUT" + echo "NAME=$(uvx hatch project metadata | jq -r .name)" - name: Extract Version from CHANGELOG.md run: | - VERSION_CHANGELOG=$(sed -n '/## Version /s/## Version //p' CHANGELOG.md | head -n 1) - echo "VERSION_CHANGELOG=$VERSION_CHANGELOG" - if [ "${{ steps.extract-version.outputs.VERSION }}" != "$VERSION_CHANGELOG" ]; then - echo "Error: Version extracted from CHANGELOG.md does not match the version in pyproject.toml" - exit 1 - else - echo "Versions are consistent." - fi - - - name: Extract Version from Tag - if: startsWith(github.ref, 'refs/tags/v') - run: | - VERSION_TAG=$(sed 's/^v//' <<< ${{ github.ref_name }}) - echo "VERSION_TAG=$VERSION_TAG" - if [ "${{ steps.extract-version.outputs.VERSION }}" != "$VERSION_TAG" ]; then - echo "Error: Version extracted from tag does not match the version in pyproject.toml" - exit 1 + FIRST_HEADER=$(sed -n '/## /s/## //p' CHANGELOG.md | head -n 1) + if [ "$FIRST_HEADER" == "Upcoming" ]; then + # Check the version coming from extract-version starts with of x.y.z.devn + if [[ "${{ steps.extract-version.outputs.VERSION }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+ ]]; then + VERSION_CHANGELOG="This is a development version." + else + echo "Error: First header in CHANGELOG.md is 'Upcoming' but the version in pyproject.toml is not a development version." + exit 1 + fi else - echo "Versions are consistent." + VERSION_CHANGELOG=$(echo $FIRST_HEADER | sed 's/Version //') + if [ "${{ steps.extract-version.outputs.VERSION }}" != "$VERSION_CHANGELOG" ]; then + echo "Error: Version extracted from CHANGELOG.md does not match the version in pyproject.toml" + exit 1 + else + echo "Versions are consistent." + fi fi - name: Configure Sentry @@ -270,7 +258,9 @@ jobs: cat ubo_app/.env - name: Build - run: uv build + run: + SETUPTOOLS_SCM_PRETEND_VERSION=${{ + steps.extract-version.outputs.VERSION }} uv build - name: Upload wheel uses: actions/upload-artifact@v4 @@ -379,10 +369,11 @@ jobs: steps.generate-image-url.outputs.dashed_suffix }}.img.gz if-no-files-found: error - pypi-publish: - name: Publish to PyPI + publish: + name: Publish if: >- - github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + github.event_name == 'push' && github.ref == 'refs/heads/main' || + github.event_name == 'pull_request' && github.head_ref == 'main' needs: - type-check - lint @@ -406,11 +397,10 @@ jobs: name: binary path: dist - - name: Publish package distributions to PyPI + - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist - verbose: true release: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') @@ -420,7 +410,7 @@ jobs: - lint - test - build - - pypi-publish + - publish - images runs-on: ubuntu-latest environment: diff --git a/.gitignore b/.gitignore index af2691c1..8263c1ea 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,9 @@ headless_kivy_pi_buffer.raw scripts/packer/packer_cache/ scripts/packer/output-* +# hatch +ubo_app/_version.py + /screenshots /snapshot.json /snapshot.bin diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cbd2835..66a773bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## Upcoming - chore: migrate from poetry to uv for the sake of improving performance and dealing with conflicting sub-dependencies +- feat(core): add colors to logs based on their level to make them more readable +- chore: use dynamic version field in `pyproject.toml` based on `hatch.build.hooks.vcs` and publish dev packages on pypi for all pushes to the main branch and all pull requests targeting the main branch ## Version 0.17.1 diff --git a/pyproject.toml b/pyproject.toml index 1358a8c7..7ea53a0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ubo-app" -version = "0.17.1" +dynamic = ["version"] description = "Ubo main app, running on device initialization. A platform for running other apps." license = { text = "Apache-2.0" } readme = "README.md" @@ -42,9 +42,18 @@ dependencies = [ ] [build-system] -requires = ["hatchling"] +requires = ["hatchling", "hatch-vcs"] build-backend = "hatchling.build" +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.hooks.vcs] +version-file = "ubo_app/_version.py" + +[tool.hatch.version.raw-options] +local_scheme = "setup_scm_schemes:local_scheme" + [tool.uv] dev-dependencies = [ "headless-kivy [test] >=0.9.8", @@ -128,6 +137,7 @@ cmd = "scripts/deploy.sh" [tool.ruff] target-version = 'py311' +extend-exclude = ["setup_scm_schemes.py"] [tool.ruff.lint] select = ["ALL"] @@ -169,7 +179,7 @@ quote-style = "single" profile = "black" [tool.pyright] -exclude = ["typings", "ubo_app/rpc/generated", ".venv"] +exclude = ["typings", "ubo_app/rpc/generated", ".venv", "setup_scm_schemes.py"] [[tool.pyright.executionEnvironments]] root = "ubo_app/services/000-audio" diff --git a/setup_scm_schemes.py b/setup_scm_schemes.py new file mode 100644 index 00000000..6bef090b --- /dev/null +++ b/setup_scm_schemes.py @@ -0,0 +1,11 @@ +from setuptools_scm.version import get_local_node_and_date +import re + + +def local_scheme(version): + version.node = re.sub( + r'.', + lambda match: str(ord(match.group(0))), + version.node + ) + return get_local_node_and_date(version).replace('+', '').replace('.d', '') diff --git a/ubo_app/logging.py b/ubo_app/logging.py index b877523b..bb32c1fc 100644 --- a/ubo_app/logging.py +++ b/ubo_app/logging.py @@ -5,8 +5,9 @@ import json import logging import logging.handlers +import os import sys -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, ClassVar, cast if TYPE_CHECKING: from collections.abc import Mapping @@ -69,6 +70,12 @@ def get_logger(name: str) -> UboLogger: return cast(UboLogger, logging.getLogger(name)) +def supports_truecolor() -> bool: + """Check if the terminal supports truecolor (24-bit).""" + color_term = os.environ.get('COLORTERM', '') + return color_term == 'truecolor' + + logger = get_logger('ubo-app') logger.propagate = False @@ -100,9 +107,28 @@ class ExtraFormatter(logging.Formatter): 'message', ) + COLORS: ClassVar[dict[int, str]] = { + VERBOSE: '\033[38;2;100;100;100m', # Dark Gray (RGB) + logging.DEBUG: '\033[38;2;150;150;150m', # Light Gray (RGB) + logging.INFO: '\033[38;2;100;200;100m', # Green (RGB) + logging.WARNING: '\033[38;2;255;165;0m', # Orange (RGB) + logging.ERROR: '\033[38;2;255;0;0m', # Red (RGB) + logging.CRITICAL: '\033[38;2;255;0;255m', # Magenta (RGB) + } + ANSI_COLORS: ClassVar[dict[int, str]] = { + VERBOSE: '\033[90m', # Gray (ANSI) + logging.DEBUG: '\033[94m', # Blue (ANSI) + logging.INFO: '\033[92m', # Green (ANSI) + logging.WARNING: '\033[93m', # Yellow (ANSI) + logging.ERROR: '\033[91m', # Red (ANSI) + logging.CRITICAL: '\033[95m', # Magenta (ANSI) + } + RESET = '\033[0m' # Reset color + def format(self: ExtraFormatter, record: logging.LogRecord) -> str: string = super().format(record) extra = {k: v for k, v in record.__dict__.items() if k not in self.def_keys} + if len(extra) > 0: string += ' - extra: ' + json.dumps( handle_circular_references(extra), @@ -114,7 +140,12 @@ def format(self: ExtraFormatter, record: logging.LogRecord) -> str: if self.max_length and len(string) > self.max_length: string = string[: self.max_length - 3] + '...' - return string + # Get the color for the log level and apply it + color = (self.COLORS if supports_truecolor() else self.ANSI_COLORS).get( + record.levelno, + self.RESET, + ) + return f'{color}{string}{self.RESET}' def add_stdout_handler(logger: UboLogger, level: int = logging.DEBUG) -> None: