diff --git a/.github/workflows/build-analysis-test.yml b/.github/workflows/build-analysis-test.yml new file mode 100644 index 0000000..f895e1c --- /dev/null +++ b/.github/workflows/build-analysis-test.yml @@ -0,0 +1,61 @@ +name: Python application + +on: + push: + branches: + - '**' + tags-ignore: + - '**' + pull_request: + +jobs: + setup: + runs-on: self-hosted + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Install dependencies + env: + PIP_INDEX_EXTRA_URL_REMAINDER: ${{ secrets.PIP_INDEX_EXTRA_URL_REMAINDER }} + PIP_USERNAME: ${{ secrets.PIP_USERNAME }} + PIP_PASSWORD: ${{ secrets.PIP_PASSWORD }} + PIP_PROTOCOL: ${{ vars.PIP_PROTOCOL }} + run: | + python -m venv .venv + source .venv/bin/activate + python -m pip install --upgrade pip + python -m pip install -e .[test,analyze] \ + --no-cache-dir \ + --extra-index-url $PIP_PROTOCOL://$PIP_USERNAME:$PIP_PASSWORD@$PIP_INDEX_EXTRA_URL_REMAINDER \ + --trusted-host $PIP_INDEX_EXTRA_URL_REMAINDER + analysis: + needs: setup + runs-on: self-hosted + steps: + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Lint (Pylint) + run: source .venv/bin/activate && pylint src + - name: Format check (Black) + run: source .venv/bin/activate && black --check src + - name: Format check (Isort) + run: source .venv/bin/activate && isort --check src + - name: Type check (Pyright) + run: source .venv/bin/activate && pyright src + - name: Security scan (Bandit) + run: source .venv/bin/activate && bandit -r src + unittests: + needs: analysis + runs-on: self-hosted + steps: + - name: Set up Python 3.11 + uses: actions/setup-python@v3 + with: + python-version: "3.11" + - name: Unittests (Pytest) + run: source .venv/bin/activate && pytest tests/unittests \ No newline at end of file diff --git a/.github/workflows/build-pypi-docker.yml b/.github/workflows/build-pypi-docker.yml new file mode 100644 index 0000000..088c6c2 --- /dev/null +++ b/.github/workflows/build-pypi-docker.yml @@ -0,0 +1,62 @@ +name: Tag creation + +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 +jobs: + pypi: + runs-on: self-hosted + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Update VERSION file + run: echo "${{ github.ref_name }}" | cut -c 2- > VERSION + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e '.[build,publish]' + - name: Build and publish + env: + PIP_USERNAME: ${{ secrets.PIP_USERNAME }} + PIP_PASSWORD: ${{ secrets.PIP_PASSWORD }} + PIP_INDEX_EXTRA_URL_REMAINDER: ${{ secrets.PIP_INDEX_EXTRA_URL_REMAINDER }} + PIP_PROTOCOL: ${{ vars.PIP_PROTOCOL }} + run: | + python -m build + twine upload --verbose \ + --repository-url $PIP_PROTOCOL://$PIP_INDEX_EXTRA_URL_REMAINDER \ + -u $PIP_USERNAME -p $PIP_PASSWORD dist/* + docker: + needs: pypi + runs-on: self-hosted + steps: + - name: Save package name + run: echo $(ls dist | grep .tar.gz | cut -d '-' -f 1) > DOCKER_CONTAINER_NAME + - name: Build docker image + env: + DOCKER_HUB_URL: ${{ secrets.DOCKER_HUB_URL }} + PIP_INDEX_EXTRA_URL_REMAINDER: ${{ secrets.PIP_INDEX_EXTRA_URL_REMAINDER }} + PIP_INDEX_EXTRA_URL: ${{ vars.PIP_PROTOCOL }}://${{ secrets.PIP_USERNAME }}:${{ secrets.PIP_PASSWORD }}@${{ secrets.PIP_INDEX_EXTRA_URL_REMAINDER }} + run: | + echo $PIP_INDEX_EXTRA_URL > /FILE && \ + docker build -t $DOCKER_HUB_URL/$(cat DOCKER_CONTAINER_NAME):$(cat VERSION) \ + --secret id=PIP_INDEX_EXTRA_URL,env=PIP_INDEX_EXTRA_URL \ + --progress=plain \ + --no-cache \ + --network=host \ + --add-host $PIP_INDEX_EXTRA_URL_REMAINDER:$(dig +short $PIP_INDEX_EXTRA_URL_REMAINDER) \ + . + - name: Push docker image and cleanup + env: + DOCKER_HUB_URL: ${{ secrets.DOCKER_HUB_URL }} + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} + run: | + echo $DOCKER_HUB_PASSWORD | docker login $DOCKER_HUB_URL -u $DOCKER_HUB_USERNAME --password-stdin + docker push $DOCKER_HUB_URL/$(cat DOCKER_CONTAINER_NAME):$(cat VERSION) + docker rmi $DOCKER_HUB_URL/$(cat DOCKER_CONTAINER_NAME):$(cat VERSION) + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d60ce0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# general things to ignore +build/ +dist/ +*.egg-info/ +*.egg +*.py[cod] +__pycache__/ +*.so +*~ + +# due to using tox and pytest +.tox +.cache + +# due to using vscode +.vscode/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3e26a0d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 AGISwarm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ffac69a --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Python Template Project + +This repository provides a Python project template optimized for development, testing, and CI/CD integration using Docker. + +## Project Structure + +- `src/AGISwarm/python_template/`: Main package. + - `__init__.py`: Package initialization. + - `__main__.py`: Main module logic. + - `py.typed`: Marker file for PEP 561 typing. +- `pyproject.toml`: Specifies build system requirements and project dependencies. +- `tests/`: Unit and functional tests. +- `.github/workflows/`: + - `build-analysis-test.yml`: CI for build and test. + - `build-pypi-docker.yml`: CI for Docker and PyPI deployment. +- `dockerfile`: Docker setup for Python. + +## Features + +- Automated tests with examples. +- CI/CD via GitHub Actions. +- Docker integration for deployment. + +## Getting Started + +### Installation + +1. Import the template repository to your GitHub account. + +2. Clone the repository: + ```bash + git clone + ``` +3. Navigate to the project directory: + ```bash + cd + ``` +4. Adapt the project to your needs: + 1. Modify the project name in `pyproject.toml` and `src/AGISwarm/python_template/__init__.py` to match your project name. + 2. Change namespace `AGISwarm` to your own namespace. + 3. Add your code to `src///` folder. + 4. Write your tests in `tests/` folder. + 1. Unittests will be run automatically by GitHub Actions every time you push to the repository. Functional tests will be not. + +5. Install dependencies: + ```bash + pip install . + ``` + +### CICD Setup + +For testing and deployment in a CI/CD setup, refer to our [CICD project](https://github.com/AGISwarm/CICD). + +### Usage + +#### Command Line + +```bash +python -m . +``` + +#### Docker + +1. Build python package: + ```bash + python -m build + ``` +2. Create PIP_INDEX_EXTRA_URL environment variable to be able to install dependencies from a self-hosted PyPI server. + + Given URL is self-hosted PyPI server from [CICD](README.md#cicd-setup) setup. + Replace with your own if needed. + Or remove secret mounting in Dockerfile, if no need. + ```bash + export PIP_INDEX_EXTRA_URL=http://pypi-server/ + ``` +3. Build Docker image. Use this command for testing purposes only. If you use [CICD](README.md#cicd-setup) setup, it will build and push the image for you after creating a tag in your repository: + ```bash + docker build -t --secret id=PIP_INDEX_EXTRA_URL,env=PIP_INDEX_EXTRA_URL . + ``` +4. Run Docker container: + ```bash + docker run + ``` + + +## License + +Distributed under the MIT License. See `LICENSE.txt` for details. + +## Contact + +Denis Diachkov - diachkov.da@gmail.com \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..bd52db8 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.0 \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..06dab5e --- /dev/null +++ b/dockerfile @@ -0,0 +1,14 @@ +FROM python:3.11-slim + + +COPY dist /dist + +RUN mkdir -p /root/pip +RUN --mount=type=secret,id=PIP_INDEX_EXTRA_URL,target=/PIP_INDEX_EXTRA_URL \ + python -m pip install --upgrade pip && \ + python -m pip install /dist/*.whl --no-cache-dir \ + --extra-index-url $(cat /PIP_INDEX_EXTRA_URL) \ + --trusted-host $(cat /PIP_INDEX_EXTRA_URL | awk -F/ '{print $3}' | awk -F@ '{print $2}') + +RUN echo $(ls /dist/*.whl | sed 's/.*\///' | sed 's/-.*//') > /PACKAGE_NAME +ENTRYPOINT python -m $(cat /PACKAGE_NAME) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fac5d2a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "AGISwarm.python-template" +dynamic = ["version"] +description = "Python template project" +readme = "README.md" + +requires-python = ">=3.11" +license = { file = "LICENSE.txt" } +keywords = ["sample", "setuptools", "development"] +classifiers = [ + "Programming Language :: Python :: 3", +] +dependencies = ['numpy'] +[project.optional-dependencies] +test = ['pytest'] +analyze = ['pyright', 'pylint', 'bandit', 'black', 'isort'] +build = ['setuptools', 'wheel', 'build'] +publish = ['twine'] + + +[tool.setuptools.dynamic] +version = { file = "VERSION" } + + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +python_template = ["py.typed"] diff --git a/src/AGISwarm/python_template/__init__.py b/src/AGISwarm/python_template/__init__.py new file mode 100644 index 0000000..932be88 --- /dev/null +++ b/src/AGISwarm/python_template/__init__.py @@ -0,0 +1,6 @@ +"""__init__.py +""" + +from importlib.metadata import version + +__version__ = version("AGISwarm.python_template") diff --git a/src/AGISwarm/python_template/__main__.py b/src/AGISwarm/python_template/__main__.py new file mode 100644 index 0000000..826ad60 --- /dev/null +++ b/src/AGISwarm/python_template/__main__.py @@ -0,0 +1,12 @@ +"""Main.py""" + +from . import __version__ + + +def main(): + """Main function""" + print(__version__) + + +if __name__ == "__main__": + main() diff --git a/src/AGISwarm/python_template/py.typed b/src/AGISwarm/python_template/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/functional_tests/test_functional_example.py b/tests/functional_tests/test_functional_example.py new file mode 100644 index 0000000..c59e721 --- /dev/null +++ b/tests/functional_tests/test_functional_example.py @@ -0,0 +1,8 @@ +"""Example test file for pytest.""" + +import pytest + + +def test_example(): + """Example test function.""" + assert 1 == 1 diff --git a/tests/unittests/test_example.py b/tests/unittests/test_example.py new file mode 100644 index 0000000..50be652 --- /dev/null +++ b/tests/unittests/test_example.py @@ -0,0 +1,7 @@ +"""Example test file for pytest.""" + +import pytest + + +def test_example(): + assert 1 == 1