From 0e58d7339c543d8be28dd49698ccdcaf87bd844b Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 2 Sep 2024 00:25:36 +0200 Subject: [PATCH 1/9] Move to toml setup, fix dependabot alert --- .github/workflows/build.yml | 47 +++++------ .github/workflows/publish.yml | 144 +++++++++++++++++++++++++++++----- brickmos/__about__.py | 1 + pyproject.toml | 57 ++++++++++++++ requirements.txt | 1 - setup.py | 33 -------- 6 files changed, 202 insertions(+), 81 deletions(-) create mode 100644 brickmos/__about__.py create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08f321c..55169cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,30 +3,25 @@ name: build on: [push] jobs: - build: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.6, 3.7, 3.8, 3.9] - - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - pytest + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: lint with ruff + run: ruff check --output-format=github . + - name: Test with pytest + run: | + pytest diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 15b6f86..6f0d514 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,26 +1,128 @@ name: publish on: - release: - types: [created] + workflow_dispatch: + inputs: + VERSION: + description: "The version to release" + required: true + IS_PRE_RELEASE: + description: "It IS a pre-release" + required: true + default: false + type: boolean jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + bump: # This job is used to bump the version and create a release + runs-on: ubuntu-latest + env: + VERSION: ${{ inputs.VERSION }} + GH_TOKEN: ${{ github.token }} + SSH_AUTH_SOCK: /tmp/ssh_agent.sock + permissions: + contents: write + steps: + - name: set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: install dependencies + run: | + pip install --upgrade pip + pip install build hatch + + - name: configure git with the bot credentials + run: | + mkdir -p ~/.ssh + ssh-keyscan github.com >> ~/.ssh/known_hosts + ssh-agent -a $SSH_AUTH_SOCK > /dev/null + ssh-add - <<< "${{ secrets.BOT_SSH_KEY }}" + + echo "${{ secrets.BOT_SIGNING_KEY }}" > ~/.ssh/signing.key + chmod 600 ~/.ssh/signing.key + + git config --global user.name "Merschbotmann" + git config --global user.email "bot.merschformann@gmail.com" + git config --global gpg.format ssh + git config --global user.signingkey ~/.ssh/signing.key + + git clone git@github.com:merschformann/brickmos.git + + - name: upgrade version with hatch + run: hatch version ${{ env.VERSION }} + working-directory: ./brickmos + + - name: commit new version + run: | + git add brickmos/__about__.py + git commit -S -m "Bump version to $VERSION" + git push + git tag $VERSION + git push origin $VERSION + working-directory: ./brickmos + + - name: create release + run: | + PRERELEASE_FLAG="" + if [ ${{ inputs.IS_PRE_RELEASE }} = true ]; then + PRERELEASE_FLAG="--prerelease" + fi + + gh release create $VERSION \ + --verify-tag \ + --generate-notes \ + --title $VERSION $PRERELEASE_FLAG + working-directory: ./brickmos + + - name: ensure passing build + run: python -m build + working-directory: ./brickmos + + publish: # This job is used to publish the release to PyPI/TestPyPI + runs-on: ubuntu-latest + needs: bump + strategy: + matrix: + include: + - target-env: pypi + target-url: https://pypi.org/p/brickmos + - target-env: testpypi + target-url: https://test.pypi.org/p/brickmos + environment: + name: ${{ matrix.target-env }} + url: ${{ matrix.target-url }} + permissions: + contents: read + id-token: write # This is required for trusted publishing to PyPI + steps: + - name: git clone main + uses: actions/checkout@v4 + with: + ref: main + + - name: set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: install dependencies + run: | + pip install --upgrade pip + pip install build hatch + + - name: build binary wheel and source tarball + run: python -m build + + - name: Publish package distributions to PyPI + if: ${{ matrix.target-env == 'pypi' }} + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: ./dist + + - name: Publish package distributions to TestPyPI + if: ${{ matrix.target-env == 'testpypi' }} + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: ./dist diff --git a/brickmos/__about__.py b/brickmos/__about__.py new file mode 100644 index 0000000..14b43dc --- /dev/null +++ b/brickmos/__about__.py @@ -0,0 +1 @@ +__version__ = "v0.0.4" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..003831a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,57 @@ +[build-system] +build-backend = "hatchling.build" +requires = ["hatchling >= 1.13.0"] + +[project] +authors = [ + { email = "marius.merschformann@gmail.com", name = "Marius Merschformann" } +] +maintainers = [ + { email = "marius.merschformann@gmail.com", name = "Marius Merschformann" } +] +classifiers = [ + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", +] +dependencies = [ + "opencv-python>=4.8.1.78", +] +description = "brickmos is a humble Lego mosaic generator." +dynamic = [ + "version", +] +keywords = [ + "lego", + "mosaic", + "bricklink", +] +license = { file = "LICENSE" } +name = "brickmos" +readme = "README.md" +requires-python = ">=3.10" + +[project.urls] +Homepage = "https://github.com/merschformann/brickmos" +Repository = "https://github.com/merschformann/brickmos" + +[project.scripts] +brickmos = "brickmos.brickify:main" + +[tool.ruff] +target-version = "py312" +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear + "UP", # pyupgrade +] +line-length = 120 +[tool.ruff.lint.mccabe] +max-complexity = 15 + +[tool.hatch.version] +path = "brickmos/__about__.py" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 97cf07f..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -opencv-python~=4.4.0.44 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 913a601..0000000 --- a/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -import setuptools - -# read the contents of README -from os import path - -current_dir = path.abspath(path.dirname(__file__)) -with open(path.join(current_dir, "README.md"), encoding="utf-8") as f: - long_description = f.read() - - -setuptools.setup( - name="brickmos", - description="brickmos - A simple brick mosaic generator", - long_description=long_description, - long_description_content_type="text/markdown", - version="0.0.3", - author="Marius Merschformann", - author_email="marius.merschformann@gmail.com", - url="https://github.com/merschformann/brickmos", - packages=setuptools.find_packages(), - install_requires=[ - "opencv-python~=4.4.0.44", - ], - entry_points={ - "console_scripts": ["brickmos=brickmos.brickify:main"], - }, - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Environment :: Console", - ], -) From 602e881a06f1b15c16fda9fc5381b4da617027a0 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 2 Sep 2024 00:28:48 +0200 Subject: [PATCH 2/9] Move to newer python versions --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 55169cf..bd30a6d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.9, 3.10, 3.11, 3.12] steps: - uses: actions/checkout@v4 From 749eb6bcde1cc02a8d7a8d635793cee97dd0e6bf Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 2 Sep 2024 00:29:49 +0200 Subject: [PATCH 3/9] Use string versions --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd30a6d..bd0dec8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.9, 3.10, 3.11, 3.12] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 From d77be4052cdd8152bb9ec6a726187e965a534f61 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 2 Sep 2024 00:34:38 +0200 Subject: [PATCH 4/9] Adhering to new linter ruff --- brickmos/brickify.py | 42 +++++++++++++++--------------------------- tests/test_cli.py | 15 ++++++--------- 2 files changed, 21 insertions(+), 36 deletions(-) diff --git a/brickmos/brickify.py b/brickmos/brickify.py index 87f635e..5bd2537 100644 --- a/brickmos/brickify.py +++ b/brickmos/brickify.py @@ -1,13 +1,13 @@ -from . import defaults import argparse import csv -import cv2 import math import os import sys -from typing import Tuple, List -import xml.etree.cElementTree as xmlET +import xml.etree.ElementTree as xmlET + +import cv2 +from . import defaults # ===> Constants @@ -111,24 +111,20 @@ def init_and_get_arguments(): sys.exit(-2) # Get image size try: - width, height = [int(a) for a in args.size.split("x")] + width, height = (int(a) for a in args.size.split("x")) args.width = width args.height = height except Exception: - print( - f"Cannot parse --size argument. Got {args.size}, but wanted something like 48x48" - ) + print(f"Cannot parse --size argument. Got {args.size}, but wanted something like 48x48") sys.exit(-3) # Get helper grid cell size if args.grid_cell != NO_GRID: try: - width, height = [int(a) for a in args.grid_cell.split("x")] + width, height = (int(a) for a in args.grid_cell.split("x")) args.grid_cell_width = width args.grid_cell_height = height except Exception: - print( - f"Cannot parse --grid_cell argument. Got {args.grid_cell}, but wanted something like 8x8" - ) + print(f"Cannot parse --grid_cell argument. Got {args.grid_cell}, but wanted something like 8x8") sys.exit(-4) # Get helper grid size # Create output directory, if it does not exist @@ -139,7 +135,7 @@ def init_and_get_arguments(): # ===> Functionality -def read_colors(color_csv: str) -> List[BrickColor]: +def read_colors(color_csv: str) -> list[BrickColor]: """ Reads the color CSV and returns the color definitions. Expected CSV-format (with header, integer RGB): red,green,blue;color-name,bricklink-color-id,bricklink-brick-type @@ -161,7 +157,7 @@ def read_colors(color_csv: str) -> List[BrickColor]: return colors_read -def closest_color(rgb: Tuple[int, int, int], color_range: list) -> BrickColor: +def closest_color(rgb: tuple[int, int, int], color_range: list) -> BrickColor: """ Returns the color from the range which is closest to the given one. :param rgb: The given color to fine the closest one for. @@ -245,7 +241,7 @@ def main(): if args.color_file == "": color_info = defaults.get_default_colors() else: - with open(args.color_file, "r") as f: + with open(args.color_file) as f: color_info = f.read() colors = read_colors(color_info) @@ -265,9 +261,7 @@ def main(): statistics = replace_with_brick_colors(image_bricks, colors) # Initialize output image - image_output = cv2.resize( - image_bricks, (w_out, h_out), interpolation=cv2.INTER_NEAREST - ) + image_output = cv2.resize(image_bricks, (w_out, h_out), interpolation=cv2.INTER_NEAREST) # Add helper grid grid_spacing = args.grid_cell_width, args.grid_cell_height @@ -275,9 +269,7 @@ def main(): add_grid(image_output, (w, h), grid_spacing) # Show some statistics - print( - f"Colors ({len(statistics)} colors, {sum([i.count for i in statistics.values()])} tiles):" - ) + print(f"Colors ({len(statistics)} colors, {sum([i.count for i in statistics.values()])} tiles):") for item in sorted(statistics.items(), key=lambda x: -x[1].count): print(f"{item[0].colorName}: {item[1].count}") @@ -285,12 +277,8 @@ def main(): write_xml(os.path.join(args.output_directory, "bricklink.xml"), statistics) # Prepare pixelated image for analysis (just resize it) - image_input = cv2.resize( - image_input, (w_out, h_out), interpolation=cv2.INTER_NEAREST - ) - image_pixelated = cv2.resize( - image_pixelated, (w_out, h_out), interpolation=cv2.INTER_NEAREST - ) + image_input = cv2.resize(image_input, (w_out, h_out), interpolation=cv2.INTER_NEAREST) + image_pixelated = cv2.resize(image_pixelated, (w_out, h_out), interpolation=cv2.INTER_NEAREST) # Write images cv2.imwrite(os.path.join(args.output_directory, "1.input.jpg"), image_input) diff --git a/tests/test_cli.py b/tests/test_cli.py index f4c35f2..b1f146a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -37,7 +37,6 @@ def hash_file(path): # Test CLI def test_main(): - # Check for update argument parser = argparse.ArgumentParser(description="brickmos golden file tests") parser.add_argument( @@ -58,8 +57,7 @@ def test_main(): # Prepare os.chdir(base_dir) os.makedirs(output_dir, exist_ok=True) - old_files = [f for f in os.listdir(output_dir)] - for f in old_files: + for f in os.listdir(output_dir): os.remove(os.path.join(output_dir, f)) base_args = [sys.executable, script_path] CliTest = collections.namedtuple( @@ -95,7 +93,6 @@ def test_main(): # Run all test cases for test in tests: - # Assemble command and arguments cmd = [*base_args, *test.args] cmd.extend( @@ -123,7 +120,7 @@ def test_main(): file.write(output) else: expected = "" - with open(test.golden_log, "r") as file: + with open(test.golden_log) as file: expected = file.read() assert output == expected @@ -134,7 +131,7 @@ def test_main(): file.write(pix_hash) else: expected = "" - with open(test.golden_pixelated, "r") as file: + with open(test.golden_pixelated) as file: expected = file.read() assert pix_hash == expected @@ -145,20 +142,20 @@ def test_main(): file.write(out_hash) else: expected = "" - with open(test.golden_output, "r") as file: + with open(test.golden_output) as file: expected = file.read() assert out_hash == expected # Compare bricklink export bricklink = "" - with open(os.path.join(output_dir, "bricklink.xml"), "r") as f: + with open(os.path.join(output_dir, "bricklink.xml")) as f: bricklink = f.read() if args.update: with open(test.golden_bricklink, "w") as file: file.write(bricklink) else: expected = "" - with open(test.golden_bricklink, "r") as file: + with open(test.golden_bricklink) as file: expected = file.read() assert bricklink == expected From a00ee715c126114d34a6316247f5d8d627cc4409 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 2 Sep 2024 00:38:03 +0200 Subject: [PATCH 5/9] Fix build workflow --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bd0dec8..cc0d8ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,9 +19,9 @@ jobs: run: | python -m pip install --upgrade pip pip install ruff pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Install project + run: pip install . - name: lint with ruff run: ruff check --output-format=github . - name: Test with pytest - run: | - pytest + run: pytest From f6bbf8436fb89ae315494f214e2a5b0669eb59b0 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 2 Sep 2024 00:38:54 +0200 Subject: [PATCH 6/9] Lower required python version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 003831a..4f15d2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ keywords = [ license = { file = "LICENSE" } name = "brickmos" readme = "README.md" -requires-python = ">=3.10" +requires-python = ">=3.9" [project.urls] Homepage = "https://github.com/merschformann/brickmos" From dc009c1c0780893c1bc85b06b3a40b2a26ebafe2 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 2 Sep 2024 00:41:23 +0200 Subject: [PATCH 7/9] Format assert --- tests/test_cli.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index b1f146a..c2f9aee 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -122,7 +122,7 @@ def test_main(): expected = "" with open(test.golden_log) as file: expected = file.read() - assert output == expected + assert output == expected, f"Expected:\n{expected}\nGot:\n{output}" # Compare hash of pixelated file pix_hash = hash_file(os.path.join(output_dir, "2.pixelated.jpg")) @@ -133,7 +133,7 @@ def test_main(): expected = "" with open(test.golden_pixelated) as file: expected = file.read() - assert pix_hash == expected + assert pix_hash == expected, f"Expected: {expected}, Got: {pix_hash}" # Compare hash of output file out_hash = hash_file(os.path.join(output_dir, "3.output.jpg")) @@ -144,7 +144,7 @@ def test_main(): expected = "" with open(test.golden_output) as file: expected = file.read() - assert out_hash == expected + assert out_hash == expected, f"Expected: {expected}, Got: {out_hash}" # Compare bricklink export bricklink = "" @@ -157,7 +157,7 @@ def test_main(): expected = "" with open(test.golden_bricklink) as file: expected = file.read() - assert bricklink == expected + assert bricklink == expected, f"Expected:\n{expected}\nGot:\n{bricklink}" if __name__ == "__main__": From 75a828b2443f8d5bb4eed18df96011e9d255c571 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 2 Sep 2024 01:53:50 +0200 Subject: [PATCH 8/9] Adding another venv ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 300d712..8815d58 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # Ignore general python stuff venv/ +.venv/ __pycache__/ .pytest_cache/ *.egg-info/ From 67411fd4279783cc95b7e9ff2338bac90ad3d094 Mon Sep 17 00:00:00 2001 From: Marius Merschformann Date: Mon, 2 Sep 2024 17:20:36 +0200 Subject: [PATCH 9/9] Fix overflow, fix workflow --- .github/workflows/build.yml | 2 ++ brickmos/brickify.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc0d8ca..b98035c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,6 +15,8 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + - name: Install apt packages + run: sudo apt update && sudo apt-get install -y libgl1-mesa-glx - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/brickmos/brickify.py b/brickmos/brickify.py index 5bd2537..d09ef02 100644 --- a/brickmos/brickify.py +++ b/brickmos/brickify.py @@ -166,10 +166,12 @@ def closest_color(rgb: tuple[int, int, int], color_range: list) -> BrickColor: """ # Get rgb values of color while adhering to cv2 BGR representation r, g, b = rgb + r, g, b = float(r), float(g), float(b) # Assess euclidean distance of color to all brick colors given color_diffs = [] for color in color_range: cr, cg, cb = color.rgb + cr, cg, cb = float(cr), float(cg), float(cb) color_diff = math.sqrt(abs(r - cr) ** 2 + abs(g - cg) ** 2 + abs(b - cb) ** 2) color_diffs.append((color_diff, color)) # Get color closest to the given one and update its stats