diff --git a/.github/workflows/safety-check.yaml b/.github/workflows/safety-check.yaml deleted file mode 100644 index 87f3916..0000000 --- a/.github/workflows/safety-check.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: Python Safety Check - -on: - push: - branches: - - '*' - pull_request: - branches: [ "main" ] - -env: - DEP_PATH: requirements.txt - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Check out master - uses: actions/checkout@master - - - name: Security vulnerabilities scan - uses: aufdenpunkt/python-safety-check@master diff --git a/.github/workflows/tests-merged.yaml b/.github/workflows/tests-merged.yaml index 17a238b..c3d4b5b 100644 --- a/.github/workflows/tests-merged.yaml +++ b/.github/workflows/tests-merged.yaml @@ -3,6 +3,8 @@ name: Unit Tests (on PR) on: push: branches: [ "main" ] + pull_request: + branches: [ "main" ] permissions: contents: read diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml deleted file mode 100644 index 08b3bb5..0000000 --- a/.github/workflows/tests.yaml +++ /dev/null @@ -1,62 +0,0 @@ -name: Unit Tests (on PR) - -on: - pull_request: - branches: [ "main" ] - -permissions: - contents: read - pull-requests: write - -jobs: - build-and-test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Python 3.11 - uses: actions/setup-python@v3 - with: - python-version: "3.11" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest - pip install -r requirements.txt - - name: Lint with flake8 - run: | - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - set -o pipefail - python -m pytest tests | tee pytest-coverage.txt - - name: Pytest coverage comment - id: coverageComment - uses: MishaKav/pytest-coverage-comment@main - with: - pytest-coverage-path: ./pytest-coverage.txt - - name: Check the output coverage - run: | - echo "Coverage Percentage - ${{ steps.coverageComment.outputs.coverage }}" - echo "Coverage Color - ${{ steps.coverageComment.outputs.color }}" - echo "Coverage Html - ${{ steps.coverageComment.outputs.coverageHtml }}" - echo "Coverage Warnings - ${{ steps.coverageComment.outputs.warnings }}" - echo "Coverage Errors - ${{ steps.coverageComment.outputs.errors }}" - echo "Coverage Failures - ${{ steps.coverageComment.outputs.failures }}" - echo "Coverage Skipped - ${{ steps.coverageComment.outputs.skipped }}" - echo "Coverage Tests - ${{ steps.coverageComment.outputs.tests }}" - echo "Coverage Time - ${{ steps.coverageComment.outputs.time }}" - echo "Not Success Test Info - ${{ steps.coverageComment.outputs.notSuccessTestInfo }}" - - name: Create the Badge - uses: schneegans/dynamic-badges-action@v1.6.0 - with: - auth: ${{ secrets.GIST_SECRET }} - gistID: 1e9f82572ceec742044c9ce6f230d275 - filename: test-coverage.json - label: Coverage Report - message: ${{ steps.coverageComment.outputs.coverage }} - color: ${{ steps.coverageComment.outputs.color }} - namedLogo: python diff --git a/src/common/validate.py b/src/common/validate.py index 87632eb..09b41dd 100644 --- a/src/common/validate.py +++ b/src/common/validate.py @@ -121,6 +121,19 @@ def validate_config(config: dict) -> bool: ) ) + # ensure each layer["values"] has a corresponding layer["filename"] and layer["weights"] + if len(layer["values"]) != len(layer["filename"]) or len( + layer["values"] + ) != len(layer["weights"]): + raise ConfigValidationError( + 'config["layers"][{}]: The number of values, filenames, and weights must be the same. Current values: {}, filenames: {}, weights: {}'.format( + i, + len(layer["values"]), + len(layer["filename"]), + len(layer["weights"]), + ) + ) + # check the incompatibilities and their config values for i, incompatibility in enumerate(config["incompatibilities"]): # check if all required incompatibility keys are present diff --git a/src/core/main.py b/src/core/main.py index a44568b..1999978 100644 --- a/src/core/main.py +++ b/src/core/main.py @@ -20,7 +20,9 @@ def __init__(self, **args): if not args["config"]: raise ValueError("No configuration file was provided.") elif not args["config"].endswith(".json"): - raise ValueError("Invalid configuration file '{}'".format(args["config"])) + raise ValueError( + "Invalid configuration file '{}'".format(args["config"]) + ) if not args["amount"]: raise ValueError("No amount was provided.") @@ -40,7 +42,7 @@ def __init__(self, **args): self.seed = ( int(args["seed"]) if args["seed"] is not None - else int.from_bytes(random.randbytes(16), byteorder='little') + else int.from_bytes(random.randbytes(16), byteorder="little") ) self.start_at = int(args["start_at"]) self.output = args["output"] @@ -124,30 +126,36 @@ def __build_genome_image(self, metadata: dict): Builds the NFT image for a single NFT. """ layers = [] - for index, attr in enumerate(metadata["attributes"]): - # get the image for the trait - for i, trait in enumerate(self.config["layers"][index]["values"]): - if trait == attr["value"]: - layers.append( - Image.open( - f'{self.config["layers"][index]["trait_path"]}/{self.config["layers"][index]["filename"][i]}.png' - ).convert("RGBA") - ) - break - - if len(layers) == 1: - rgb_im = layers[0].convert("RGBA") - elif len(layers) == 2: - main_composite = Image.alpha_composite(layers[0], layers[1]) - rgb_im = main_composite.convert("RGBA") - elif len(layers) >= 3: - main_composite = Image.alpha_composite(layers[0], layers[1]) - for index, remaining in enumerate(layers): - main_composite = Image.alpha_composite(main_composite, remaining) - rgb_im = main_composite.convert("RGBA") - - # create folder structure if it doesn't exist - rgb_im.save("{}/images/{}.png".format(self.output, metadata["token_id"])) + try: + for index, attr in enumerate(metadata["attributes"]): + # get the image for the trait + for i, trait in enumerate(self.config["layers"][index]["values"]): + if trait == attr["value"]: + layers.append( + Image.open( + f'{self.config["layers"][index]["trait_path"]}/{self.config["layers"][index]["filename"][i]}.png' + ).convert("RGBA") + ) + break + + if len(layers) == 1: + rgb_im = layers[0].convert("RGBA") + elif len(layers) == 2: + main_composite = Image.alpha_composite(layers[0], layers[1]) + rgb_im = main_composite.convert("RGBA") + elif len(layers) >= 3: + main_composite = Image.alpha_composite(layers[0], layers[1]) + for index, remaining in enumerate(layers): + main_composite = Image.alpha_composite(main_composite, remaining) + rgb_im = main_composite.convert("RGBA") + + # create folder structure if it doesn't exist + rgb_im.save("{}/images/{}.png".format(self.output, metadata["token_id"])) + + except Exception as e: + self.logger.error( + "Error generating image for token %d: %s", metadata["token_id"], e + ) def generate(self): """ diff --git a/tests/test_validate_config.py b/tests/test_validate_config.py index 8bc8dee..c615416 100644 --- a/tests/test_validate_config.py +++ b/tests/test_validate_config.py @@ -45,6 +45,66 @@ def test_validate_config_missing_key(): validate_config(config) +def test_validate_config_layer_mismatch_weights(): + config = { + "layers": [ + { + "name": "Background", + "values": ["Python Logo", "some other value"], + "trait_path": "./trait-layers/foreground", + "filename": ["logo", "some file"], + "weights": [100], + } + ], + "baseURI": ".", + "name": "NFT #", + "description": "This is a description for this NFT series.", + } + + with pytest.raises(ConfigValidationError): + validate_config(config) + + +def test_validate_config_layer_mismatch_filename(): + config = { + "layers": [ + { + "name": "Background", + "values": ["Python Logo", "some other value"], + "trait_path": "./trait-layers/foreground", + "filename": ["logo"], + "weights": [100, 100], + } + ], + "baseURI": ".", + "name": "NFT #", + "description": "This is a description for this NFT series.", + } + + with pytest.raises(ConfigValidationError): + validate_config(config) + + +def test_validate_config_layer_mismatch_values(): + config = { + "layers": [ + { + "name": "Background", + "values": ["Python Logo"], + "trait_path": "./trait-layers/foreground", + "filename": ["logo", "logo 2"], + "weights": [100, 100], + } + ], + "baseURI": ".", + "name": "NFT #", + "description": "This is a description for this NFT series.", + } + + with pytest.raises(ConfigValidationError): + validate_config(config) + + def test_validate_config_incorrect_type(): config = { "layers": "should be a list",