diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f94b59c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +**/*.egg-info +**/__pycache__ +env/ +venv/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index ab77e2b..b6e4761 100644 --- a/.gitignore +++ b/.gitignore @@ -109,7 +109,6 @@ venv/ ENV/ env.bak/ venv.bak/ -miniconda3/ # Spyder project settings .spyderproject @@ -128,7 +127,3 @@ dmypy.json # Pyre type checker .pyre/ - -# Miniconda -Miniconda3* -sha256sum.txt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..937daed --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1 +FROM python:3.10-slim AS builder +RUN python3 -m pip install -U pip wheel setuptools +WORKDIR /project/install +COPY requirements.py requirements.py +COPY setup.cfg setup.cfg +RUN python3 requirements.py > requirements.txt +RUN python3 -m pip install -r requirements.txt + +FROM builder +COPY EXCLUDE EXCLUDE +COPY LICENSE LICENSE +COPY MANIFEST.in MANIFEST.in +COPY pyproject.toml pyproject.toml +COPY README.md README.md +COPY src src +COPY tests tests +RUN python3 -m pip install .[develop] +ENTRYPOINT [ "/usr/local/bin/python3", "-m", "build"] \ No newline at end of file diff --git a/EXCLUDE b/EXCLUDE index 35a0363..2969a91 100644 --- a/EXCLUDE +++ b/EXCLUDE @@ -1,7 +1,6 @@ *__pycache__* *.py[cod] *$py.class -miniconda3/* -sha256sum.txt -Miniconda3-py* *.pytest_cache* +env/* +venv/* diff --git a/LICENSE b/LICENSE index 45ae65f..372ec07 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Jason A. Regina +Copyright (c) 2023 Author Name Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 700f182..22007a5 100644 --- a/Makefile +++ b/Makefile @@ -1,34 +1,20 @@ -REPOSITORY=https://repo.anaconda.com/miniconda -INSTALLER=Miniconda3-py38_4.9.2-Linux-x86_64.sh -HASH=1314b90489f154602fd794accfc90446111514a5a72fe1f71ab83e07de9504a7 -HASHFILE=sha256sum.txt -PYENV=miniconda3 -PYTHON=$(PYENV)/bin/python3 +TAG=my-tag +APP=my-cli +ARGUMENTS=-c 5 -r 7 -.PHONY: develop tests build checksum clean +.PHONY: build setup run tests -develop: $(PYENV)/bin/activate - $(PYTHON) -m pip install -e .[develop] +build: tests + docker run --name $(TAG)-builder $(TAG) + docker cp $(TAG)-builder:/project/install/dist $(PWD)/dist + docker stop $(TAG)-builder + docker rm $(TAG)-builder -tests: develop - $(PYTHON) -m pytest -s +setup: + docker build -t $(TAG) . -build: $(PYENV)/bin/activate - $(PYTHON) -m build +run: setup + docker run --entrypoint "/usr/local/bin/$(APP)" --rm $(TAG) $(ARGUMENTS) -$(PYENV)/bin/activate: checksum - test -d $(PYENV) || bash ./$(INSTALLER) -b -p $(PYENV) - $(PYTHON) -m pip install -U pip wheel setuptools build - touch $(PYENV)/bin/activate - -checksum: $(INSTALLER) $(HASHFILE) - sha256sum -c $(HASHFILE) - -$(INSTALLER): - wget $(REPOSITORY)/$(INSTALLER) - -$(HASHFILE): - echo "$(HASH) $(INSTALLER)" > $(HASHFILE) - -clean: - rm -rf dist/ $(PYENV) $(HASHFILE) $(INSTALLER) +tests: setup + docker run --entrypoint "/usr/local/bin/pytest" --rm $(TAG) \ No newline at end of file diff --git a/README.md b/README.md index 7fc231b..0d83b3b 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,44 @@ # Generic Python Project -This template can be used as a starting point for building distributable Python packages. It includes a Makefile with targets to build a wheel and a reproducible Python virtual environment based on miniconda. To learn more about packaging in Python see the [Python Packaging User Guide](https://packaging.python.org/). +This template can be used as a starting point for building distributable Python packages. It includes a Makefile with targets to setup, run, test, and build a final installable PyPI compatible package. To learn more about packaging in Python see the [Python Packaging User Guide](https://packaging.python.org/). ## Build ```bash $ make ``` -Running `make` in this directory will build the default target "build". The chain of dependencies for the default target includes targets that retrieve a miniconda installation script for Linux and creates a stand-alone miniconda environment with the required dependencies. The name of the miniconda environment is `miniconda3` and can be activated as normal using `source miniconda3/bin/activate` and deactivated using `conda deactivate`. The build target runs `python -m build` which collects the required files and packages in `src` and generates an install wheel under `dist`. - -To start from scratch run clean: -```bash -$ make clean -``` +Running `make` in this directory will build the default target "build". The chain of dependencies for the default target includes targets that build a docker image with development dependencies. This target will also run `pytest` and `build`, and copy the resulting `dist` directory with `.whl` and `.tar.gz` packages into the currect directory. ## Installation After running build, you can install the default package by running ``` -miniconda3/bin/python -m pip install dist/hello-0.1.0-py3-none-any.whl +python -m pip install dist/my_package-0.1.0-py3-none-any.whl +``` + +...or, assuming you have an API token and a valid [`.pypirc` file](https://packaging.python.org/en/latest/specifications/pypirc/) you can upload and install your package from PyPI with + +```bash +python3 -m twine upload dist/* +python3 -m pip install my_package ``` -## Testing +See [Packaging Python Projects](https://packaging.python.org/en/latest/tutorials/packaging-projects/) for more details on uploading your package. -An example test is in `tests/test_hello.py`. This test uses `pytest` to test the `greet` function. You can read more about using `pytest` at the [PyTest Documentation](https://docs.pytest.org/). To run all tests just use: +## Other targets +```bash +$ make setup ``` -miniconda3/bin/python -m pytest +This target builds the docker image and is required by all other targets. + +```bash +$ make run ``` +This target runs the CLI application from a docker container indicated in `setup.cfg` with the arguments specified by `ARGUMENTS` in the `Makefile`. This target is mainly a convenience and reference for users. CLI arguments are more easily changed by running this underlying command directly from the command prompt. +```bash +$ make tests +``` +This target runs `pytest` on the code in the image created by "setup" in a docker container. \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 493e239..28c5df6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,35 +1,37 @@ [metadata] -name = hello -version = 0.1.0 -author = Your Name -author_email = your.name@noaa.gov -description = Generic template for data-driven projects using Python. +name = my_package +version = attr: my_package._version.__version__ +author = Author Name +author_email = author.name@email.com +description = My package description. long_description = file: README.md long_description_content_type = text/markdown; charset=UTF-8 -license = USDOC +license = MIT license_files = LICENSE -url = https://github.com/NOAA-OWP/ +url = https://github.com/my_github/my_package project_urls = - Documentation = https://github.com/NOAA-OWP/ - Source = https://github.com/NOAA-OWP/ - Tracker = https://github.com/NOAA-OWP/ + Documentation = https://my-github.github.io/me/my_package.html + Source = https://github.com/my_github/my_package + Tracker = https://github.com/my_github/my_package/issues classifiers = - Development Status :: 1 - Planning + Development Status :: 3 - Alpha Intended Audience :: Education Intended Audience :: Science/Research - License :: Free To Use But Restricted Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 Topic :: Scientific/Engineering :: Hydrology Operating System :: OS Independent - Private :: Do not Upload + License :: OSI Approved :: MIT License [options] -packages = find: +packages = find_namespace: package_dir = =src install_requires = - numpy + pandas + click python_requires = >=3.8 include_package_data = True @@ -39,3 +41,9 @@ where = src [options.extras_require] develop = pytest + build + +[options.entry_points] +console_scripts = + my-cli = my_package.cli:run + \ No newline at end of file diff --git a/src/hello/__init__.py b/src/hello/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/hello/hello.py b/src/hello/hello.py deleted file mode 100644 index f6ad6cd..0000000 --- a/src/hello/hello.py +++ /dev/null @@ -1,17 +0,0 @@ -def greet(name: str): - """ - Create a simple greeting. - - Parameters - ---------- - name: str, required - Name of person to greet. - - Returns - ------- - greeting: str - String with a simple greeting. - - """ - # Return greeting - return f"Hello, {name}!" diff --git a/src/my_package/__init__.py b/src/my_package/__init__.py new file mode 100644 index 0000000..485a217 --- /dev/null +++ b/src/my_package/__init__.py @@ -0,0 +1,3 @@ +# removing __version__ import will cause build to fail. +# see: https://github.com/pypa/setuptools/issues/1724#issuecomment-627241822 +from ._version import __version__ diff --git a/src/my_package/_version.py b/src/my_package/_version.py new file mode 100644 index 0000000..a68927d --- /dev/null +++ b/src/my_package/_version.py @@ -0,0 +1 @@ +__version__ = "0.1.0" \ No newline at end of file diff --git a/src/my_package/cli.py b/src/my_package/cli.py new file mode 100644 index 0000000..ce6a2c1 --- /dev/null +++ b/src/my_package/cli.py @@ -0,0 +1,28 @@ +import click +import pandas as pd +from my_package.my_module import make_dataframe +from my_package._version import __version__ + +@click.command() +@click.option("-c", "--ncols", "ncols", nargs=1, type=int, help="Number of columns", default=3) +@click.option("-r", "--nrows", "nrows", nargs=1, type=int, help="Number of rows", default=4) +def run( + ncols: int, + nrows: int + ) -> None: + """Generate a random dataframe and print to screen. + + Example: + + my-cli -c 3 -r 6 + """ + # Get a dataframe + df = make_dataframe(ncols=ncols, nrows=nrows) + + # Print + print(f"my_package {__version__}") + with pd.option_context("display.precision", 2): + print(df) + +if __name__ == "__main__": + run() diff --git a/src/my_package/my_module.py b/src/my_package/my_module.py new file mode 100644 index 0000000..d64ec06 --- /dev/null +++ b/src/my_package/my_module.py @@ -0,0 +1,24 @@ +"""Generic example module""" +from string import ascii_uppercase +import numpy as np +import pandas as pd + +def make_dataframe(ncols: int = 4, nrows: int = 5) -> pd.DataFrame: + """ + Generate a pandas.DataFrame of random data with ncols columns and + nrows index length. + + Parameters + ---------- + ncols: int, optional, default 4 + Number of columns for resulting dataframe. + nrows: int, optional, default 5 + Number of rows of resulting dataframe. + + Returns + ------- + pandas.DataFrame with ncols columns and index length nrows. + + """ + # Return dataframe + return pd.DataFrame({c: np.random.random(nrows) for c in ascii_uppercase[:ncols]}) diff --git a/tests/test_hello.py b/tests/test_hello.py deleted file mode 100644 index 2d6151e..0000000 --- a/tests/test_hello.py +++ /dev/null @@ -1,9 +0,0 @@ -import pytest -from hello.hello import greet - -def test_greet(): - # Create a greeting - greeting = greet("Jo") - - # Check the greeting - assert greeting == "Hello, Jo!" diff --git a/tests/test_my_cli.py b/tests/test_my_cli.py new file mode 100644 index 0000000..e7ebd12 --- /dev/null +++ b/tests/test_my_cli.py @@ -0,0 +1,17 @@ +import pytest +from click.testing import CliRunner +from my_package.cli import run +from my_package._version import __version__ + +@pytest.fixture +def runner_result(): + runner = CliRunner() + return runner.invoke(run) + +def test_hello_world(runner_result): + # Check exit code + assert runner_result.exit_code == 0 + + # Check first line of output + first_line = runner_result.output.split("\n")[0] + assert first_line == f"my_package {__version__}" diff --git a/tests/test_my_package.py b/tests/test_my_package.py new file mode 100644 index 0000000..c58f4a2 --- /dev/null +++ b/tests/test_my_package.py @@ -0,0 +1,16 @@ +import pytest +import my_package +from my_package import my_module + +def test_package_version(): + # Check version + assert my_package.__version__ == "0.1.0" + +@pytest.fixture +def default_dataframe(): + return my_module.make_dataframe() + +def test_default_dataframe(default_dataframe): + # Test defaults + assert len(default_dataframe.columns) == 4 + assert len(default_dataframe.index) == 5