Repository: travel-tools/travel (language: python)

Issue:
Feat/piper only



Discussion / hints:

Fix patch:
diff --git a/.dockerignore b/.dockerignore
index 04b82c3..8355cbb 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -40,7 +40,6 @@ __pycache__/
 .Python
 build/
 develop-eggs/
-dist/
 downloads/
 eggs/
 .eggs/
diff --git a/.github/workflows/piper-docker.yml b/.github/workflows/piper-docker.yml
deleted file mode 100644
index d05381e..0000000
--- a/.github/workflows/piper-docker.yml
+++ /dev/null
@@ -1,56 +0,0 @@
-name: Build Piper Docker
-on:
-  push:
-    branches: [ release/docker/* ]
-  pull_request:
-    types: [opened, synchronize, reopened]
-    branches: [ dev ]
-  workflow_dispatch:
-
-defaults:
-  run:
-    shell: bash
-
-jobs:
-
-  build:
-    runs-on: ubuntu-latest
-    steps:
-      - name: Login to GHCR
-        uses: docker/login-action@v1
-        with:
-          registry: ghcr.io
-          username: ${{ github.repository_owner }}
-          password: ${{ secrets.GITHUB_TOKEN }}
-      - name: Build and Push
-        uses: docker/build-push-action@v2.6.1
-        with:
-          file: containers/python/Dockerfile
-          push: true
-          tags: ghcr.io/piper-tools/piper:${{ github.sha }}
-
-  test:
-    needs: build
-    runs-on: ubuntu-latest
-    container: ghcr.io/piper-tools/piper:${{ github.sha }}
-    steps:
-      - name: Test the Complex Project
-        run: |
-          cd /application/tests/data/complexproject
-          python -m piper setup
-          cd microservices/second/
-          source venv-second/bin/activate
-          python package/second/__init__.py
-
-  clean:
-    needs: build
-    runs-on: ubuntu-latest
-    container: python:3.7.4-slim
-    steps:
-      - uses: actions/checkout@v2
-      - name: Delete all except last 5
-        env:
-          REGISTRY_PAT: ${{ secrets.REGISTRY_PAT }}
-        run: |
-          pip install requests==2.26.0
-          python devops/github/registry.py --artifact piper --action delete-all-except-last --keep 5
diff --git a/.github/workflows/piper.yml b/.github/workflows/piper.yml
new file mode 100644
index 0000000..475dd2f
--- /dev/null
+++ b/.github/workflows/piper.yml
@@ -0,0 +1,215 @@
+name: Piper
+on:
+  pull_request:
+    types: [ opened, synchronize, reopened ]
+    branches: [ dev, master ]
+  workflow_dispatch:
+
+defaults:
+  run:
+    shell: bash
+
+jobs:
+
+  ##########################################
+  # PYTHON
+  ##########################################
+
+  pack:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.7.11'
+      - name: Install dependencies
+        run: pip install requests==2.26.0
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Get next tag
+        id: tags
+        run: echo "::set-output name=next::$( python devops/github/tags.py ${{ github.repository }} ${{ github.head_ref }} )"
+      - name: "Change tag"
+        run: python devops/github/replace.py src/${{ github.event.repository.name }}/package/setup.py version=.+, version=\"${{ steps.tags.outputs.next }}\",
+      - name: Pack
+        run: |
+          cd src/${{ github.event.repository.name }}/package
+          python setup.py sdist
+          cp -r dist ${{ github.workspace }}
+      - name: Upload dist
+        uses: actions/upload-artifact@v2
+        with:
+          name: dist
+          path: dist
+          if-no-files-found: error
+    outputs:
+      version: ${{ steps.tags.outputs.next }}
+
+  test-package:
+    needs: pack
+    runs-on: ubuntu-latest
+    steps:
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.7.11'
+      - name: Download dist
+        uses: actions/download-artifact@v2
+        with:
+          name: dist
+          path: ${{ github.workspace }}/dist
+      - name: Install package
+        run: |
+          pip install dist/$( ls dist )
+      - uses: actions/checkout@v2
+      - name: Test
+        run: |
+          pip install pytest
+          source devops/${{ github.event.repository.name }}/test.sh
+
+  release-package-on-test:
+    if: ${{ github.base_ref == 'master' && ( startsWith(github.head_ref, 'release/major/') || startsWith(github.head_ref, 'release/minor/') || startsWith(github.head_ref, 'hotfix/major/') || startsWith(github.head_ref, 'hotfix/minor/') ) }}
+    environment: test-pypi
+    needs: test-package
+    runs-on: ubuntu-latest
+    steps:
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.7.11'
+      - name: Download dist
+        uses: actions/download-artifact@v2
+        with:
+          name: dist
+          path: ${{ github.workspace }}/dist
+      - name: Release on Test
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_TOKEN }}
+          repository_url: https://test.pypi.org/legacy/
+
+  test-package-on-test-pypi:
+    needs: [release-package-on-test, pack]
+    runs-on: ubuntu-latest
+    steps:
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.7.11'
+      - name: Install package
+        run: pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ piper-tools==${{ needs.pack.outputs.version }}
+      - uses: actions/checkout@v2
+      - name: Test
+        run: |
+          pip install pytest
+          source devops/${{ github.event.repository.name }}/test.sh
+
+  new-version:
+    needs: [test-package-on-test-pypi, pack]
+    runs-on: ubuntu-latest
+    steps:
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.7.11'
+      - name: Checkout from PR
+        uses: actions/checkout@v2
+        if: github.event_name == 'pull_request'
+        with:
+          ref: ${{ github.event.pull_request.head.ref }}
+      - name: Checkout from branch
+        uses: actions/checkout@v2
+        if: github.event_name != 'pull_request'
+      - name: Change version
+        run: python devops/github/replace.py src/${{ github.event.repository.name }}/package/setup.py version=.+, version=\"${{ steps.tags.outputs.next }}\",
+      - name: Configure
+        run: |
+          git config --global user.name 'GitHub Actions'
+          git config --global user.email 'actions@users.noreply.github.com'
+      - name: Commit
+        run: |
+          git commit -am "[skip ci] Version ${{ needs.pack.outputs.version }}"
+          git push
+      - name: Tag
+        run: |
+          git tag ${{ needs.pack.outputs.version }}
+          git push --tags
+
+  release-package:
+    environment: prod-pypi
+    needs: new-version
+    runs-on: ubuntu-latest
+    steps:
+      - name: Setup Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: '3.7.11'
+      - name: Download dist
+        uses: actions/download-artifact@v2
+        with:
+          name: dist
+          path: ${{ github.workspace }}/dist
+      - name: Release on Test
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_TOKEN }}
+
+
+  ##########################################
+  # DOCKER
+  ##########################################
+
+  build-docker:
+    needs: test-package
+    runs-on: ubuntu-latest
+    steps:
+      - name: Set up Docker Buildx
+        uses: docker/setup-buildx-action@v1
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Download dist
+        uses: actions/download-artifact@v2
+        with:
+          name: dist
+          path: ${{ github.workspace }}/dist
+      - name: Login to GHCR
+        uses: docker/login-action@v1
+        with:
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+      - name: Build and Push
+        uses: docker/build-push-action@v2.6.1
+        with:
+          build-args: |
+            DIST=dist
+          context: ${{ github.workspace }}
+          file: containers/${{ github.event.repository.name }}/Dockerfile
+          push: true
+          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
+
+  test-docker:
+    needs: build-docker
+    runs-on: ubuntu-latest
+    container: ghcr.io/${{ github.repository }}:${{ github.sha }}
+    steps:
+      - uses: actions/checkout@v2
+      - name: Test
+        run: |
+          pip install pytest
+          source devops/${{ github.event.repository.name }}/test.sh
+
+  clean-docker:
+    needs: build-docker
+    runs-on: ubuntu-latest
+    container: python:3.7.4-slim
+    steps:
+      - uses: actions/checkout@v2
+      - name: Delete all except last 5
+        env:
+          REGISTRY_PAT: ${{ secrets.REGISTRY_PAT }}
+        run: |
+          pip install requests==2.26.0
+          python devops/github/registry.py --artifact ${{ github.event.repository.name }} --action delete-all-except-last --keep 5
diff --git a/.gitignore b/.gitignore
index 1e93a5f..51c3132 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,9 +3,6 @@
 # Learn more about .gitignore:
 #     https://www.atlassian.com/git/tutorials/saving-changes/gitignore
 
-# Specific
-/src/solver/data/
-
 
 # Most of the content is taken from https://github.com/github/gitignore/blob/master/Python.gitignore
 
diff --git a/README.md b/README.md
index 603d7e3..a7ded35 100644
--- a/README.md
+++ b/README.md
@@ -26,4 +26,4 @@ All in all, Piper lets you design **proper structure and modularity** for your c
 
 ## Example of Piper Project
 
-You can find an example of Piper project [here](https://github.com/federicopugliese/piper/tree/master/src/python/package/tests/data/complexproject).
\ No newline at end of file
+You can find an example of Piper project [here](https://github.com/piper-tools/piper/tree/master/src/piper/tests/data/complexproject).
\ No newline at end of file
diff --git a/containers/piper/Dockerfile b/containers/piper/Dockerfile
new file mode 100644
index 0000000..6596e46
--- /dev/null
+++ b/containers/piper/Dockerfile
@@ -0,0 +1,14 @@
+ARG TAG="3.7.4-slim"
+
+# From python image
+FROM python:${TAG}
+
+# Copy the dist
+ARG DIST="src/piper/package/dist"
+COPY ${DIST} dist/
+
+# Install the package
+RUN pip install --no-cache-dir dist/$(ls dist)
+
+# Run it
+ENTRYPOINT ["piper"]
diff --git a/containers/python/Dockerfile b/containers/piper/Dockerfile.source
similarity index 85%
rename from containers/python/Dockerfile
rename to containers/piper/Dockerfile.source
index 86167e1..adba9ab 100644
--- a/containers/python/Dockerfile
+++ b/containers/piper/Dockerfile.source
@@ -1,6 +1,6 @@
 FROM python:3.7.4-slim
 
-ARG SRC="src/python/package"
+ARG SRC="src/piper/package"
 WORKDIR application
 
 # Pre-install requirements, so that this sub-image is cached - no need to re-download it upon code-only change.
@@ -11,7 +11,6 @@ RUN pip install --no-cache-dir -r requirements.txt
 COPY ${SRC}/setup.py .
 COPY ${SRC}/MANIFEST.in .
 COPY ${SRC}/piper piper/
-COPY ${SRC}/tests tests/
 
 # Install the package
 RUN pip install -e .
diff --git a/containers/piper/docker-compose.dist.yml b/containers/piper/docker-compose.dist.yml
new file mode 100644
index 0000000..d98932d
--- /dev/null
+++ b/containers/piper/docker-compose.dist.yml
@@ -0,0 +1,13 @@
+version: "3"
+
+services:
+
+  piper:
+
+    build:
+      context: "./../.."
+      dockerfile: "containers/piper/Dockerfile"
+      args:
+        - DIST=src/piper/package/dist/
+
+    image: "piper"
diff --git a/containers/piper/docker-compose.yml b/containers/piper/docker-compose.yml
new file mode 100644
index 0000000..9996e3e
--- /dev/null
+++ b/containers/piper/docker-compose.yml
@@ -0,0 +1,11 @@
+version: "3"
+
+services:
+
+  piper:
+
+    build:
+      context: "./../.."
+      dockerfile: "containers/piper/Dockerfile.source"
+
+    image: "piper"
diff --git a/containers/python/docker-compose.yml b/containers/python/docker-compose.yml
deleted file mode 100644
index 9a42712..0000000
--- a/containers/python/docker-compose.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-version: "3"
-
-services:
-
-  python:
-
-    build:
-      context: "./../.."
-      dockerfile: "containers/python/Dockerfile"
-
-    image: "python"
diff --git a/devops/github/replace.py b/devops/github/replace.py
new file mode 100644
index 0000000..68883a4
--- /dev/null
+++ b/devops/github/replace.py
@@ -0,0 +1,21 @@
+import argparse
+import re
+
+
+def replace(file: str, regex: str, replacement: str):
+
+    with open(file) as f:
+        content = f.read()
+    content = re.sub(regex, replacement, content)
+    with open(file, "w") as f:
+        f.write(content)
+
+if __name__ == '__main__':
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("file")
+    parser.add_argument("regex")
+    parser.add_argument("replacement")
+    args = parser.parse_args()
+
+    replace(args.file, args.regex, args.replacement)
diff --git a/devops/github/tags.py b/devops/github/tags.py
new file mode 100644
index 0000000..c55e081
--- /dev/null
+++ b/devops/github/tags.py
@@ -0,0 +1,36 @@
+import argparse
+
+import requests
+from typing import List, Dict
+
+
+def get_last(repository: str) -> str:
+    tags = requests.get(url=f"https://api.github.com/repos/{repository}/git/refs/tags").json()
+    return max([t["ref"] for t in tags])
+
+def get_next(tag: str, major: bool, hotfix: bool) -> str:
+    maj, minor, patch = tuple(int(x) for x in tag.split("/")[-1].split("."))
+    maj = maj + 1 if major else maj
+    minor = minor + 1 if not major and not hotfix else minor
+    patch = patch + 1 if not major and hotfix else patch
+    return f"{maj}.{minor}.{patch}"
+
+def main(repository: str, major: bool, hotfix: bool):
+
+    tag = get_last(repository)
+    next = get_next(tag, major, hotfix)
+    print(next)
+
+if __name__ == '__main__':
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("repository")
+    parser.add_argument("release_branch")
+    parser.add_argument("--major", action="store_true")
+    parser.add_argument("--hotfix", action="store_true")
+    args = parser.parse_args()
+
+    major = args.major or "/major/" in args.release_branch
+    hotfix = args.hotfix or "hotfix/" in args.release_branch
+
+    main(args.repository, major, hotfix)
diff --git a/src/python/package/LICENSE b/src/piper/package/LICENSE
similarity index 100%
rename from src/python/package/LICENSE
rename to src/piper/package/LICENSE
diff --git a/src/python/package/MANIFEST.in b/src/piper/package/MANIFEST.in
similarity index 100%
rename from src/python/package/MANIFEST.in
rename to src/piper/package/MANIFEST.in
diff --git a/src/piper/package/README.md b/src/piper/package/README.md
new file mode 100644
index 0000000..a7ded35
--- /dev/null
+++ b/src/piper/package/README.md
@@ -0,0 +1,29 @@
+# Piper
+
+A software manager for easy **development** and **distribution** of **Python code**.
+
+The main features that Piper adds to Python are:
+
+- Support for large-scale, **multi-package projects**
+- **Reproducibility** (clear, transparent **dependency management**)
+- Robust **development-lifecycle**, from blueprinting to distribution 
+
+Piper is inspired by what **[Maven](https://maven.apache.org/)** is for Java and uses [Pip](https://github.com/pypa/pip) and [Virtual Environments](https://docs.python.org/3/library/venv.html).
+
+## Why Piper
+
+**Python is great** in many things, particularly for **scripting**. But it is powerful enough to create **complex software** too. Still, when doing so, it lacks some robustness and quickness.
+
+Instead:
+ 
+- Piper lets you **forget about repetitive setup of Python projects**, with creation of Virtual Environments, issues with imports, PYTHONPATH, folder structures. It does all of this for you.
+
+- Piper ensures that when you run a Python script, all its **requirements are implicitly installed**. Along with reproducibility, this makes room for **easy collaboration** between developers.
+
+- Also, while with standard tools it's hard to have multiple packages, one requiring the other, with Piper is a matter of **few YAML files**. You can **split your project** in several packages instead of having tons of requirements, code replication and/or single package-monoliths. 
+
+All in all, Piper lets you design **proper structure and modularity** for your code. No worries about imports and low-level stuff.
+
+## Example of Piper Project
+
+You can find an example of Piper project [here](https://github.com/piper-tools/piper/tree/master/src/piper/tests/data/complexproject).
\ No newline at end of file
diff --git a/src/python/package/piper/__init__.py b/src/piper/package/piper/__init__.py
similarity index 79%
rename from src/python/package/piper/__init__.py
rename to src/piper/package/piper/__init__.py
index 539c1e3..a1f4629 100644
--- a/src/python/package/piper/__init__.py
+++ b/src/piper/package/piper/__init__.py
@@ -10,7 +10,7 @@
 
 def _init_logger():
     root_log = logging.getLogger()
-    formatter = logging.Formatter("%(asctime)s [%(process)d] %(levelname)-5s [%(name)-48s] %(message)s")
+    formatter = logging.Formatter("%(message)s")
     stream_handler = logging.StreamHandler(sys.stdout)
     stream_handler.setFormatter(formatter)
     root_log.addHandler(stream_handler)
diff --git a/src/piper/package/piper/__main__.py b/src/piper/package/piper/__main__.py
new file mode 100644
index 0000000..55921bd
--- /dev/null
+++ b/src/piper/package/piper/__main__.py
@@ -0,0 +1,51 @@
+import argparse
+import logging
+import os
+
+from piper.cli import python_wrapper, blueprinter, packer
+from piper.cli.cleaner import Cleaner
+from piper.cli.setupper import Setupper
+
+logger = logging.getLogger(__name__)
+
+
+def main():
+
+    # Piper
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--context", default=os.getcwd(), help="Path to the folder containing the pipe.yml file")
+    parser.set_defaults(action=lambda args: parser.parse_args(["-h"]))
+    subparsers = parser.add_subparsers()
+
+    # Clean
+    clean = subparsers.add_parser("clean")
+    clean.set_defaults(action=lambda args, rest: Cleaner().manage(args.context))
+
+    # Blueprint
+    blueprint = subparsers.add_parser("blueprint")
+    blueprint.set_defaults(action=lambda args, rest: blueprinter.run(args.context))
+
+    # Setup
+    setup = subparsers.add_parser("setup")
+    setup.set_defaults(action=lambda args, rest: Setupper().manage(args.context))
+
+    # Pack
+    pack = subparsers.add_parser("pack")
+    pack.add_argument("package", help="Name of the pipe to run setup.py commands")
+    pack.set_defaults(action=lambda args, rest: packer.pack(args.context, args.package, rest))
+
+    # Release
+
+    # # Python
+    # python = subparsers.add_parser("python")
+    # python.add_argument("package", help="Name of the pipe (it will be used to activate its venv)")
+    # python.set_defaults(action=lambda args, rest: python_wrapper.run(args.context, args.package, rest))
+
+    # Parse args and invoke the corresponding functions
+    arguments, remainder = parser.parse_known_args()
+    arguments.action(arguments, remainder)
+    logger.info("All done.")
+
+
+if __name__ == '__main__':
+    main()
diff --git a/src/python/package/piper/cli/__init__.py b/src/piper/package/piper/cli/__init__.py
similarity index 100%
rename from src/python/package/piper/cli/__init__.py
rename to src/piper/package/piper/cli/__init__.py
diff --git a/src/python/package/piper/cli/base.py b/src/piper/package/piper/cli/base.py
similarity index 57%
rename from src/python/package/piper/cli/base.py
rename to src/piper/package/piper/cli/base.py
index 241ef44..96bfdad 100644
--- a/src/python/package/piper/cli/base.py
+++ b/src/piper/package/piper/cli/base.py
@@ -1,4 +1,5 @@
 import abc
+import logging
 from typing import Set
 
 from piper.config.pipe import Pipe
@@ -6,6 +7,9 @@
 from piper.tools.python import main_python, Python
 
 
+logger = logging.getLogger(__name__)
+
+
 class PiperCommand(abc.ABC):
 
     def __init__(self, python: Python = main_python):
@@ -22,7 +26,14 @@ def _manage_from_pipe_recursive(self, pipe: Pipe, done: Set[Pipe]):
             # Manage dependencies first, then the pipe
             for p in [*pipe.flat_dependencies(), pipe]:
                 if p not in done:
+                    logger.info("")
+                    logger.info("="*60)
+                    logger.info(f"=== {p.name.center(52, ' ')} ===")
+                    logger.info("="*60)
                     self._manage(p)
+                    logger.info("=" * 60)
+                    logger.info("")
+                    logger.info("")
                     done.add(p)
 
         else:
@@ -34,11 +45,15 @@ def _manage_from_pipe_recursive(self, pipe: Pipe, done: Set[Pipe]):
     def manage_from_pipe(self, pipe: Pipe):
         self._manage_from_pipe_recursive(pipe, done=set())
 
-    def manage(self, pipe_location: str):
+    def manage(self, context: str, package: str = None) -> (Pipe, Pipe):
 
         # Read the target pipe and all the pipes
-        target = get_pipe_name(pipe_location)
-        pipes = read_all_pipes(pipe_location)
+        target = package or get_pipe_name(context)
+        pipes = read_all_pipes(context)
 
         # Manage this target pipe
-        self.manage_from_pipe(pipes[target])
+        if target not in pipes:
+            raise ValueError(f"The specified pipe does not exist: {context}")
+        pipe = pipes[target]
+        self.manage_from_pipe(pipe)
+        return pipe, pipes
diff --git a/src/piper/package/piper/cli/blueprinter.py b/src/piper/package/piper/cli/blueprinter.py
new file mode 100644
index 0000000..498fbac
--- /dev/null
+++ b/src/piper/package/piper/cli/blueprinter.py
@@ -0,0 +1,66 @@
+import logging
+import os
+import shutil
+from typing import Dict, Any
+
+import pkg_resources
+import yaml
+from piper import RESOURCES_LOCATION
+from piper.config.pipe import Pipe
+from piper.config.sanitizers import pip_sanitizer, pipe_sanitizer
+from piper.tools.venv import Virtualenv
+
+logger = logging.getLogger(__name__)
+_GENERATE_BLUEPRINT = os.path.join(RESOURCES_LOCATION, "blueprint", "generate.py")
+
+
+def _generate_breath_first(blueprint_file: str, venv: Virtualenv, context: str, yml: Dict[str, Any]):
+
+    # Generate these blueprints
+    for pipe, properties in yml.items():
+
+        # Get the folder where the pipe will be created
+        pipe = pipe_sanitizer.sanitize_name(pipe)
+        pipe_context = os.path.join(context, pipe)
+
+        # Is it a blueprint?
+        if "blueprint" in properties:
+
+            # Get the package of the blueprint
+            blueprint = properties["blueprint"]
+            if len(blueprint) != 1:
+                raise ValueError(f"Error in '{pipe}': this pipe's blueprint must be a single 'name: version_or_path' dictionary")
+            name = pip_sanitizer.sanitize_package(list(blueprint.keys())[0])
+            venv.pip.install(blueprint, allow_local_files=True)
+
+            # Generate the blueprint
+            command = f'"{_GENERATE_BLUEPRINT}" --context "{pipe_context}" --blueprint {name} --file "{blueprint_file}" --pipe {pipe}'
+            venv.python.run(command)
+
+    # Generate the subpipes
+    for pipe, properties in yml.items():
+        pipe_context = os.path.join(context, pipe)
+        if "pipes" in properties:
+            _generate_breath_first(blueprint_file, venv, pipe_context, properties["pipes"])
+
+
+def run(context: str):
+
+    # Temporary create a venv
+    venv = Virtualenv(Pipe(location=context, yml={}))
+    venv.create()
+    version = pip_sanitizer.sanitize_version(pkg_resources.get_distribution("PyYAML").version)
+    venv.pip.install({"PyYAML": version})
+
+    # Read the blueprint file
+    path = os.path.join(context, "blueprint.yml")
+    with open(path) as f:
+        yml = yaml.load(f, Loader=yaml.SafeLoader)
+    if not yml or "pipes" not in yml:
+        raise ValueError("Blueprint file must not be empty and should contain at least 'pipes' key.")
+
+    # Generate
+    _generate_breath_first(path, venv, context, yml["pipes"])
+
+    # Remove the temporary venv
+    shutil.rmtree(venv.path)
diff --git a/src/python/package/piper/cli/cleaner.py b/src/piper/package/piper/cli/cleaner.py
similarity index 74%
rename from src/python/package/piper/cli/cleaner.py
rename to src/piper/package/piper/cli/cleaner.py
index 73333d7..845c016 100644
--- a/src/python/package/piper/cli/cleaner.py
+++ b/src/piper/package/piper/cli/cleaner.py
@@ -4,10 +4,8 @@
 
 from piper.cli.base import PiperCommand
 from piper.config.pipe import Pipe
-from piper.tools.python import main_python
 from piper.tools.venv import Virtualenv
 
-
 logger = logging.getLogger(__name__)
 
 
@@ -18,7 +16,8 @@ def _manage(self, pipe: Pipe):
         logger.info(f"Cleaning {pipe.name}")
         to_remove = [
             os.path.join(pipe.setup_py_folder, f"{pipe.package}.egg-info"),     # egg-info
-            Virtualenv(main_python=main_python, pipe=pipe).path                 # venv folder
+            Virtualenv(pipe).path,                                              # venv folder
+            pipe.build_folder                                                   # artifacts build folder
         ]
 
         # Clean
diff --git a/src/piper/package/piper/cli/packer.py b/src/piper/package/piper/cli/packer.py
new file mode 100644
index 0000000..b005c50
--- /dev/null
+++ b/src/piper/package/piper/cli/packer.py
@@ -0,0 +1,46 @@
+import logging
+import os
+import shutil
+from distutils.dir_util import copy_tree
+
+import setuptools
+from piper.cli.setupper import Setupper
+from piper.tools.venv import Virtualenv
+
+logger = logging.getLogger(__name__)
+
+
+def pack(context: str, package: str, command: str):
+
+    # Setup the pipes and dependencies
+    current_pipe, all_pipes = Setupper().manage(context, package=package)
+
+    # Get the right python (this might be useful in case the setup.py uses a particular syntax)
+    env = Virtualenv(all_pipes[package])
+
+    # Clean the previous target folder, if existing
+    build_folder = current_pipe.build_folder
+    if os.path.isdir(build_folder):
+        shutil.rmtree(build_folder)
+
+    # Copy the structure of this package
+    _copy_folder(current_pipe.setup_py_folder, build_folder)
+    source_build_folder = os.path.join(build_folder, os.path.basename(current_pipe.setup_py_folder))
+    # For all dependencies, copy their code too
+    for dep in current_pipe.flat_dependencies():
+        # For all code packages, copy it inside the copied setup.py folder
+        for folder in setuptools.find_packages(where=dep.setup_py_folder):
+            _copy_folder(
+                os.path.join(dep.setup_py_folder, folder),
+                os.path.join(source_build_folder)
+            )
+
+    # Setup the code
+    setup_py = os.path.join(source_build_folder, "setup.py")
+    env.python.run(f"{setup_py} {' '.join(command)}", cwd=source_build_folder)  # TODO should check for spaces and commas, or use list!
+
+
+def _copy_folder(source, destination):
+    os.makedirs(destination, exist_ok=True)
+    right_destination = os.path.join(destination, os.path.basename(os.path.normpath(source)))
+    return copy_tree(source, right_destination)
\ No newline at end of file
diff --git a/src/piper/package/piper/cli/python_wrapper.py b/src/piper/package/piper/cli/python_wrapper.py
new file mode 100644
index 0000000..9641e81
--- /dev/null
+++ b/src/piper/package/piper/cli/python_wrapper.py
@@ -0,0 +1,27 @@
+import logging
+import os
+
+from piper.cli.setupper import Setupper
+from piper.tools.python import main_python
+from piper.tools.venv import Virtualenv
+
+logger = logging.getLogger(__name__)
+
+
+def run(context: str, package: str, command: str):
+
+    # Setup the pipes and dependencies
+    current_pipe, all_pipes = Setupper().manage(context, package=package)
+
+    # Run the code
+    env = Virtualenv(all_pipes[package])
+    if os.path.isdir(env.path):
+        python = env.python
+    else:
+        python = main_python
+
+    # Good in unix
+    if os.name == "posix":
+        python.replace_process(command)
+    else:
+        logger.error(f" !!! Run the same command with {python.path}")
diff --git a/src/python/package/piper/cli/setupper.py b/src/piper/package/piper/cli/setupper.py
similarity index 78%
rename from src/python/package/piper/cli/setupper.py
rename to src/piper/package/piper/cli/setupper.py
index c4c9c1b..fc5037a 100644
--- a/src/python/package/piper/cli/setupper.py
+++ b/src/piper/package/piper/cli/setupper.py
@@ -3,7 +3,6 @@
 
 from piper.cli.base import PiperCommand
 from piper.config.pipe import Pipe
-from piper.tools.python import main_python
 from piper.tools.venv import Virtualenv
 
 
@@ -16,7 +15,7 @@ def _manage(self, pipe: Pipe):
             Path(pipe.requirements_file).touch()
 
         # Create the virtualenv
-        venv = Virtualenv(main_python=main_python, pipe=pipe)
+        venv = Virtualenv(pipe)
         venv.create()
         venv.update()
 
diff --git a/src/python/package/piper/config/__init__.py b/src/piper/package/piper/config/__init__.py
similarity index 100%
rename from src/python/package/piper/config/__init__.py
rename to src/piper/package/piper/config/__init__.py
diff --git a/src/python/package/piper/config/pipe.py b/src/piper/package/piper/config/pipe.py
similarity index 65%
rename from src/python/package/piper/config/pipe.py
rename to src/piper/package/piper/config/pipe.py
index 60d19bd..42eb345 100644
--- a/src/python/package/piper/config/pipe.py
+++ b/src/piper/package/piper/config/pipe.py
@@ -1,7 +1,7 @@
 import logging
 import os
 
-from piper.config.sanitizers import python, pip
+from piper.config.sanitizers import python_sanitizer, pip_sanitizer
 
 logger = logging.getLogger(__name__)
 
@@ -15,13 +15,14 @@ def __init__(self, location: str, yml: dict):
 
         # Pop the config entries
         config = yml.copy()
-        self.python = python.sanitize_version(config.pop("python", None), nullable=True)
-        self.dependencies = {pip.sanitize_package(dep): None for dep in config.pop("dependencies", [])}
-        self.requirements = {pip.sanitize_package(name): pip.sanitize_version(version) for name, version in config.pop("requirements", {}).items()}
+        self.python = python_sanitizer.sanitize_version(config.pop("python", None), nullable=True)
+        self.dependencies = {pip_sanitizer.sanitize_package(dep): None for dep in config.pop("dependencies", [])}
+        self.requirements = {pip_sanitizer.sanitize_package(name): pip_sanitizer.sanitize_version(version) for name, version in config.pop("requirements", {}).items()}
 
         # Extra utils
         self.package = self.name  # But could be different
         self.setup_py_folder = os.path.join(self.location, "package")  # But could be different
+        self.build_folder = os.path.join(self.location, "build")  # But could be different
         self.requirements_file = os.path.join(self.setup_py_folder, "requirements.txt")
 
         # If there are still configs, they are unknown. Raise an error
@@ -31,7 +32,7 @@ def __init__(self, location: str, yml: dict):
     def fill_dependency_with_pipe(self, pipe):
         self.dependencies[pipe.name] = pipe
 
-    def flat_dependencies(self):
+    def flat_dependencies(self, with_current: bool = False):
 
         def visit(pipe, visited, level=0):
             for dep in pipe.dependencies.values():
@@ -41,6 +42,8 @@ def visit(pipe, visited, level=0):
 
         pipes = visit(self, {})
         dependencies = [pipe for pipe, level in sorted(pipes.items(), key=lambda x: x[1], reverse=True)]
+        if with_current:
+            dependencies.append(self)
         return dependencies
 
     def __str__(self):
diff --git a/src/python/package/piper/config/reader.py b/src/piper/package/piper/config/reader.py
similarity index 100%
rename from src/python/package/piper/config/reader.py
rename to src/piper/package/piper/config/reader.py
diff --git a/src/python/package/piper/config/sanitizers/__init__.py b/src/piper/package/piper/config/sanitizers/__init__.py
similarity index 100%
rename from src/python/package/piper/config/sanitizers/__init__.py
rename to src/piper/package/piper/config/sanitizers/__init__.py
diff --git a/src/python/package/piper/config/sanitizers/asserter.py b/src/piper/package/piper/config/sanitizers/asserter.py
similarity index 100%
rename from src/python/package/piper/config/sanitizers/asserter.py
rename to src/piper/package/piper/config/sanitizers/asserter.py
diff --git a/src/python/package/piper/config/sanitizers/pip.py b/src/piper/package/piper/config/sanitizers/pip_sanitizer.py
similarity index 100%
rename from src/python/package/piper/config/sanitizers/pip.py
rename to src/piper/package/piper/config/sanitizers/pip_sanitizer.py
diff --git a/src/piper/package/piper/config/sanitizers/pipe_sanitizer.py b/src/piper/package/piper/config/sanitizers/pipe_sanitizer.py
new file mode 100644
index 0000000..6d79bb5
--- /dev/null
+++ b/src/piper/package/piper/config/sanitizers/pipe_sanitizer.py
@@ -0,0 +1,9 @@
+import re
+
+from piper.config.sanitizers import asserter
+
+_NAME = re.compile(r"^[A-Za-z0-9\-\._]+$")
+
+
+def sanitize_name(name: str) -> str:
+    return asserter.regex(_NAME, name)
diff --git a/src/python/package/piper/config/sanitizers/python.py b/src/piper/package/piper/config/sanitizers/python_sanitizer.py
similarity index 100%
rename from src/python/package/piper/config/sanitizers/python.py
rename to src/piper/package/piper/config/sanitizers/python_sanitizer.py
diff --git a/src/piper/package/piper/resources/blueprint/generate.py b/src/piper/package/piper/resources/blueprint/generate.py
new file mode 100644
index 0000000..65ac4ee
--- /dev/null
+++ b/src/piper/package/piper/resources/blueprint/generate.py
@@ -0,0 +1,62 @@
+import argparse
+import importlib
+import inspect
+import sys
+from pathlib import Path
+
+import yaml
+from piperblue.blueprint import PiperBlueprint
+
+
+def _find(yml, pipe):
+    try:
+        for p, properties in yml.items():
+            if p == pipe:
+                return properties
+            found = _find(yml[p], pipe)
+            if found:
+                return found
+    except:
+        return None
+
+
+def _generate(file: str, blueprint_name: str, pipe: str, context: str):
+
+    # Find the config
+    with open(file) as f:
+        yml = yaml.load(f, Loader=yaml.SafeLoader)
+    properties = _find(yml, pipe)
+    config = properties.get("config")
+
+    # Import the blueprint class
+    module = f"{blueprint_name}.blueprint"
+    all_blueprints = inspect.getmembers(
+        importlib.import_module(module),
+        lambda member: inspect.isclass(member) and member.__module__ == module and issubclass(member, PiperBlueprint)
+    )[0]
+    blueprint = all_blueprints[1]
+
+    # Get its resources folder
+    resources_folder = str(Path(sys.modules[blueprint.__module__].__file__).parent / "resources")
+
+    # Instantiate it and run it
+    blueprint().generate(resources_folder, context, config)
+
+
+def main():
+
+    # Parse
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--file")
+    parser.add_argument("--context")
+    parser.add_argument("--pipe")
+    parser.add_argument("--blueprint")
+    args = parser.parse_args()
+
+    # Generate
+    _generate(args.file, args.blueprint, args.pipe, args.context)
+
+
+if __name__ == '__main__':
+
+    main()
diff --git a/src/python/package/piper/tools/__init__.py b/src/piper/package/piper/tools/__init__.py
similarity index 100%
rename from src/python/package/piper/tools/__init__.py
rename to src/piper/package/piper/tools/__init__.py
diff --git a/src/python/package/piper/tools/executable.py b/src/piper/package/piper/tools/executable.py
similarity index 53%
rename from src/python/package/piper/tools/executable.py
rename to src/piper/package/piper/tools/executable.py
index 9185fdd..c4a34d1 100644
--- a/src/python/package/piper/tools/executable.py
+++ b/src/piper/package/piper/tools/executable.py
@@ -1,7 +1,5 @@
 import logging
-import os
 import subprocess
-import shlex
 
 from piper.tools.shell import Shell
 
@@ -14,5 +12,5 @@ def __init__(self, tool: str, pre_command: str = None):
         super().__init__(pre_command)
         self._tool = tool
 
-    def run(self, command: str) -> subprocess.CompletedProcess:
-        return super().run(f"{self._tool} {command}")
+    def run(self, command: str, capture: bool = False, text: bool = True, cwd: str = None) -> subprocess.CompletedProcess:
+        return super().run(f"{self._tool} {command}", capture=capture, text=text, cwd=cwd)
diff --git a/src/piper/package/piper/tools/pip.py b/src/piper/package/piper/tools/pip.py
new file mode 100644
index 0000000..87139dc
--- /dev/null
+++ b/src/piper/package/piper/tools/pip.py
@@ -0,0 +1,17 @@
+import os
+from typing import Dict
+
+from piper.tools.executable import Executable
+from piper.config.sanitizers.pip_sanitizer import sanitize_package, sanitize_version
+
+class Pip(Executable):
+
+    def __init__(self, pre_command: str = None):
+        super().__init__("pip", pre_command=pre_command)
+
+    def install(self, requirements: Dict[str, str], allow_local_files=False):
+        packages = [
+            version if allow_local_files and os.path.isdir(version) else f'{sanitize_package(name)}=={sanitize_version(version)}'
+            for name, version in requirements.items()
+        ]
+        super().run(f"install {' '.join(packages)}")
diff --git a/src/python/package/piper/tools/python.py b/src/piper/package/piper/tools/python.py
similarity index 66%
rename from src/python/package/piper/tools/python.py
rename to src/piper/package/piper/tools/python.py
index fdef6cf..8ab4671 100644
--- a/src/python/package/piper/tools/python.py
+++ b/src/piper/package/piper/tools/python.py
@@ -1,5 +1,6 @@
 import os
 import sys
+from typing import List
 
 from piper.tools.executable import Executable
 
@@ -11,5 +12,7 @@ def __init__(self, path: str = None, pre_command: str = None):
         super().__init__(self.path, pre_command=pre_command)
         # TODO version from Popen
 
+    def replace_process(self, command: List[str]):
+        os.execv(self.path, [os.path.basename(os.path.normpath(self.path)), *command])
 
 main_python = Python()
diff --git a/src/piper/package/piper/tools/shell.py b/src/piper/package/piper/tools/shell.py
new file mode 100644
index 0000000..e6be843
--- /dev/null
+++ b/src/piper/package/piper/tools/shell.py
@@ -0,0 +1,97 @@
+import logging
+import queue
+import subprocess
+from queue import Queue
+from threading import Thread
+from typing import Optional
+
+logger = logging.getLogger(__name__)
+
+_STDOUT = "stdout"
+_STDERR = "stderr"
+
+
+class Shell:
+
+    def __init__(self, pre_command: str = None):
+        self._pre_command = f"{pre_command} && " if pre_command else ""
+
+    def run(self, command: str, capture: bool = False, text: bool = True, cwd: str = None) -> Optional[subprocess.CompletedProcess]:
+        if capture:
+            return self.captured_run(command, text=text, cwd=cwd)
+        else:
+            self.live_run(command, text=text, cwd=cwd)
+
+    def captured_run(self, command: str, text: bool = True, cwd: str = None):
+        return subprocess.run(
+            self._pre_command + command,
+            check=True,
+            capture_output=True,
+            text=text,
+            shell=True,
+            cwd=cwd
+        )
+
+    def live_run(self, command: str, text: bool = True, cwd: str = None):
+
+        output = Queue()
+
+        def pipe(std, stream):
+            # Blocking for loop: the stream will be empty only on process termination
+            for line in stream:
+                output.put((std, line))
+            if not stream.closed:
+                stream.close()
+
+        # Instantiate the child process
+        process = subprocess.Popen(
+            self._pre_command + command,
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            text=text,
+            shell=True,
+            cwd=cwd
+        )
+
+        # Start the producers
+        stdout = Thread(target=pipe, args=(_STDOUT, process.stdout))
+        stdout.start()
+        stderr = Thread(target=pipe, args=(_STDERR, process.stderr))
+        stderr.start()
+
+        # Be the consumer
+        outcome = None
+        while outcome is None:
+
+            try:
+
+                try:
+                    # Get a line, waiting at most one second (cannot be less)
+                    line = output.get(timeout=0.2)
+                except queue.Empty:
+                    # Nothing to do
+                    pass
+                else:
+                    # Print that line
+                    std, msg = line
+                    msg = msg.rstrip("\r\n") if text else msg
+                    if std == _STDOUT:
+                        logger.info(msg)
+                    else:
+                        logger.error(msg)
+                finally:
+                    # Check if the process has ended
+                    outcome = process.poll()
+
+            except KeyboardInterrupt:
+
+                process.kill()
+                outcome = -9
+
+        # Release resources (if possible)
+        stdout.join(1)
+        stderr.join(1)
+        if outcome != 0:
+            exit(outcome)
+        return outcome
diff --git a/src/python/package/piper/tools/venv.py b/src/piper/package/piper/tools/venv.py
similarity index 68%
rename from src/python/package/piper/tools/venv.py
rename to src/piper/package/piper/tools/venv.py
index 03bfd13..22c33e8 100644
--- a/src/python/package/piper/tools/venv.py
+++ b/src/piper/package/piper/tools/venv.py
@@ -7,6 +7,7 @@
 from piper.tools.pip import Pip
 from piper.tools.python import Python
 from piper.tools.shell import Shell
+from piper.tools.python import main_python as default_python
 
 logger = logging.getLogger(__name__)
 
@@ -18,7 +19,7 @@
 
 class Virtualenv:
 
-    def __init__(self, main_python: Python, pipe: Pipe):
+    def __init__(self, pipe: Pipe, main_python: Python = default_python):
         self._main_python = main_python
         self._name = f"venv-{pipe.name}"
 
@@ -47,25 +48,29 @@ def update(self):
         # Install requirements
         logger.info("Installing requirements...")
         for pipe in [*self.pipe.flat_dependencies(), self.pipe]:
-            out = self.pip.run(f"install -e {pipe.setup_py_folder}")
-            logger.info(f"{out.stdout}, {out.stderr}")
+            self.pip.run(f"install -e {pipe.setup_py_folder}")
             if pipe.requirements:
-                out = self.pip.run(f"install {' '.join([f'{name}=={version}' for name, version in pipe.requirements.items()])}")
-                logger.info(f"{out.stdout}, {out.stderr}")
+                self.pip.run(f"install {' '.join([f'{name}=={version}' for name, version in pipe.requirements.items()])}")
         logger.info("Done.")
 
     def freeze(self):
 
-        # Pip freeze
-        output = self.pip.run(f"freeze").stdout
-        requirements = re.sub("[\r\n]+", "\n", output).split("\n")
+        try:
 
-        # Get real requirements and update the requirements.txt file
-        real_requirements = [req for req in requirements if _is_valid_requirement(req)]
-        with open(self.pipe.requirements_file, "w") as f:
-            for requirement in real_requirements:
-                f.write(requirement + "\n")
-            logger.info(f"Freezing {real_requirements}")
+            # Pip freeze
+            output = self.pip.run(f"freeze", capture=True).stdout
+            requirements = re.sub("[\r\n]+", "\n", output).split("\n")
+
+            # Get real requirements and update the requirements.txt file
+            real_requirements = [req for req in requirements if _is_valid_requirement(req)]
+            with open(self.pipe.requirements_file, "w") as f:
+                for requirement in real_requirements:
+                    f.write(requirement + "\n")
+                logger.info(f"Freezing {real_requirements}")
+
+        except subprocess.CalledProcessError as e:
+            logger.error(e.stderr)
+            exit(e.returncode)
 
 
 def _is_valid_requirement(requirement: str) -> bool:
diff --git a/src/piper/package/requirements.txt b/src/piper/package/requirements.txt
new file mode 100644
index 0000000..81fdc38
--- /dev/null
+++ b/src/piper/package/requirements.txt
@@ -0,0 +1,1 @@
+PyYAML>=5.4.1,<6.0.0
diff --git a/src/piper/package/setup.py b/src/piper/package/setup.py
new file mode 100644
index 0000000..233ce35
--- /dev/null
+++ b/src/piper/package/setup.py
@@ -0,0 +1,55 @@
+from setuptools import setup, find_packages
+import os
+
+
+# Function to retrieve resources files
+def _get_resources(package_name):
+    # Get all the resources (also on nested levels)
+    res_paths = os.path.join(package_name, "resources")
+    all_resources = [os.path.join(folder, file) for folder, _, files in os.walk(res_paths) for file in files]
+    # Remove the prefix: start just from "resources"
+    return [resource[resource.index("resources"):] for resource in all_resources]
+
+
+# Read requirements
+this_location = os.path.dirname(os.path.realpath(__file__))
+requirements_file = os.path.join(this_location, "requirements.txt")
+with open(requirements_file, "r") as f:
+    requirements = f.read().splitlines()
+
+# Read README
+readme_file = os.path.join(this_location, "README.md")
+with open(readme_file, "r", encoding="utf-8") as f:
+    readme = f.read()
+
+
+# Package configuration
+_NAME="piper-tools"
+setup(
+    name=_NAME,
+    version="",
+    description="A software manager for easy development and distribution of Python code",
+    packages=find_packages(),
+    package_data={"piper": _get_resources("piper")},
+    install_requires=requirements,
+    python_requires=">=3.7",
+    entry_points={
+        'console_scripts': [
+            'piper = piper.__main__:main',
+        ],
+    },
+
+    long_description=readme,
+    long_description_content_type="text/markdown",
+    url="https://github.com/piper-tools/piper",
+    author="Federico Pugliese",
+    author_email="federico.pugliese.wr@gmail.com",
+    license="Apache Software License, Version 2.0",
+    classifiers=[
+        "License :: OSI Approved :: Apache Software License",
+        "Development Status :: 3 - Alpha",
+        "Intended Audience :: Developers",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.7"
+    ]
+)
diff --git a/src/piperblue/package/LICENSE b/src/piperblue/package/LICENSE
new file mode 100644
index 0000000..8f28074
--- /dev/null
+++ b/src/piperblue/package/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2021 Federico Pugliese
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/src/piperblue/package/MANIFEST.in b/src/piperblue/package/MANIFEST.in
new file mode 100644
index 0000000..5ee0522
--- /dev/null
+++ b/src/piperblue/package/MANIFEST.in
@@ -0,0 +1,1 @@
+include requirements.txt
diff --git a/src/piperblue/package/README.md b/src/piperblue/package/README.md
new file mode 100644
index 0000000..2f87244
--- /dev/null
+++ b/src/piperblue/package/README.md
@@ -0,0 +1,3 @@
+# Piperblue
+
+The base package to extend to create a [Piper](https://github.com/piper-tools/piper) Blueprint.
diff --git a/src/piperblue/package/piperblue/__init__.py b/src/piperblue/package/piperblue/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/piperblue/package/piperblue/blueprint.py b/src/piperblue/package/piperblue/blueprint.py
new file mode 100644
index 0000000..429cf38
--- /dev/null
+++ b/src/piperblue/package/piperblue/blueprint.py
@@ -0,0 +1,59 @@
+import abc
+import os
+import re
+import shutil
+from typing import Dict, Any
+
+
+class PiperBlueprint(abc.ABC):
+
+    def generate(self, resources_folder: str, context: str, config: Dict[str, Any]) -> None:
+
+        # Copy the folder
+        pipe = os.path.join(resources_folder, "blueprint")
+        copy_tree(pipe, context)
+
+        # Dynamic content generation
+        self.dynamic_generate(resources_folder, context, config)
+    
+        # Make substitutions
+        self.substitute(context, config)
+
+    def substitute(self, context: str, config: Dict[str, Any]) -> None:
+
+        # Rename the files and folders
+        files_and_folders = self.get_file_and_folders_renamings(context, config)
+        for source, dest in files_and_folders.items():
+            os.rename(source, dest)
+
+        # Get substitutions to apply
+        substitutions = self.get_substitutions_in_files(config)
+        replacements = {re.escape("{{"+k+"}}"): v for k, v in substitutions.items()}
+        pattern = re.compile("|".join(replacements.keys()))
+        # Substitute placeholders
+        for root, _, files in os.walk(context):
+            for file in files:
+                # Read file
+                print(os.path.join(root,file))
+                with open(os.path.join(root, file), encoding="utf-8") as f:
+                    content = f.read()
+                # Replace
+                replaced = pattern.sub(lambda m: replacements[re.escape(m.group(0))], content)
+                # Rewrite file
+                with open(os.path.join(root, file), "w", encoding="utf-8") as f:
+                    f.write(replaced)
+
+    def dynamic_generate(self, resources_folder: str, context: str, blueprint: Dict[str, Any]) -> None:
+        pass
+
+    @abc.abstractmethod
+    def get_file_and_folders_renamings(self, context: str, config: Dict[str, Any]) -> Dict[str, str]:
+        pass
+
+    @abc.abstractmethod
+    def get_substitutions_in_files(self, config: Dict[str, Any]) -> Dict[str, str]:
+        pass
+
+
+def copy_tree(source, destination):
+    return shutil.copytree(source, destination, ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
diff --git a/src/piperblue/package/requirements.txt b/src/piperblue/package/requirements.txt
new file mode 100644
index 0000000..e69de29
diff --git a/src/piperblue/package/setup.py b/src/piperblue/package/setup.py
new file mode 100644
index 0000000..a9c5528
--- /dev/null
+++ b/src/piperblue/package/setup.py
@@ -0,0 +1,50 @@
+from setuptools import setup, find_packages
+import os
+
+
+# Function to retrieve resources files
+def _get_resources(package_name):
+    # Get all the resources (also on nested levels)
+    res_paths = os.path.join(package_name, "resources")
+    all_resources = [os.path.join(folder, file) for folder, _, files in os.walk(res_paths) for file in files]
+    # Remove the prefix: start just from "resources"
+    return [resource[resource.index("resources"):] for resource in all_resources]
+
+
+# Read requirements
+this_location = os.path.dirname(os.path.realpath(__file__))
+requirements_file = os.path.join(this_location, "requirements.txt")
+with open(requirements_file, "r") as f:
+    requirements = f.read().splitlines()
+
+# Read README
+readme_file = os.path.join(this_location, "README.md")
+with open(readme_file, "r", encoding="utf-8") as f:
+    readme = f.read()
+
+
+# Package configuration
+_NAME="piperblue"
+setup(
+    name=_NAME,
+    version="0.0.0",
+    description="The base package to extend to create a Piper Blueprint",
+    packages=find_packages(),
+    package_data={"piperblue": _get_resources("piperblue")},
+    install_requires=requirements,
+    python_requires=">=3.7",
+
+    long_description=readme,
+    long_description_content_type="text/markdown",
+    url="https://github.com/piper-tools/piper",
+    author="Federico Pugliese",
+    author_email="federico.pugliese.wr@gmail.com",
+    license="Apache Software License, Version 2.0",
+    classifiers=[
+        "License :: OSI Approved :: Apache Software License",
+        "Development Status :: 3 - Alpha",
+        "Intended Audience :: Developers",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.7"
+    ]
+)
diff --git a/src/piperblue/pipe.yml b/src/piperblue/pipe.yml
new file mode 100644
index 0000000..42e7d60
--- /dev/null
+++ b/src/piperblue/pipe.yml
@@ -0,0 +1,1 @@
+python: 3.7.4
diff --git a/src/python/package/piper/__main__.py b/src/python/package/piper/__main__.py
deleted file mode 100644
index 5b30b61..0000000
--- a/src/python/package/piper/__main__.py
+++ /dev/null
@@ -1,58 +0,0 @@
-import argparse
-import logging
-import os
-from enum import Enum
-
-from piper.cli import runner
-from piper.cli.cleaner import Cleaner
-from piper.cli.setupper import Setupper
-
-logger = logging.getLogger(__name__)
-
-
-class Action(str, Enum):
-
-    RUN = "run"
-    SETUP = "setup"
-    CLEAN = "clean"
-
-
-def get_args():
-
-    parser = argparse.ArgumentParser()
-    parser.add_argument("action", choices=[v.value for v in Action])
-    parser.add_argument("--pipe", required=False)
-    args, _ = parser.parse_known_args()
-    return args
-
-
-def main():
-
-    # Get args
-    args = get_args()
-
-    # Parse
-    action = args.action
-    pipe_location = args.pipe or os.getcwd()
-
-    if action == Action.RUN:
-
-        runner.run(pipe_location=pipe_location)
-
-    elif action == Action.SETUP:
-
-        Setupper().manage(pipe_location=pipe_location)
-
-    elif action == Action.CLEAN:
-
-        Cleaner().manage(pipe_location=pipe_location)
-
-    else:
-
-        raise RuntimeError(f"Action {action} unknown")
-
-    logger.info("All done.")
-
-
-if __name__ == '__main__':
-    main()
diff --git a/src/python/package/piper/cli/runner.py b/src/python/package/piper/cli/runner.py
deleted file mode 100644
index 26cef5e..0000000
--- a/src/python/package/piper/cli/runner.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from piper.cli.setupper import Setupper
-
-
-def run(pipe_location: str, script: str = None):
-
-    # Setup the pipes and dependencies
-    Setupper().manage(pipe_location)
-
-    # Run the code
-    pass
diff --git a/src/python/package/piper/tools/pip.py b/src/python/package/piper/tools/pip.py
deleted file mode 100644
index cfed4ee..0000000
--- a/src/python/package/piper/tools/pip.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from piper.tools.executable import Executable
-
-
-class Pip(Executable):
-
-    def __init__(self, pre_command: str = None):
-        super().__init__("pip", pre_command=pre_command)
-
diff --git a/src/python/package/piper/tools/shell.py b/src/python/package/piper/tools/shell.py
deleted file mode 100644
index 96d15f2..0000000
--- a/src/python/package/piper/tools/shell.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import logging
-import os
-import subprocess
-import shlex
-
-
-logger = logging.getLogger(__name__)
-
-
-class Shell:
-
-    def __init__(self, pre_command: str = None):
-        self._pre_command = f"{pre_command} && " if pre_command else ""
-
-    def run(self, command: str) -> subprocess.CompletedProcess:
-        return subprocess.run(
-            self._pre_command + command,
-            check=True,
-            capture_output=True,
-            text=True,
-            shell=True
-        )
diff --git a/src/python/package/requirements.txt b/src/python/package/requirements.txt
deleted file mode 100644
index 1fb7f67..0000000
--- a/src/python/package/requirements.txt
+++ /dev/null
@@ -1,1 +0,0 @@
-PyYAML==5.4.1


Test patch:
diff --git a/devops/piper/test.sh b/devops/piper/test.sh
new file mode 100644
index 0000000..ab76104
--- /dev/null
+++ b/devops/piper/test.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+cd src/piper/tests/data/complexproject
+piper setup
+cd microservices/second/
+venv-second/bin/python package/second/__init__.py
+piper clean
diff --git a/devops/piperblue/test.sh b/devops/piperblue/test.sh
new file mode 100644
index 0000000..02078dc
--- /dev/null
+++ b/devops/piperblue/test.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+# Testing the blueprint
+pip install -e src/piperblue/tests/data/piperblueexample/package
+python -m pytest src/piperblue/tests/src/
diff --git a/src/python/package/tests/__init__.py b/src/piper/tests/__init__.py
similarity index 100%
rename from src/python/package/tests/__init__.py
rename to src/piper/tests/__init__.py
diff --git a/src/piper/tests/data/blueprint/complex/blueprint.yml b/src/piper/tests/data/blueprint/complex/blueprint.yml
new file mode 100644
index 0000000..24aee82
--- /dev/null
+++ b/src/piper/tests/data/blueprint/complex/blueprint.yml
@@ -0,0 +1,31 @@
+pipes:
+
+  common:
+    blueprint:
+      piperbluebasic: 0.0.0
+    config:
+      package:
+        name: common
+        description: bla
+      python: 3.7.4
+
+  microservices:
+    pipes:
+
+      first:
+        blueprint:
+          piperbluebasic: 0.0.0
+        config:
+          package:
+            name: first
+            description: bla
+          python: 3.7.4
+
+      second:
+        blueprint:
+          piperbluebasic: 0.0.0
+        config:
+          package:
+            name: second
+            description: bla
+          python: 3.7.4
diff --git a/src/piper/tests/data/complexproject/common/package/MANIFEST.in b/src/piper/tests/data/complexproject/common/package/MANIFEST.in
new file mode 100644
index 0000000..5ee0522
--- /dev/null
+++ b/src/piper/tests/data/complexproject/common/package/MANIFEST.in
@@ -0,0 +1,1 @@
+include requirements.txt
diff --git a/src/python/package/tests/data/complexproject/common/package/common/__init__.py b/src/piper/tests/data/complexproject/common/package/common/__init__.py
similarity index 100%
rename from src/python/package/tests/data/complexproject/common/package/common/__init__.py
rename to src/piper/tests/data/complexproject/common/package/common/__init__.py
diff --git a/src/python/package/tests/data/complexproject/common/package/setup.py b/src/piper/tests/data/complexproject/common/package/setup.py
similarity index 100%
rename from src/python/package/tests/data/complexproject/common/package/setup.py
rename to src/piper/tests/data/complexproject/common/package/setup.py
diff --git a/src/python/package/tests/data/complexproject/common/pipe.yml b/src/piper/tests/data/complexproject/common/pipe.yml
similarity index 100%
rename from src/python/package/tests/data/complexproject/common/pipe.yml
rename to src/piper/tests/data/complexproject/common/pipe.yml
diff --git a/src/piper/tests/data/complexproject/microservices/first/package/MANIFEST.in b/src/piper/tests/data/complexproject/microservices/first/package/MANIFEST.in
new file mode 100644
index 0000000..5ee0522
--- /dev/null
+++ b/src/piper/tests/data/complexproject/microservices/first/package/MANIFEST.in
@@ -0,0 +1,1 @@
+include requirements.txt
diff --git a/src/python/package/tests/data/complexproject/microservices/first/package/first/__init__.py b/src/piper/tests/data/complexproject/microservices/first/package/first/__init__.py
similarity index 100%
rename from src/python/package/tests/data/complexproject/microservices/first/package/first/__init__.py
rename to src/piper/tests/data/complexproject/microservices/first/package/first/__init__.py
diff --git a/src/python/package/tests/data/complexproject/microservices/first/package/setup.py b/src/piper/tests/data/complexproject/microservices/first/package/setup.py
similarity index 100%
rename from src/python/package/tests/data/complexproject/microservices/first/package/setup.py
rename to src/piper/tests/data/complexproject/microservices/first/package/setup.py
diff --git a/src/python/package/tests/data/complexproject/microservices/first/pipe.yml b/src/piper/tests/data/complexproject/microservices/first/pipe.yml
similarity index 100%
rename from src/python/package/tests/data/complexproject/microservices/first/pipe.yml
rename to src/piper/tests/data/complexproject/microservices/first/pipe.yml
diff --git a/src/python/package/tests/data/complexproject/microservices/pipe.yml b/src/piper/tests/data/complexproject/microservices/pipe.yml
similarity index 100%
rename from src/python/package/tests/data/complexproject/microservices/pipe.yml
rename to src/piper/tests/data/complexproject/microservices/pipe.yml
diff --git a/src/piper/tests/data/complexproject/microservices/second/package/second/MANIFEST.in b/src/piper/tests/data/complexproject/microservices/second/package/second/MANIFEST.in
new file mode 100644
index 0000000..5ee0522
--- /dev/null
+++ b/src/piper/tests/data/complexproject/microservices/second/package/second/MANIFEST.in
@@ -0,0 +1,1 @@
+include requirements.txt
diff --git a/src/python/package/tests/data/complexproject/microservices/second/package/second/__init__.py b/src/piper/tests/data/complexproject/microservices/second/package/second/__init__.py
similarity index 100%
rename from src/python/package/tests/data/complexproject/microservices/second/package/second/__init__.py
rename to src/piper/tests/data/complexproject/microservices/second/package/second/__init__.py
diff --git a/src/python/package/tests/data/complexproject/microservices/second/package/setup.py b/src/piper/tests/data/complexproject/microservices/second/package/setup.py
similarity index 100%
rename from src/python/package/tests/data/complexproject/microservices/second/package/setup.py
rename to src/piper/tests/data/complexproject/microservices/second/package/setup.py
diff --git a/src/python/package/tests/data/complexproject/microservices/second/pipe.yml b/src/piper/tests/data/complexproject/microservices/second/pipe.yml
similarity index 100%
rename from src/python/package/tests/data/complexproject/microservices/second/pipe.yml
rename to src/piper/tests/data/complexproject/microservices/second/pipe.yml
diff --git a/src/python/package/tests/data/complexproject/pipe.yml b/src/piper/tests/data/complexproject/pipe.yml
similarity index 100%
rename from src/python/package/tests/data/complexproject/pipe.yml
rename to src/piper/tests/data/complexproject/pipe.yml
diff --git a/src/piper/tests/data/simpleproject/package/MANIFEST.in b/src/piper/tests/data/simpleproject/package/MANIFEST.in
new file mode 100644
index 0000000..5ee0522
--- /dev/null
+++ b/src/piper/tests/data/simpleproject/package/MANIFEST.in
@@ -0,0 +1,1 @@
+include requirements.txt
diff --git a/src/python/package/tests/data/simpleproject/package/setup.py b/src/piper/tests/data/simpleproject/package/setup.py
similarity index 100%
rename from src/python/package/tests/data/simpleproject/package/setup.py
rename to src/piper/tests/data/simpleproject/package/setup.py
diff --git a/src/python/package/tests/pipefiles/__init__.py b/src/piper/tests/data/simpleproject/package/simple/__init__.py
similarity index 100%
rename from src/python/package/tests/pipefiles/__init__.py
rename to src/piper/tests/data/simpleproject/package/simple/__init__.py
diff --git a/src/python/package/tests/data/simpleproject/pipe.yml b/src/piper/tests/data/simpleproject/pipe.yml
similarity index 100%
rename from src/python/package/tests/data/simpleproject/pipe.yml
rename to src/piper/tests/data/simpleproject/pipe.yml
diff --git a/src/piper/tests/pipefiles/__init__.py b/src/piper/tests/pipefiles/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/python/package/tests/pipefiles/test_pipe_file_reader.py b/src/piper/tests/pipefiles/test_pipe_file_reader.py
similarity index 100%
rename from src/python/package/tests/pipefiles/test_pipe_file_reader.py
rename to src/piper/tests/pipefiles/test_pipe_file_reader.py
diff --git a/src/piperblue/tests/blueprint.py b/src/piperblue/tests/blueprint.py
new file mode 100644
index 0000000..298e54b
--- /dev/null
+++ b/src/piperblue/tests/blueprint.py
@@ -0,0 +1,28 @@
+import os
+import sys
+from pathlib import Path
+
+from piperblueexample.blueprint import PiperBlueprintExample
+
+
+if __name__ == '__main__':
+
+    resources_folder = str(Path(sys.modules[PiperBlueprintExample.__module__].__file__).parent / "resources")
+
+    context = "blueprint"
+
+    yml = {
+        "package": {
+            "name": "blueprint",
+            "description": "bla"
+        },
+        "python": "3.7.4"
+    }
+
+    PiperBlueprintExample().generate(resources_folder, context, yml)
+
+    # Assertions
+    assert os.path.isdir(context)
+    assert os.path.isdir(os.path.join(context, "package", "blueprint"))
+    with open(os.path.join(context, "pipe.yml")) as f:
+        assert "3.7.4" in f.read()
diff --git a/src/piperblue/tests/data/piperblueexample/package/LICENSE b/src/piperblue/tests/data/piperblueexample/package/LICENSE
new file mode 100644
index 0000000..8f28074
--- /dev/null
+++ b/src/piperblue/tests/data/piperblueexample/package/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2021 Federico Pugliese
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/src/piperblue/tests/data/piperblueexample/package/MANIFEST.in b/src/piperblue/tests/data/piperblueexample/package/MANIFEST.in
new file mode 100644
index 0000000..5ee0522
--- /dev/null
+++ b/src/piperblue/tests/data/piperblueexample/package/MANIFEST.in
@@ -0,0 +1,1 @@
+include requirements.txt
diff --git a/src/piperblue/tests/data/piperblueexample/package/piperblueexample/__init__.py b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/__init__.py
new file mode 100644
index 0000000..53231a2
--- /dev/null
+++ b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/__init__.py
@@ -0,0 +1,4 @@
+import os
+
+PACKAGE_LOCATION = os.path.join(os.path.dirname(os.path.realpath(__file__)))
+RESOURCES_LOCATION = os.path.join(PACKAGE_LOCATION, "resources")
diff --git a/src/piperblue/tests/data/piperblueexample/package/piperblueexample/blueprint.py b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/blueprint.py
new file mode 100644
index 0000000..729d4dc
--- /dev/null
+++ b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/blueprint.py
@@ -0,0 +1,29 @@
+import os
+from typing import Dict, Any
+
+from piperblue.blueprint import PiperBlueprint
+
+
+class PiperBlueprintExample(PiperBlueprint):
+
+    def get_file_and_folders_renamings(self, context: str, config: Dict[str, Any]) -> Dict[str, str]:
+
+        # Parse the config
+        package_name = config["package"]["name"]
+        package_folder = os.path.join(context, "package")
+        return {
+            os.path.join(package_folder, "packagename"): os.path.join(package_folder, package_name)
+        }
+
+    def get_substitutions_in_files(self, config: Dict[str, Any]) -> Dict[str, str]:
+
+        # Parse the config
+        python = config["python"]
+        package = config["package"]
+        package_name = package["name"]
+        description = package["description"]
+        return {
+            "package_name": package_name,
+            "description": description,
+            "python_version": python
+        }
diff --git a/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/package/MANIFEST.in b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/package/MANIFEST.in
new file mode 100644
index 0000000..5ee0522
--- /dev/null
+++ b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/package/MANIFEST.in
@@ -0,0 +1,1 @@
+include requirements.txt
diff --git a/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/package/packagename/__init__.py b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/package/packagename/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/package/setup.py b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/package/setup.py
new file mode 100644
index 0000000..0630d8b
--- /dev/null
+++ b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/package/setup.py
@@ -0,0 +1,19 @@
+from setuptools import setup, find_packages
+import os
+
+
+# Read requirements
+requirements_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.txt")
+with open(requirements_file, "r") as f:
+    requirements = f.read().splitlines()
+
+
+# Package configuration
+_NAME = "{{package_name}}"
+setup(
+    name=_NAME,
+    version="0.0.0",
+    description="{{description}}",
+    packages=find_packages(),
+    install_requires=requirements
+)
diff --git a/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/pipe.yml b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/pipe.yml
new file mode 100644
index 0000000..d08d7c2
--- /dev/null
+++ b/src/piperblue/tests/data/piperblueexample/package/piperblueexample/resources/blueprint/pipe.yml
@@ -0,0 +1,1 @@
+python: {{python_version}}
diff --git a/src/piperblue/tests/data/piperblueexample/package/requirements.txt b/src/piperblue/tests/data/piperblueexample/package/requirements.txt
new file mode 100644
index 0000000..d3f5a12
--- /dev/null
+++ b/src/piperblue/tests/data/piperblueexample/package/requirements.txt
@@ -0,0 +1,1 @@
+
diff --git a/src/python/package/setup.py b/src/piperblue/tests/data/piperblueexample/package/setup.py
similarity index 60%
rename from src/python/package/setup.py
rename to src/piperblue/tests/data/piperblueexample/package/setup.py
index d37f3c1..689320f 100644
--- a/src/python/package/setup.py
+++ b/src/piperblue/tests/data/piperblueexample/package/setup.py
@@ -12,15 +12,18 @@ def _get_resources(package_name):
 
 
 # Read requirements
-requirements_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "requirements.txt")
+this_location = os.path.dirname(os.path.realpath(__file__))
+requirements_file = os.path.join(this_location, "requirements.txt")
 with open(requirements_file, "r") as f:
     requirements = f.read().splitlines()
 
 
 # Package configuration
-setup(name="piper",
-      version="0.0.0",
-      description="A Snake Charmer to manage you Python code",
-      packages=find_packages(),
-      package_data={"piper": _get_resources("piper")},
-      install_requires=requirements)
+setup(
+    name="piperblueexample",
+    version="0.0.0",
+    description="An example of Piper Blueprint",
+    packages=find_packages(),
+    package_data={"piperblueexample": _get_resources("piperblueexample")},
+    install_requires=requirements
+)
diff --git a/src/piperblue/tests/src/test_blueprint.py b/src/piperblue/tests/src/test_blueprint.py
new file mode 100644
index 0000000..7103a5e
--- /dev/null
+++ b/src/piperblue/tests/src/test_blueprint.py
@@ -0,0 +1,36 @@
+import os
+import sys
+from pathlib import Path
+
+from piperblueexample.blueprint import PiperBlueprintExample
+
+
+def test_generation():
+
+    # Parameters
+    resources_folder = str(Path(sys.modules[PiperBlueprintExample.__module__].__file__).parent / "resources")
+    context = "example"
+    name = "thename"
+    description = "blabla"
+    version = "3.7.4"
+    yml = {
+        "package": {
+            "name": name,
+            "description": description
+        },
+        "python": version
+    }
+
+    # Generate
+    PiperBlueprintExample().generate(resources_folder, context, yml)
+
+    # Folders created and renamed
+    assert os.path.isdir(context)
+    package_folder = os.path.join(context, "package")
+    assert os.path.isdir(os.path.join(package_folder, name))
+
+    # Placeholders substituted
+    with open(os.path.join(package_folder, "setup.py")) as f:
+        assert f'description="{description}"' in f.read()
+    with open(os.path.join(context, "pipe.yml")) as f:
+        assert version in f.read()