diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8387623 --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +{{ cookiecutter.project_slug }}-env/ + +docs/build/ \ No newline at end of file diff --git a/README.rst b/README.rst index 9e86682..b936965 100644 --- a/README.rst +++ b/README.rst @@ -2,16 +2,16 @@ skeleton-python-binary =============================== -.. image:: https://img.shields.io/github/v/tag/pole-surfaces-planetaires/skeleton-python-binary -.. image:: https://img.shields.io/github/v/release/pole-surfaces-planetaires/skeleton-python-binary?include_prereleases +.. image:: https://img.shields.io/github/v/tag/pdssp/skeleton-python-binary +.. image:: https://img.shields.io/github/v/release/pdssp/skeleton-python-binary?include_prereleases -.. image:: https://img.shields.io/github/downloads/pole-surfaces-planetaires/skeleton-python-binary/total -.. image:: https://img.shields.io/github/issues-raw/pole-surfaces-planetaires/skeleton-python-binary -.. image:: https://img.shields.io/github/issues-pr-raw/pole-surfaces-planetaires/skeleton-python-binary +.. image:: https://img.shields.io/github/downloads/pdssp/skeleton-python-binary/total +.. image:: https://img.shields.io/github/issues-raw/pdssp/skeleton-python-binary +.. image:: https://img.shields.io/github/issues-pr-raw/pdssp/skeleton-python-binary .. image:: https://img.shields.io/badge/Maintained%3F-yes-green.svg - :target: https://github.com/pole-surfaces-planetaires/skeleton-python-binary/graphs/commit-activity -.. image:: https://img.shields.io/github/license/pole-surfaces-planetaires/skeleton-python-binary -.. image:: https://img.shields.io/github/forks/pole-surfaces-planetaires/skeleton-python-binary?style=social + :target: https://github.com/pdssp/skeleton-python-binary/graphs/commit-activity +.. image:: https://img.shields.io/github/license/pdssp/skeleton-python-binary +.. image:: https://img.shields.io/github/forks/pdssp/skeleton-python-binary?style=social Create a functional skeleton of a python package @@ -19,7 +19,7 @@ Create a functional skeleton of a python package ✨ Demo preview --------------- -.. image:: https://raw.githubusercontent.com/pole-surfaces-planetaires/skeleton-python-binary/main/.github/images/demo.gif +.. image:: https://raw.githubusercontent.com/pdssp/skeleton-python-binary/main/.github/images/demo.gif Usage ----- @@ -35,7 +35,7 @@ To create a skeleton of a python project: pip install cookiecutter - cookiecutter https://github.com/pole-surfaces-planetaires/skeleton-python-binary + cookiecutter https://github.com/pdssp/skeleton-python-binary diff --git a/cookiecutter.json b/cookiecutter.json index 01b8659..da1c7ae 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -1,29 +1,33 @@ { - "full_name": "Jean-Christophe Malapert", - "institute": "CNES", - "email": "jean-christophe.malapert@cnes.fr", + "full_name": "John Doo", + "institute": "My institute", + "email": "john.doo@foo.com", "github_username": "", "project_name": "My Great Project", "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_') }}", "project_class_lib": "{{ cookiecutter.project_slug.title().replace('_', '') + 'Lib' }}", - "project_short_description": "My Great Project description.", + "project_url": "https://github.com/pdssp/{{ cookiecutter.project_slug }}", + "project_web_site":"{{ cookiecutter.project_url.replace('github.com', 'pdssp.github.io') }}", + "project_description": "My Great Project description.", + "docker_image_name": "{{ cookiecutter.project_slug + '_image' }}", "year": "{% now 'utc', '%Y' %}", "month": "{% now 'utc', '%B' %}", - "_extensions": [ - "jinja2_time.TimeExtension" - ], "consortium_name": [ - "Pôle Surfaces Planétaires" + "PDSSP (Pôle de Données et Services Surfaces Planétaires)" ], - "pypi_username": "{{ cookiecutter.github_username }}", "version": "0.0.1", - "use_pytest": "n", - "use_pypi_deployment_with_travis": "y", - "add_pyup_badge": "n", - "create_author_file": "y", "open_source_license": [ - "GNU General Public License v3", - "GNU Lesser General Public License v3", - "GNU Affero General Public License v3" - ] -} \ No newline at end of file + "Apache V2.0", + "MIT", + "GNU Lesser General Public License v3" + ], + "python_version": [ + "3.12", + "3.11" + ], + "pypi_username": "{{ cookiecutter.github_username }}", + "add_pyup_badge": "n", + "use_pypi_deployment_with_travis": "y", + "use_pyenv":false +} + diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py new file mode 100644 index 0000000..2bdf82d --- /dev/null +++ b/hooks/pre_gen_project.py @@ -0,0 +1,54 @@ +import platform +import sys +import os + +def is_compatible_with_plateform(minimal_python_version_wanted): + major_version_wanted, minor_version_wanted = minimal_python_version_wanted.split(".") + python_version = platform.python_version_tuple() + major_version, minor_version, _ = python_version + return major_version_wanted == major_version and int(minor_version_wanted) <= int(minor_version) + +def is_compatible_with_gitlab_docker(minimal_python_version_wanted): + major_version_wanted, minor_version_wanted = minimal_python_version_wanted.split(".") + python_version = "3.13." + major_version, minor_version, _ = python_version.split(".") + return major_version_wanted == major_version and int(minor_version_wanted) <= int(minor_version) + +def is_pyenv_installed(): + path_directories = os.environ.get('PATH', '').split(os.pathsep) + + # Look at the install directory of pyenv in PATH + for directory in path_directories: + if 'pyenv' in directory: + return True + + return False + +def is_pyenv_selected(pyenv): + return pyenv.lower() in ("yes", "true", "t", "1") + + +minimal_python_version_wanted = "{{ cookiecutter.python_version }}" +pyenv_wanted = "{{ cookiecutter.use_pyenv }}" + + +if is_compatible_with_plateform(minimal_python_version_wanted) and is_compatible_with_gitlab_docker(minimal_python_version_wanted): + if is_pyenv_selected(pyenv_wanted): + if is_pyenv_installed(): + sys.exit(0) + else: + print ("Please set use_pyenv=False or install it") + sys.exit(1) + else: + sys.exit(0) +elif is_pyenv_selected(pyenv_wanted): + if is_pyenv_installed(): + sys.exit(0) + else: + print ("Please install pyenv") + sys.exit(1) +else: + print("Please set use_pyenv=True with this python version") + sys.exit(1) + + diff --git a/tests/test_generation.py b/tests/test_generation.py new file mode 100644 index 0000000..44163c5 --- /dev/null +++ b/tests/test_generation.py @@ -0,0 +1,167 @@ +import os +import pytest +import tempfile +from cookiecutter.main import cookiecutter + +@pytest.fixture +def generate_project(): + """Fixture for generating a Cookiecutter project in a tmp directory""" + with tempfile.TemporaryDirectory() as tempdir: + cookiecutter( + '.', + no_input=True, + output_dir=tempdir, + extra_context={"project_name": "TestProject", "full_name": "John Doe", "email": "John.Doe@foo.com", "open_source_license": "MIT", "python_version": "3.12"}, + ) + yield tempdir + + +def test_project_structure(generate_project): + """Check that the files and directories have been created.""" + project_dir = os.path.join(generate_project, "testproject") + + # Check the following files exist + expected_files = [ + "tox.ini", + "README.rst", + "pyproject.toml", + "Makefile", + "LICENSE", + "Dockerfile", + "CONTRIBUTING.rst", + "CHANGELOG", + "AUTHORS.rst", + ".pre-commit-config.yaml", + ".gitignore", + ".flake8", + ".dockerignore", + "tests/acceptance/docker_test.py", + "tests/unit/monitoring_test.py", + "scripts/generate-changelog.bash", + "scripts/init_remote_repo.bash", + "scripts/install-hooks.bash", + "scripts/license.py", + "scripts/post-commit.bash", + "docs/make.bat", + "docs/Makefile", + "docker/build.sh", + "docker/Dockerfile", + "testproject/__init__.py", + "testproject/__main__.py", + "testproject/_version.py", + "testproject/testproject.py", + "testproject/monitoring.py" + ] + + for file_name in expected_files: + assert os.path.exists(os.path.join(project_dir, file_name)), f"{file_name} was not created" + + +def test_readme_content(generate_project): + """Check the contain of the README is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "README.rst") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "TestProject" in content, "The project name is not in the README" + assert "John Doe" in content, "The author name is not in the README" + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + +def test_pyproject_toml(generate_project): + """Check the contain of the pyproject.toml is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "pyproject.toml") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + +def test_makefile(generate_project): + """Check the Makefile is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "Makefile") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + +def test_license(generate_project): + """Check the License is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "LICENSE") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + +def test_Dockerfile(generate_project): + """Check the Dockerfile is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "Dockerfile") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + +def test_contributing(generate_project): + """Check the CONTRIBUTING.rst is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "CONTRIBUTING.rst") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + + +def test_acceptance_tests(generate_project): + """Check the docker_test is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "tests", "acceptance", "docker_test.py") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + +def test_unit_tests(generate_project): + """Check the monitoring_test is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "tests", "unit", "monitoring_test.py") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + +def test_docker(generate_project): + """Check the docker files are correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "docker", "build.sh") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + + readme_path = os.path.join(generate_project, "testproject", "docker", "Dockerfile") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + +def test_main(generate_project): + """Check the __main__ is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "testproject", "__main__.py") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" + +def test_init(generate_project): + """Check the __init__ is correctly generated.""" + readme_path = os.path.join(generate_project, "testproject", "testproject", "__init__.py") + + with open(readme_path, 'r') as readme_file: + content = readme_file.read() + + assert "cookiecutter." not in content, "Some cookiecutter variables are not replaced" \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/.dockerignore b/{{cookiecutter.project_slug}}/.dockerignore new file mode 100644 index 0000000..444eaa4 --- /dev/null +++ b/{{cookiecutter.project_slug}}/.dockerignore @@ -0,0 +1,9 @@ +# Ignore everything +* + +# Allow files and directories +!/{{ cookiecutter.project_slug }}/ +!/Makefile +!/pyproject.toml +!/README.rst + diff --git a/{{cookiecutter.project_slug}}/.travis.yml b/{{cookiecutter.project_slug}}/.travis.yml index 02700a8..5e38763 100644 --- a/{{cookiecutter.project_slug}}/.travis.yml +++ b/{{cookiecutter.project_slug}}/.travis.yml @@ -23,6 +23,6 @@ deploy: secure: PLEASE_REPLACE_ME on: tags: true - repo: pole-surfaces-planetaires/{{ cookiecutter.project_slug }} + repo: pdssp/{{ cookiecutter.project_slug }} python: 3.8 {%- endif %} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/CONTRIBUTING.rst b/{{cookiecutter.project_slug}}/CONTRIBUTING.rst index 00fcbf9..59e86ad 100644 --- a/{{cookiecutter.project_slug}}/CONTRIBUTING.rst +++ b/{{cookiecutter.project_slug}}/CONTRIBUTING.rst @@ -15,7 +15,7 @@ Types of Contributions Report Bugs ~~~~~~~~~~~ -Report bugs at https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}/issues. +Report bugs at https://github.com/pdssp/{{ cookiecutter.project_slug }}/issues. If you are reporting a bug, please include: @@ -45,7 +45,7 @@ articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ -The best way to send feedback is to file an issue at https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}/issues. +The best way to send feedback is to file an issue at https://github.com/pdssp/{{ cookiecutter.project_slug }}/issues. If you are proposing a feature: @@ -108,11 +108,7 @@ Tips To run a subset of tests:: -{% if cookiecutter.use_pytest == 'y' -%} - $ pytest tests.test_{{ cookiecutter.project_slug }} -{% else %} - $ python -m unittest tests.test_{{ cookiecutter.project_slug }} -{%- endif %} +$ pytest tests.test_{{ cookiecutter.project_slug }} Deploying --------- diff --git a/{{cookiecutter.project_slug}}/Dockerfile b/{{cookiecutter.project_slug}}/Dockerfile new file mode 120000 index 0000000..93c313e --- /dev/null +++ b/{{cookiecutter.project_slug}}/Dockerfile @@ -0,0 +1 @@ +docker/Dockerfile \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/Makefile b/{{cookiecutter.project_slug}}/Makefile index e423d07..091e8e7 100644 --- a/{{cookiecutter.project_slug}}/Makefile +++ b/{{cookiecutter.project_slug}}/Makefile @@ -1,6 +1,7 @@ .DEFAULT_GOAL := init -.PHONY: prepare-dev install-dev data help lint tests coverage upload-prod-pypi upload-test-pypi update_req update_req_dev pyclean doc doc-pdf visu-doc-pdf visu-doc tox licences -VENV = ".{{cookiecutter.project_slug}}" +.PHONY: upload-test-pypi upload-prod-pypi docker add_req_prod add_req_dev add_major_version add_minor_version add_patch_version add_premajor_version add_preminor_version add_prepatch_version add_prerelease_version prepare-dev install-dev data help lint tests coverage upload-prod-pypi upload-test-pypi update_req update_req_dev pyclean doc doc-pdf visu-doc-pdf visu-doc tox licences check_update update_latest_dev update_latest_main show_deps_main show_deps_dev show_obsolete +VENV = ".venv" +VENV_RUN = ".{{cookiecutter.project_slug}}" define PROJECT_HELP_MSG @@ -19,7 +20,6 @@ Usage:\n -------------------------------------------------------------------------\n make prepare-dev\t\t Prepare Development environment\n make install-dev\t\t Install COTS and {{ cookiecutter.project_slug }} for development purpose\n - make data\t\t\t Download data\n make tests\t\t\t Run units and integration tests\n \n make doc\t\t\t Generate the documentation\n @@ -29,18 +29,42 @@ Usage:\n \n make release\t\t\t Release the package as tar.gz\n make upload-test-pypi\t\t Upload the pypi package on the test platform\n - make upload-prod-pypi\t\t Upload the pypi package on the prod platform\n - \n - make update_req\t\t Update the version of the packages in requirements.txt\n - make update_req_dev\t\t Update the version of the packages in requirements_dev.txt\n + make upload-prod-pypi\t\t Upload the pypi package on the prod platform\n \n make pyclean\t\t\t Clean .pyc files and __pycache__ directories\n \n + make add_req_prod pkg=\t Add the package in the dependencies of .toml\n + make add_req_dev pkg=\t Add the package in the DEV dependencies of .toml\n + \n + make docker\t\t\t Create the docker image\n + \n + -------------------------------------------------------------------------\n + \t\tVersion\n + -------------------------------------------------------------------------\n + make version\t\t\t Display the version\n + make add_major_version\t\t Add a major version\n + make add_minor_version\t\t Add a major version\n + make add_patch_version\t\t Add a major version\n + make add_premajor_version\t Add a pre-major version\n + make add_preminor_version\t Add a pre-minor version\n + make add_prepatch_version\t Add a pre-patch version\n + make add_prerelease_version\t Add a pre-release version\n + \n + -------------------------------------------------------------------------\n + \t\tMaintenance (use make install-dev before using these tasks)\n + -------------------------------------------------------------------------\n + make check_update\t\t Check the COTS update\n + make update_req\t\t Update the version of the packages in the authorized range in toml file\n + make update_latest_dev\t\t Update to the latest version for development\n + make update_latest_main\t Update to the latest version for production\n + make show_deps_main\t\t Show main COTS for production\n + make show_deps_dev\t\t Show main COTS for development\n + make show_obsolete\t\t Show obsolete COTS\n + \n -------------------------------------------------------------------------\n \t\tOthers\n -------------------------------------------------------------------------\n make licences\t\t\t Display the list of licences\n - make version\t\t\t Display the version\n make coverage\t\t\t Coverage\n make lint\t\t\t Lint\n make tox\t\t\t Run all tests\n @@ -60,7 +84,7 @@ help: # ----------------------------------------------------- # init: - python3 setup.py install + poetry install --only=main # # Sotware Installation for user @@ -70,36 +94,72 @@ init: # don’t want to install into it. # user: - python3 setup.py install --user + poetry install --only=main +{%- if cookiecutter.use_pyenv -%} +#Comment line is required to properly write pyenv-dev prepare-dev: - cp .pypirc ~${USER} && git init && echo "python3 -m venv {{cookiecutter.project_slug}}-env && export PYTHONPATH=. && export PATH=`pwd`/{{cookiecutter.project_slug}}-env/bin:"${PATH}"" > ${VENV} && echo "source \"`pwd`/{{cookiecutter.project_slug}}-env/bin/activate\"" >> ${VENV} && scripts/install-hooks.bash && echo "\nnow source this file: \033[31msource ${VENV}\033[0m" + @if command -v pyenv >/dev/null; then \ + echo "pyenv is installed, proceeding with setup..."; \ + else \ + echo "pyenv is not installed. Please install pyenv first."; \ + exit 1; \ + fi + git config --global init.defaultBranch main + git init + echo "echo \"Using Virtual env for {{cookiecutter.project_slug}}.\"" > ${VENV_RUN} + echo "echo \"Please, type 'deactivate' to exit this virtual env.\"" >> ${VENV_RUN} + PYTHON_VERSION={{cookiecutter.python_version}} && \ + pyenv install -s $$PYTHON_VERSION && \ + FULL_PYTHON_VERSION=$$(pyenv versions --bare --skip-aliases | grep $$PYTHON_VERSION | sort -V | tail -1) && \ + PYENV_ROOT=$$(pyenv root) && \ + PYENV_PYTHON="$$PYENV_ROOT/versions/$$FULL_PYTHON_VERSION/bin/python" && \ + echo "$$PYENV_PYTHON -m venv --prompt {{cookiecutter.project_slug}} ${VENV}" >> ${VENV_RUN} && \ + echo "export PYTHONPATH=." >> ${VENV_RUN} && \ + echo "export PATH=`pwd`/${VENV}/bin:$$PATH" >> ${VENV_RUN} && \ + echo "source '`pwd`/${VENV}/bin/activate'" >> ${VENV_RUN} && \ + scripts/install-hooks.bash && \ + echo "\nnow source this file: \033[31msource ${VENV_RUN}\033[0m" +{%- else -%} +#Comment line is required to properly write pyenv-dev +prepare-dev: + git config --global init.defaultBranch main + git init + echo "echo \"Using Virtual env for {{cookiecutter.project_slug}}.\"" > ${VENV_RUN} + echo "echo \"Please, type 'deactivate' to exit this virtual env.\"" >> ${VENV_RUN} + echo "python3 -m venv --prompt {{cookiecutter.project_slug}} ${VENV} && \ + export PYTHONPATH=. && \ + export PATH=`pwd`/${VENV}/bin:${PATH}" >> ${VENV_RUN} && \ + echo "source \"`pwd`/${VENV}/bin/activate\"" >> ${VENV_RUN} && \ + scripts/install-hooks.bash && \ + echo "\nnow source this file: \033[31msource ${VENV_RUN}\033[0m" +{% endif %} install-dev: - pip install --upgrade pip && pip install -r requirements.txt && pip install -r requirements_dev.txt && pre-commit install && pre-commit autoupdate && python3 setup.py develop + poetry install && poetry run pre-commit install && poetry run pre-commit autoupdate coverage: ## Run tests with coverage - coverage erase - coverage run --include={{cookiecutter.project_slug}}/* -m pytest -ra - coverage report -m + poetry run coverage erase + poetry run coverage run --include={{cookiecutter.project_slug}}/* -m pytest --ignore=./tests/msap_data -ra + poetry run coverage report -m lint: ## Lint and static-check - flake8 --ignore=E203,E266,E501,W503,F403,F401 --max-line-length=79 --max-complexity=18 --select=B,C,E,F,W,T4,B9 {{cookiecutter.project_slug}} - pylint {{cookiecutter.project_slug}} - mypy {{cookiecutter.project_slug}} + poetry run flake8 --ignore=E203,E266,E501,W503,F403,F401 --max-line-length=79 --max-complexity=18 --select=B,C,E,F,W,T4,B9 {{cookiecutter.project_slug}} + poetry run pylint {{cookiecutter.project_slug}} + poetry run mypy --install-types --non-interactive {{cookiecutter.project_slug}} tests: ## Run tests - pytest -ra + poetry run pytest --ignore=./tests/msap_data tox: - tox -e py37 + poetry run tox -e py310 doc: - make licences > third_party.txt + make licences rm -rf docs/source/_static/coverage - pytest -ra --html=docs/source/_static/report.html - make coverage - coverage html -d docs/source/_static/coverage + poetry run pytest --ignore=./tests/msap_data -ra --html=docs/source/_static/report.html + poetry run make coverage + poetry run coverage html -d docs/source/_static/coverage make html -C docs doc-pdf: @@ -112,28 +172,77 @@ visu-doc: firefox docs/build/html/index.html release: - python3 setup.py sdist + poetry build version: - python3 setup.py --version + poetry version -s + +add_major_version: + poetry version major + poetry run git tag `poetry version -s` -data: - pip install -r requirements_data.txt && python scripts/data_download.py +add_minor_version: + poetry version minor + poetry run git tag `poetry version -s` + +add_patch_version: + poetry version patch + poetry run git tag `poetry version -s` + +add_premajor_version: + poetry version premajor + +add_preminor_version: + poetry version preminor + +add_prepatch_version: + poetry version prepatch + +add_prerelease_version: + poetry version prerelease + +add_req_prod: + poetry add "$(pkg)" + +add_req_dev: + poetry add -G dev "$(pkg)" upload-test-pypi: flit publish --repository pypitest upload-prod-pypi: - flit publish + flit publish licences: - pip-licenses + poetry run python3 scripts/license.py +check_update: + poetry show -l + update_req: - pur -r requirements.txt + poetry update + +update_latest_dev: + packages=$$(poetry show -T --only=dev | grep -oP "^\S+"); \ + packages_latest=$$(echo $$packages | tr '\n' ' ' | sed 's/ /@latest /g'); \ + poetry add -G dev $$packages_latest + +update_latest_main: + packages=$$(poetry show -T --only=main | grep -oP "^\S+"); \ + packages_latest=$$(echo $$packages | tr '\n' ' ' | sed 's/ /@latest /g'); \ + poetry add -G main $$packages_latest + +show_deps_main: + poetry show -T --only=main -update_req_dev: - pur -r requirements_dev.txt +show_deps_dev: + poetry show -T --only=dev + +show_obsolete: + poetry show -o pyclean: find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete + +docker: + sh docker/build.sh diff --git a/{{cookiecutter.project_slug}}/README.rst b/{{cookiecutter.project_slug}}/README.rst index 558b1fe..5108ff2 100644 --- a/{{cookiecutter.project_slug}}/README.rst +++ b/{{cookiecutter.project_slug}}/README.rst @@ -4,21 +4,21 @@ {{ cookiecutter.project_name }} =============================== -.. image:: https://img.shields.io/github/v/tag/pole-surfaces-planetaires/{{ cookiecutter.project_slug }} -.. image:: https://img.shields.io/github/v/release/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}?include_prereleases +.. image:: https://img.shields.io/github/v/tag/pdssp/{{ cookiecutter.project_slug }} +.. image:: https://img.shields.io/github/v/release/pdssp/{{ cookiecutter.project_slug }}?include_prereleases {% if cookiecutter.add_pyup_badge == 'y' %} .. image:: https://img.shields.io/pypi/v/{{ cookiecutter.project_slug }} {% endif %} -.. image https://img.shields.io/github/downloads/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}/total -.. image https://img.shields.io/github/issues-raw/pole-surfaces-planetaires/{{ cookiecutter.project_slug }} -.. image https://img.shields.io/github/issues-pr-raw/pole-surfaces-planetaires/{{ cookiecutter.project_slug }} +.. image https://img.shields.io/github/downloads/pdssp/{{ cookiecutter.project_slug }}/total +.. image https://img.shields.io/github/issues-raw/pdssp/{{ cookiecutter.project_slug }} +.. image https://img.shields.io/github/issues-pr-raw/pdssp/{{ cookiecutter.project_slug }} .. image:: https://img.shields.io/badge/Maintained%3F-yes-green.svg - :target: https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}/graphs/commit-activity -.. image https://img.shields.io/github/license/pole-surfaces-planetaires/{{ cookiecutter.project_slug }} -.. image https://img.shields.io/github/forks/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}?style=social + :target: https://github.com/pdssp/{{ cookiecutter.project_slug }}/graphs/commit-activity +.. image https://img.shields.io/github/license/pdssp/{{ cookiecutter.project_slug }} +.. image https://img.shields.io/github/forks/pdssp/{{ cookiecutter.project_slug }}?style=social -{{ cookiecutter.project_short_description }} +{{ cookiecutter.project_description }} Stable release @@ -48,13 +48,13 @@ You can either clone the public repository: .. code-block:: console - $ git clone git://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }} + $ git clone git://github.com/pdssp/{{ cookiecutter.project_slug }} Or download the `tarball`_: .. code-block:: console - $ curl -OJL https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}/tarball/master + $ curl -OJL https://github.com/pdssp/{{ cookiecutter.project_slug }}/tarball/master Once you have a copy of the source, you can install it with: @@ -64,8 +64,8 @@ Once you have a copy of the source, you can install it with: $ make user # or Install for non-root usage -.. _Github repo: https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }} -.. _tarball: https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}/tarball/master +.. _Github repo: https://github.com/pdssp/{{ cookiecutter.project_slug }} +.. _tarball: https://github.com/pdssp/{{ cookiecutter.project_slug }}/tarball/master @@ -74,7 +74,7 @@ Development .. code-block:: console - $ git clone https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }} + $ git clone https://github.com/pdssp/{{ cookiecutter.project_slug }} $ cd {{ cookiecutter.project_slug }} $ make prepare-dev $ source .{{ cookiecutter.project_slug }} @@ -113,9 +113,9 @@ Author 🤝 Contributing --------------- -Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}/issues). You can also take a look at the [contributing guide](https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}/blob/master/CONTRIBUTING.rst) +Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/pdssp/{{ cookiecutter.project_slug }}/issues). You can also take a look at the [contributing guide](https://github.com/pdssp/{{ cookiecutter.project_slug }}/blob/master/CONTRIBUTING.rst) 📝 License ---------- -This project is [{{ cookiecutter.open_source_license }}](https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}/blob/master/LICENSE) licensed. +This project is [{{ cookiecutter.open_source_license }}](https://github.com/pdssp/{{ cookiecutter.project_slug }}/blob/master/LICENSE) licensed. diff --git a/{{cookiecutter.project_slug}}/docker/Dockerfile b/{{cookiecutter.project_slug}}/docker/Dockerfile index c7333f7..428f332 100644 --- a/{{cookiecutter.project_slug}}/docker/Dockerfile +++ b/{{cookiecutter.project_slug}}/docker/Dockerfile @@ -1,51 +1,65 @@ -# {{cookiecutter.project_name}} - {{cookiecutter.project_short_description}} +# -*- coding: utf-8 -*- +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} # Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) -# -# This file is part of {{cookiecutter.project_name}}. -# -# {{cookiecutter.project_name}} is free software: you can redistribute it and/or modify -# it under the terms of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# {{cookiecutter.project_name}} is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} for more details. -# -# You should have received a copy of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} -# along with {{cookiecutter.project_name}}. If not, see . -FROM ubuntu:latest - -LABEL maintainer="{{ cookiecutter.full_name }} <{{ cookiecutter.email }}>" +# This file is part of {{cookiecutter.project_name}} <{{ cookiecutter.project_url }}> +# SPDX-License-Identifier: {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GPL-3.0-or-later{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}LGPL-3.0-or-later{% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}AGPL-3.0{% endif %} +ARG DEBIAN_NAME="bullseye" +ARG PYTHON_VERSION="{{cookiecutter.python_version}}" +FROM python:${PYTHON_VERSION}-slim-${DEBIAN_NAME} AS base + +WORKDIR /app +RUN apt-get update -qq \ + && apt upgrade --yes \ + && apt-get clean all \ + && rm -rf /var/lib/apt/lists/* + +FROM base AS builder +ARG CI_JOB_TOKEN +ENV project_url={{ cookiecutter.project_url }}.git + +WORKDIR /app + +RUN apt-get update -qq \ + && apt upgrade --yes \ + && apt-get -y --no-install-recommends install \ + build-essential \ + ca-certificates \ + gdb \ + gfortran \ + wget \ + file \ + apt-utils \ + git \ + && apt-get clean all \ + && rm -r /var/lib/apt/lists/* + +RUN pip install poetry && poetry config installer.max-workers 10 && poetry config virtualenvs.create false +COPY . . +RUN make + +FROM python:{{cookiecutter.python_version}}-slim-bullseye AS release ARG BUILD_DATE ARG VERSION ARG DEBIAN_FRONTEND=noninteractive -# Labels (based on http://label-schema.org/rc1/) +WORKDIR /app + +# Labels (based on OCI Image Format Specification) LABEL \ - org.label-schema.schema-version="1.0" \ - org.label-schema.build-date=$BUILD_DATE \ - org.label-schema.name="cdm" \ - org.label-schema.description="{{ cookiecutter.project_short_description }}" \ - org.label-schema.url="https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}" \ - org.label-schema.vcs-url="https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}" \ - org.label-schema.vcs-ref=$VCS_REF \ - org.label-schema.vendor="{{ cookiecutter.consortium_name }}" \ - org.label-schema.version=$VERSION - -RUN apt-get update && \ - apt-get install -y \ - software-properties-common apt-transport-https wget git\ - make python3-pip && \ - update-alternatives --install /usr/bin/pip pip /usr/bin/pip3 1 && \ - pip install setuptools && \ - git clone "https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}.git" && \ - cd {{ cookiecutter.project_slug }} && \ - make - -# Custom prompt -RUN echo 'export PS1="\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[01;31m\]\u@platoscope-dev\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$"' >> /etc/bash.bashrc - -CMD ["{{cookiecutter.project_slug}}"] \ No newline at end of file + org.opencontainers.image.version=$VERSION \ + org.opencontainers.image.created=$BUILD_DATE \ + org.opencontainers.image.title="{{ cookiecutter.project_name }}" \ + org.opencontainers.image.ref.name="{{ cookiecutter.project_slug }}" \ + org.opencontainers.image.description="{{ cookiecutter.project_description }}" \ + org.opencontainers.image.licenses="{{ cookiecutter.open_source_license }}" \ + org.opencontainers.image.source="{{ cookiecutter.project_url }}" \ + org.opencontainers.image.documentation="{{ cookiecutter.project_web_site }}" \ + org.opencontainers.image.vendor="{{ cookiecutter.consortium_name }}" \ + org.opencontainers.image.authors="{{ cookiecutter.full_name }} <{{ cookiecutter.email }}>" + +# Get the installed libraries and application from the ealier stage +COPY --from=builder /usr/local/ /usr/local/ + +# Regenerate the shared-library cache +RUN ldconfig diff --git a/{{cookiecutter.project_slug}}/docker/build.sh b/{{cookiecutter.project_slug}}/docker/build.sh index 2131a36..53bd63d 100755 --- a/{{cookiecutter.project_slug}}/docker/build.sh +++ b/{{cookiecutter.project_slug}}/docker/build.sh @@ -1,5 +1,5 @@ #!/bin/sh -# {{cookiecutter.project_name}} - {{cookiecutter.project_short_description}} +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} # Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) # # This file is part of {{cookiecutter.project_name}}. diff --git a/{{cookiecutter.project_slug}}/docker/commit-dev.sh b/{{cookiecutter.project_slug}}/docker/commit-dev.sh index 36ccdc8..19ac234 100755 --- a/{{cookiecutter.project_slug}}/docker/commit-dev.sh +++ b/{{cookiecutter.project_slug}}/docker/commit-dev.sh @@ -1,5 +1,5 @@ #!/bin/sh -# {{cookiecutter.project_name}} - {{cookiecutter.project_short_description}} +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} # Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) # # This file is part of {{cookiecutter.project_name}}. diff --git a/{{cookiecutter.project_slug}}/docker/remove-dev.sh b/{{cookiecutter.project_slug}}/docker/remove-dev.sh index 25d0df4..7d09a62 100755 --- a/{{cookiecutter.project_slug}}/docker/remove-dev.sh +++ b/{{cookiecutter.project_slug}}/docker/remove-dev.sh @@ -1,5 +1,5 @@ #!/bin/sh -# {{cookiecutter.project_name}} - {{cookiecutter.project_short_description}} +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} # Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) # # This file is part of {{cookiecutter.project_name}}. diff --git a/{{cookiecutter.project_slug}}/docker/run-dev.sh b/{{cookiecutter.project_slug}}/docker/run-dev.sh index 26a8d6b..ec05a43 100755 --- a/{{cookiecutter.project_slug}}/docker/run-dev.sh +++ b/{{cookiecutter.project_slug}}/docker/run-dev.sh @@ -1,5 +1,5 @@ #!/bin/sh -# {{cookiecutter.project_name}} - {{cookiecutter.project_short_description}} +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} # Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) # # This file is part of {{cookiecutter.project_name}}. diff --git a/{{cookiecutter.project_slug}}/docs/source/conf.py b/{{cookiecutter.project_slug}}/docs/source/conf.py index 2d671fe..4beec01 100644 --- a/{{cookiecutter.project_slug}}/docs/source/conf.py +++ b/{{cookiecutter.project_slug}}/docs/source/conf.py @@ -37,7 +37,7 @@ def setup(app): version = {{ cookiecutter.project_slug }}.__version__ release = {{ cookiecutter.project_slug }}.__version__ title = {{ cookiecutter.project_slug }}.__title__ -project_url = "https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}" +project_url = "https://github.com/pdssp/{{ cookiecutter.project_slug }}" # -- General configuration --------------------------------------------------- @@ -214,7 +214,7 @@ def setup(app): \centering \vspace*{40mm} %%% * is used to give space from top - \textbf{\Huge {"""+title+ r"""}} + \textbf{\Huge {"""+title.replace("_", r"\_")+ r"""}} \vspace{0mm} \begin{figure}[!h] @@ -223,7 +223,7 @@ def setup(app): \end{figure} \vspace{0mm} - \Large \textbf{"""+author+ r"""} + \Large \textbf{"""+author.replace("_", r"\_")+ r"""} \small Created on : {{ cookiecutter.month }}, {{ cookiecutter.year }} @@ -233,9 +233,9 @@ def setup(app): %% \vfill adds at the bottom \vfill - \small \textit{More information about }{\href{""" + project_url + r"""}{""" + title + r"""}} + \small \textit{More information about }{\href{""" + project_url.replace("_", r"\_") + r"""}{""" + title.replace("_", r"\_") + r"""}} - \small \textit{""" + copyright + r"""} + \small \textit{""" + copyright.replace("_", r"\_") + r"""} \end{titlepage} \clearpage @@ -265,7 +265,7 @@ def setup(app): ( master_doc, "{{ cookiecutter.project_slug }}.tex", - title + " Documentation", + title.replace("_", r"\_") + " Documentation", author, "manual", ), @@ -291,7 +291,7 @@ def setup(app): title + ' Documentation', author, '{{ cookiecutter.project_slug }}', - '{{ cookiecutter.project_short_description }}', + '{{ cookiecutter.project_description }}', 'Miscellaneous'), ] @@ -374,4 +374,4 @@ def setup(app): # epub_uid = '' # A list of files that should not be packed into the epub file. -epub_exclude_files = ["search.html"] +epub_exclude_files = ["search.html"] \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/docs/source/reference_manual.rst b/{{cookiecutter.project_slug}}/docs/source/reference_manual.rst index 8b10abf..0d1ceec 100644 --- a/{{cookiecutter.project_slug}}/docs/source/reference_manual.rst +++ b/{{cookiecutter.project_slug}}/docs/source/reference_manual.rst @@ -17,20 +17,8 @@ Help method in terms of the actions to be performed by the user, how to invoke the \ function, possible errors, how to resolve them and what results to expect.* -.. automodule:: {{ cookiecutter.project_slug }}.custom_logging -.. autoclass:: UtilsLogs - :members: - :private-members: - -.. autoclass:: LogRecord - :members: - :private-members: - -.. autoclass:: CustomColorFormatter - :members: - :private-members: - -.. autoclass:: ShellColorFormatter +.. automodule:: {{ cookiecutter.project_slug }}.monitoring +.. autoclass:: UtilsMonitoring :members: :private-members: diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index c57772b..ab6b3d9 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -1,6 +1,5 @@ -[build-system] -requires = ["setuptools", "wheel", "setuptools_scm"] -build-backend = 'setuptools.build_meta' +[tool.flit.scripts] +{{cookiecutter.project_slug}} = "{{cookiecutter.project_slug}}.__main__:run" [tool.black] line-length = 79 @@ -19,10 +18,49 @@ exclude = ''' )/ ''' -[tool.flit.metadata] -module = "{{cookiecutter.project_slug}}" -author = "{{cookiecutter.full_name}}" -author-email = "{{cookiecutter.email}}" -home-page = "https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}" -classifiers = [ "License :: OSI Approved :: {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License v3 (GPLv3){% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 (LGPLv3){% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3 (AGPLv3){% endif %}",] -description-file = "README.rst" \ No newline at end of file +[tool.poetry] +name = "{{cookiecutter.project_slug}}" +readme = "README.rst" +description = "{{cookiecutter.project_description}}" +authors = ["{{cookiecutter.full_name}} <{{cookiecutter.email}}>", ] +license = {% if cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}"LGPL-3.0-or-later"{% elif cookiecutter.open_source_license == 'Apache V2.0' -%}"Apache-2.0"{% elif cookiecutter.open_source_license == 'MIT' -%}"MIT"{% endif %} +packages = [{include = "{{ cookiecutter.project_slug }}"}] +homepage = "{{cookiecutter.project_url}}" +version = "{{cookiecutter.version}}" +include = ["AUTHORS.rst","CHANGELOG","CONTRIBUTING.rst","Dockerfile","LICENSE","Makefile","pyproject.toml","README.rst","tox.ini","docker/*","docs/*","scripts/*","**/conf/*", ".dockerignore"] + +[tool.poetry.dependencies] +python = "^{{cookiecutter.python_version}}" +toml = "^0.10.2" +loguru = "^0.7.2" + +[tool.poetry.scripts] +{{cookiecutter.project_slug}} = "{{cookiecutter.project_slug}}.__main__:run" + +[tool.poetry.group.dev.dependencies] +pre-commit = "^3.8.0" +pre-commit-hooks = "^4.6.0" +flit = "^3.10.1" +flit-core = "^3.10.1" +black = "^24.10.0" +coverage = "^7.6.4" +flake8 = "^7.1.1" +mccabe = "^0.7.0" +mypy = "^1.13.0" +mypy-extensions = "^1.0.0" +pylint = "^3.3.1" +pytest = "^8.3.3" +tox = "^4.12.1" +Sphinx = "^7.4.7" +pip-licenses = "^5.0.0" +sphinx_rtd_theme = "^2.0.0" +sphinx-bootstrap-theme = "^0.8.1" +sphinxcontrib-plantuml = "^0.27" +pytest-html = "^4.1.1" +dockerfile-parse = "^2.0.1" +pytest-mock = "^3.14.0" +docker = "^7.1.0" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/requirements.txt b/{{cookiecutter.project_slug}}/requirements.txt deleted file mode 100644 index eeea964..0000000 --- a/{{cookiecutter.project_slug}}/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -setuptools-scm==6.3.2 -types-setuptools==57.4.4 \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/requirements_data.txt b/{{cookiecutter.project_slug}}/requirements_data.txt deleted file mode 100644 index e69de29..0000000 diff --git a/{{cookiecutter.project_slug}}/requirements_dev.txt b/{{cookiecutter.project_slug}}/requirements_dev.txt deleted file mode 100644 index 4deb201..0000000 --- a/{{cookiecutter.project_slug}}/requirements_dev.txt +++ /dev/null @@ -1,20 +0,0 @@ -pre-commit==2.16.0 -pre-commit-hooks==4.0.1 -flit==3.5.1 -flit-core==3.5.1 -black==20.8b1 -coverage==6.2 -flake8==4.0.1 -mccabe==0.6.1 -mypy==0.910 -mypy-extensions==0.4.3 -pylint==3.0.0a1 -pytest==6.2.5 -tox==3.24.4 -Sphinx==4.3.1 -pip-licenses==3.5.3 -sphinx_rtd_theme -pur==5.4.2 -sphinx-bootstrap-theme==0.8.0 -sphinxcontrib-plantuml -pytest-html==3.1.1 diff --git a/{{cookiecutter.project_slug}}/scripts/data_download.py b/{{cookiecutter.project_slug}}/scripts/data_download.py deleted file mode 100644 index a3d4b02..0000000 --- a/{{cookiecutter.project_slug}}/scripts/data_download.py +++ /dev/null @@ -1,4 +0,0 @@ -import os -import os.path - -# download script TODO diff --git a/{{cookiecutter.project_slug}}/scripts/init_remote_repo.bash b/{{cookiecutter.project_slug}}/scripts/init_remote_repo.bash index e61fa83..d1545f9 100644 --- a/{{cookiecutter.project_slug}}/scripts/init_remote_repo.bash +++ b/{{cookiecutter.project_slug}}/scripts/init_remote_repo.bash @@ -1,4 +1,4 @@ #!/usr/bin/env bash git branch -M main -git remote add origin https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}.git +git remote add origin {{ cookiecutter.project_url }}.git git push -u origin main diff --git a/{{cookiecutter.project_slug}}/scripts/license.py b/{{cookiecutter.project_slug}}/scripts/license.py new file mode 100644 index 0000000..3eca20b --- /dev/null +++ b/{{cookiecutter.project_slug}}/scripts/license.py @@ -0,0 +1,34 @@ +import subprocess +import toml + +# Ouverture du fichier pyproject.toml +with open("pyproject.toml", "r") as file: + # Parsing du fichier TOML + pyproject = toml.loads(file.read()) + +# Récupération des dépendances de développement +dev_dependencies = pyproject["tool"]["poetry"]["dependencies"] + +# Boucle pour extraire les noms des packages sans la version +packages = [] +for package in dev_dependencies: + package_name = package.split("=")[0].strip() + packages.append(package_name) + +subprocess.run( + [ + "pip-licenses", + "--from", + "meta", + "-f", + "rst", + "-a", + "-u", + "-d", + "--output-file", + "third_party.rst", + "-p", + + ] + + packages +) \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/setup.py b/{{cookiecutter.project_slug}}/setup.py deleted file mode 100644 index 258b823..0000000 --- a/{{cookiecutter.project_slug}}/setup.py +++ /dev/null @@ -1,54 +0,0 @@ -import setuptools -from os import path, sep - -here = path.abspath(path.dirname(__file__)) - -# Get the long description from the README file -with open(path.join(here, "README.rst"), encoding="utf-8") as f: - readme = f.read() - -with open("requirements.txt") as f: - required = f.read().splitlines() - -setup_requirements = ['setuptools_scm',{%- if cookiecutter.use_pytest == 'y' %}'pytest-runner',{%- endif %} ] - -test_requirements = [{%- if cookiecutter.use_pytest == 'y' %}'pytest>=3',{%- endif %} ] - -about = {} -with open( - path.join(here, "{{cookiecutter.project_slug}}", "_version.py"), - encoding="utf-8", -) as f: - exec(f.read(), about) - -setuptools.setup( - use_scm_version=True, - name=about["__name_soft__"], - description=about["__description__"], - long_description=readme, - author=about["__author__"], - author_email=about["__author_email__"], - url=about["__url__"], - license=about["__license__"], - long_description_content_type="text/markdown", - packages=setuptools.find_packages(), - setup_requires=setup_requirements, - test_suite='tests', - tests_require=test_requirements, - package_data={about["__name_soft__"]: ["README.md", "logging.conf"]}, - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3){% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License v3 (GPLv3){% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 (LGPLv3){% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3 (AGPLv3){% endif %}", - "Operating System :: OS Independent", - ], - python_requires=">=3.6", - install_requires=required, - entry_points={ - "console_scripts": [ - about["__name_soft__"] - + "=" - + about["__name_soft__"] - + ".__main__:run", - ], - }, # Optional -) diff --git a/{{cookiecutter.project_slug}}/tests/acceptance/docker_test.py b/{{cookiecutter.project_slug}}/tests/acceptance/docker_test.py new file mode 100644 index 0000000..f1ff727 --- /dev/null +++ b/{{cookiecutter.project_slug}}/tests/acceptance/docker_test.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} +# Copyright (C) 2024 - Centre National d'Etudes Spatiales +# This file is part of {{cookiecutter.project_name}} <{{ cookiecutter.project_url }}> +# SPDX-License-Identifier: Apache-2.0 +# Auto-generated by skeleton-python-binary +import pytest +from dockerfile_parse import DockerfileParser +import docker +import re +import requests +import os + + +class TestOptimize: + """Conformance Class for size optimization""" + + @pytest.fixture + def dockerfile_parser(self): + dockerfile_path = 'docker/Dockerfile' + parser = DockerfileParser(dockerfile_path) + return parser + + def test_official_docker_images_size_optimization(self, dockerfile_parser): + """Verify that the size of official Docker images complies with the specified criteria: Base Image: Not exceeding 200 MB""" + image_name = dockerfile_parser.baseimage + package, version = image_name.split(':') + query = requests.get(f"https://hub.docker.com/v2/repositories/library/{package}/tags/{version}/images") + size = -1 + if query.status_code == 200: + size = query.json()[0]['size'] + else: + raise Exception(f"Can't find the {image_name}") + assert size <= 200 * 1024 * 1024 + + + def test_no_temporary_artifacts_in_dockerfile(self, dockerfile_parser): + """Clean Up Temporary Artifacts + + Clean Up Temporary Artifact files, caches, and unnecessary artifacts after each Docker instruction to minimize + their inclusion in the final image. + """ + # Define patterns indicative of clean up temporary artifacts + patterns = [ + r'apt-get\s+clean', # apt-get clean + r'rm\s+-rf\s+\/var\/lib\/apt\/lists\/\*', # rm -rf /var/lib/apt/lists/* + r'ldconfig' # ldconfig + ] + + # Check if patterns are present in the Dockerfile + found_patterns = [] + for pattern in patterns: + if re.search(pattern, dockerfile_parser.content): + found_patterns.append(pattern) + + # Fail the test if patterns are found + assert len(found_patterns) == 3, f"Found patterns indicative of clean up temporary artifacts: {found_patterns}" + + + def test_dockerignore_exists(self): + """Verify that a .dockerignore file exists to exclude unnecessary files and directories during the Docker image build process.""" + assert os.path.exists(".dockerignore"), "Add a .dockerignore" + + def test_limit_dockerfile_instructions(self, dockerfile_parser): + """test_limit_dockerfile_instructions and minimize layers""" + dockerfile_content = dockerfile_parser.content + last_from_index = dockerfile_content.rfind('FROM') + dockerfile_content = dockerfile_content[last_from_index:] + layer_instructions = ['FROM', 'RUN', 'COPY', 'ADD', 'WORKDIR', 'EXPOSE', 'ENV', 'USER', 'VOLUME'] + layer_count = 0 + for instruction in layer_instructions: + layer_count += dockerfile_content.count(instruction) + assert layer_count < 5 + + + def test_detect_multi_stage_build(self, dockerfile_parser): + """Verify that the Dockerfile uses multi-stage builds""" + assert dockerfile_parser.is_multistage, "The build image is not multi-stage" + + +class TestMaintenance: + """Conformance Class for maintenability""" + + @pytest.fixture + def dockerfile_parser(self): + dockerfile_path = 'docker/Dockerfile' + parser = DockerfileParser(dockerfile_path) + return parser + + @pytest.fixture + def labels_parser(self, dockerfile_parser): + return dockerfile_parser.labels + + def test_has_maintainer_label(self, labels_parser): + """Confirm that a LABEL instruction is present in the Dockerfile specifying the maintainer.""" + assert 'org.opencontainers.image.authors' in labels_parser.keys(), "LABEL instruction for authors not found" + assert not labels_parser['org.opencontainers.image.authors'].isspace() + + def test_has_version_label(self, labels_parser): + """Confirm that a LABEL instruction is present in the Dockerfile specifying the build timestamp.""" + assert 'org.opencontainers.image.version' in labels_parser.keys(), "LABEL instruction for version not found" + assert not labels_parser['org.opencontainers.image.authors'].isspace() + + def test_build_timestamp_label(self, labels_parser): + """Confirm that a LABEL instruction is present in the Dockerfile specifying the build timestamp.""" + assert 'org.opencontainers.image.created' in labels_parser.keys(), "LABEL instruction for build timestamp not found" + assert not labels_parser['org.opencontainers.image.created'].isspace() + + def test_has_documentation_label(self, labels_parser): + """Confirm that a LABEL instruction is present in the Dockerfile specifying the documentation URL.""" + assert 'org.opencontainers.image.created' in labels_parser.keys(), "LABEL instruction for documentation not found" + assert not labels_parser['org.opencontainers.image.created'].isspace() + + def test_has_source_label(self, labels_parser): + """Confirm that a LABEL instruction is present in the Dockerfile specifying the source code URL.""" + assert 'org.opencontainers.image.source' in labels_parser.keys(), "LABEL instruction for source not found" + assert not labels_parser['org.opencontainers.image.source'].isspace() + + def test_has_vendor_label(self, labels_parser): + """Confirm that a LABEL instruction is present in the Dockerfile specifying the distributing entity information.""" + assert 'org.opencontainers.image.vendor' in labels_parser.keys(), "LABEL instruction for vendor not found" + assert not labels_parser['org.opencontainers.image.vendor'].isspace() + + def test_has_licenses_label(self, labels_parser): + """Confirm that a LABEL instruction is present in the Dockerfile specifying the software license information.""" + assert 'org.opencontainers.image.licenses' in labels_parser.keys(), "LABEL instruction for licenses not found" + assert not labels_parser['org.opencontainers.image.licenses'].isspace() + + def test_has_title_label(self, labels_parser): + """Confirm that a LABEL instruction is present in the Dockerfile specifying the human-readable image title.""" + assert 'org.opencontainers.image.title' in labels_parser.keys(), "LABEL instruction for title not found" + assert not labels_parser['org.opencontainers.image.title'].isspace() + + def test_has_title_description(self, labels_parser): + """Confirm that a LABEL software description is present in the Dockerfile specifying the human-readable software.""" + assert 'org.opencontainers.image.description' in labels_parser.keys(), "LABEL instruction for description not found" + assert not labels_parser['org.opencontainers.image.description'].isspace() + + def test_has_mame_label(self, labels_parser): + """Confirm that the Dockerfile's FROM instruction specifies a particular version of the base image, ensuring build + consistency and reducing the risk of unexpected changes""" + assert 'org.opencontainers.image.ref.name' in labels_parser.keys(), "LABEL instruction for name not found" + assert not labels_parser['org.opencontainers.image.ref.name'].isspace() + + def test_has_version_from(self, dockerfile_parser): + """Confirm that the Dockerfile's FROM instruction specifies a particular version of the base image, + ensuring build consistency and reducing the risk of unexpected changes""" + version_pattern = r'^\d+(\.\d+)*(-?[a-zA-Z]+)*$' + image_name = dockerfile_parser.baseimage + version_part = image_name.split(":")[-1] + assert version_part != "latest" + assert re.match(version_pattern,version_part) is not None + +class TestSecurity: + """Conformance Class for security""" + + @pytest.fixture + def dockerfile_parser(self): + dockerfile_path = 'docker/Dockerfile' + parser = DockerfileParser(dockerfile_path) + return parser + + def _is_official_image(self, image_name): + client = docker.from_env() + client.images.pull(image_name) + image_info = client.images.get(image_name) + return bool(image_info.attrs.get('RepoDigests')) + + def _contains_update_instructions(self, dockerfile_content): + # Define keywords or patterns indicating update instructions + update_keywords = ['apt-get update', 'apt update', 'apk update', 'pip install --upgrade', 'pip3 install --upgrade'] + + # Check if any of the update keywords are present in the Dockerfile content + for keyword in update_keywords: + if keyword in dockerfile_content: + return True + return False + + def _contains_user_root(self, dockerfile_content): + return "USER root" in dockerfile_content + + def test_is_official_base_image(self, dockerfile_parser): + """Confirm that the Dockerfile specifies an official base image from a trusted registry.""" + base_image = dockerfile_parser.baseimage + assert self._is_official_image(base_image), "No official image found in the Dockerfile" + + def test_regularly_updates_base_images_and_dependencies(self, dockerfile_parser): + """Confirm that the Dockerfile contains instructions for regularly updating base images and dependencies.""" + assert self._contains_update_instructions(dockerfile_parser.content), "No update instructions found in the Dockerfile" + + def test_restrict_user_root_usage(self, dockerfile_parser): + """Confirm that the Dockerfile restricts the use of 'USER root'.""" + assert not self._contains_user_root(dockerfile_parser.content), "Dockerfile contains 'USER root', which is restricted" + +class TestRunning: + """Conformance class with Running""" + + @pytest.fixture + def dockerfile_parser(self): + dockerfile_path = 'docker/Dockerfile' + parser = DockerfileParser(dockerfile_path) + return parser + + def test_contains_workdir(self, dockerfile_parser): + """Confirms that WORKDIR /app is in the dockerfile content.""" + dockerfile_content = dockerfile_parser.content + assert "WORKDIR /app" in dockerfile_content, "Dockerfile does not contain the WORKDIR /app" diff --git a/{{cookiecutter.project_slug}}/tests/unit/monitoring_test.py b/{{cookiecutter.project_slug}}/tests/unit/monitoring_test.py new file mode 100644 index 0000000..f194996 --- /dev/null +++ b/{{cookiecutter.project_slug}}/tests/unit/monitoring_test.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} +# Copyright (C) 2024 - Centre National d'Etudes Spatiales +# This file is part of {{cookiecutter.project_name}} <{{ cookiecutter.project_url }}> +# SPDX-License-Identifier: Apache-2.0 +# Auto-generated by skeleton-python-binary +""" +# Test Suite for UtilsMonitoring Decorators + +This test suite focuses on ensuring that the logging and functionality of +decorators within the `UtilsMonitoring` class are working as expected. + +## Overview of the tests: + +1. **Mock logger**: + We mock the logger to prevent actual logging during tests, ensuring that + we can verify the logging behavior without affecting production logs. + +2. **Tests for `io_display` decorator**: + - `test_io_display_logs_inputs_and_outputs`: + This test checks that the `io_display` decorator logs both the inputs and + outputs of the function `test_func` when configured to do so. + + - `test_io_display_logs_only_inputs`: + This test checks that only the input of the function `test_func` is logged + when the decorator is configured to log only inputs. + + - `test_io_display_logs_only_outputs`: + This test ensures that only the output of the function `test_func` is logged + when the decorator is set to log only outputs. + +3. **Test for `time_spend` decorator**: + - `test_time_spend_logs_function_duration`: + Verifies that the `time_spend` decorator logs the execution duration + of the `test_sleep_func` function, checking for proper log formatting. + +4. **Test for `measure_memory` decorator**: + - `test_measure_memory_logs_memory_usage`: + Confirms that the `measure_memory` decorator logs memory usage for the + function `test_memory_func` which creates a large list, checking that + memory information is logged appropriately. +""" +from loguru import logger +import pytest +import time +from {{cookiecutter.project_slug}}.monitoring import UtilsMonitoring +from unittest.mock import MagicMock, ANY, patch + +# Mock logger to prevent actual logging during tests +@pytest.fixture(autouse=True) +def mock_logger(mocker): + mock_logger = mocker.patch('{{cookiecutter.project_slug}}.monitoring.logger', new_callable=MagicMock) + yield mock_logger + +# Tests for the io_display decorator +def test_io_display_logs_inputs_and_outputs(mock_logger): + @UtilsMonitoring.io_display(input=True, output=True, level="DEBUG") + def test_func(a, b): + return a + b + + result = test_func(3, 4) + assert result == 7 + mock_logger.log.assert_any_call("DEBUG", "Input in the function 'test_func' with args=(3, 4), kwargs={}") + mock_logger.log.assert_any_call("DEBUG", "Output in the function 'test_func' with result=7") + +@patch('{{cookiecutter.project_slug}}.monitoring.logger') +def test_io_display_logs_only_inputs(mock_logger): + @UtilsMonitoring.io_display(input=True, output=False, level="INFO") + def test_func(a, b): + return a * b + + result = test_func(3, 4) + assert result == 12 + + # Check for input logging + mock_logger.log.assert_any_call("INFO", "Input in the function 'test_func' with args=(3, 4), kwargs={}") + + # Assert that the specific output log message was NOT called + output_log_message = "Output in the function 'test_func' with result=12" + assert not any(call[0] == ("INFO", output_log_message) for call in mock_logger.log.call_args_list) + +@patch('{{cookiecutter.project_slug}}.monitoring.logger') +def test_io_display_logs_only_outputs(mock_logger): + @UtilsMonitoring.io_display(input=False, output=True, level="INFO") + def test_func(a, b): + return a - b + + result = test_func(10, 3) + assert result == 7 + + # Check for output logging + mock_logger.log.assert_any_call("INFO", "Output in the function 'test_func' with result=7") + + # Assert that the specific input log message was NOT called + input_log_message = "Input in the function 'test_func' with args=(10, 3), kwargs={}" + assert not any(call[0] == ("INFO", input_log_message) for call in mock_logger.log.call_args_list) + +# Tests for the time_spend decorator +def test_time_spend_logs_function_duration(mock_logger): + @UtilsMonitoring.time_spend(level="INFO") + def test_sleep_func(): + time.sleep(0.1) # Sleep for 100ms + + test_sleep_func() + + # Assert that logging happened with the appropriate log level and check for the correct message format + mock_logger.log.assert_any_call( + "INFO", ANY # Use ANY to match any string + ) + + # Optionally, you can check for a specific pattern if you want to be more strict + # For example, checking if the message contains "finished in" and "ms" + log_calls = [call for call in mock_logger.log.call_args_list] + assert any("finished in" in str(call) for call in log_calls) + +# Tests for the measure_memory decorator +def test_measure_memory_logs_memory_usage(mock_logger): + @UtilsMonitoring.measure_memory(level="WARNING") + def test_memory_func(): + return [i for i in range(10000)] # Create a list + + result = test_memory_func() + assert len(result) == 10000 + assert mock_logger.log.called # Check that logging happened + mock_logger.log.assert_any_call( + "WARNING", ANY # Checking if warning logs contain memory information + ) \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/tests/{{cookiecutter.project_slug}}_test.py b/{{cookiecutter.project_slug}}/tests/{{cookiecutter.project_slug}}_test.py deleted file mode 100644 index d53f66b..0000000 --- a/{{cookiecutter.project_slug}}/tests/{{cookiecutter.project_slug}}_test.py +++ /dev/null @@ -1,106 +0,0 @@ -import logging - -#import numpy as np -import pytest -import {{cookiecutter.project_slug}} -#from PIL import Image - -logger = logging.getLogger(__name__) - -@pytest.fixture -def setup(): - logger.info("----- Init the tests ------") - - -def test_init_setup(setup): - logger.info("Setup is initialized") - - -# def assert_images_equal(image_1: str, image_2: str): -# img1 = Image.open(image_1) -# img2 = Image.open(image_2) - -# # Convert to same mode and size for comparison -# img2 = img2.convert(img1.mode) -# img2 = img2.resize(img1.size) - -# diff = np.asarray(img1).astype("float") - np.asarray(img2).astype("float") - -# sum_sq_diff = np.sum(diff ** 2) # type: ignore - -# if sum_sq_diff == 0: -# # Images are exactly the same -# pass -# else: -# normalized_sum_sq_diff = sum_sq_diff / np.sqrt(sum_sq_diff) -# assert normalized_sum_sq_diff < 0.001 - -def test_name(): - name = {{cookiecutter.project_slug}}.__name_soft__ - assert name == "{{cookiecutter.project_slug}}" - -def test_logger(): - loggers = [logging.getLogger()] - loggers = loggers + [logging.getLogger(name) for name in logging.root.manager.loggerDict] - assert loggers[0].name == "root" - -def test_new_level(): - {{cookiecutter.project_slug}}.custom_logging.UtilsLogs.add_logging_level("TRACE", 20) - logger = logging.getLogger("__main__") - logger.setLevel(logging.TRACE) # type: ignore - ch = logging.StreamHandler() - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - ch.setFormatter(formatter) - logger.addHandler(ch) - logger.trace("Test the logger") # type: ignore - logger.log(logging.TRACE, "test") # type: ignore - - -def test_message(): - record = {{cookiecutter.project_slug}}.custom_logging.LogRecord( - "__main__", - logging.INFO, - "pathname", - 2, - "message {val1} {val2}", - {"val1": 10, "val2": "test"}, - None, - ) - assert "message 10 test", record.getMessage() - - record = {{cookiecutter.project_slug}}.custom_logging.LogRecord( - "__main__", - logging.INFO, - "pathname", - 2, - "message {0} {1}", - ("val1", "val2"), - None, - ) - assert "message val1 val2", record.getMessage() - - -def test_color_formatter(): - formatter = {{cookiecutter.project_slug}}.custom_logging.CustomColorFormatter() - logger = logging.getLogger("{{cookiecutter.project_slug}}.custom_logging") - logger.setLevel(logging.INFO) - ch = logging.StreamHandler() - ch.setFormatter(formatter) - ch.setLevel(logging.INFO) - logger.addHandler(ch) - logger.addHandler(logging.NullHandler()) - logger.info("test") - - shell_formatter = {{cookiecutter.project_slug}}.custom_logging.ShellColorFormatter() - record = {{cookiecutter.project_slug}}.custom_logging.LogRecord( - "__main__", - logging.INFO, - "pathname", - 2, - "message {0} {1}", - ("val1", "val2"), - None, - ) - shell_formatter.format(record) \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py index 8b988ba..b973c08 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__init__.py @@ -1,25 +1,10 @@ -# {{cookiecutter.project_name}} - {{cookiecutter.project_short_description}} +# -*- coding: utf-8 -*- +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} # Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) -# -# This file is part of {{cookiecutter.project_name}}. -# -# {{cookiecutter.project_name}} is free software: you can redistribute it and/or modify -# it under the terms of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# {{cookiecutter.project_name}} is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} for more details. -# -# You should have received a copy of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} -# along with {{cookiecutter.project_name}}. If not, see . -"""{{cookiecutter.project_short_description}}""" -import logging -import logging.config +# This file is part of {{cookiecutter.project_name}} <{{ cookiecutter.project_url }}> +# SPDX-License-Identifier: {% if cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}LGPL-3.0-or-later{% elif cookiecutter.open_source_license == 'Apache V2.0' -%}Apache-2.0{% elif cookiecutter.open_source_license == 'MIT' -%}MIT{% endif %} +"""{{cookiecutter.project_description}}""" import os -from logging import NullHandler from ._version import __author__ from ._version import __author_email__ @@ -30,21 +15,3 @@ from ._version import __title__ from ._version import __url__ from ._version import __version__ -from .custom_logging import LogRecord -from .custom_logging import UtilsLogs - -logging.getLogger(__name__).addHandler(NullHandler()) - -UtilsLogs.add_logging_level("TRACE", 15) -try: - PATH_TO_CONF = os.path.dirname(os.path.realpath(__file__)) - logging.config.fileConfig( - os.path.join(PATH_TO_CONF, "logging.conf"), - disable_existing_loggers=False, - ) - logging.debug( - f"file {os.path.join(PATH_TO_CONF, 'logging.conf')} loaded" - ) -except Exception as exception: # pylint: disable=broad-except - logging.warning(f"cannot load logging.conf : {exception}") -logging.setLogRecordFactory(LogRecord) # pylint: disable=no-member diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__main__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__main__.py index 76f23e2..c32add9 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__main__.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/__main__.py @@ -1,22 +1,10 @@ -# {{cookiecutter.project_name}} - {{cookiecutter.project_short_description}} +# -*- coding: utf-8 -*- +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} # Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) -# -# This file is part of {{cookiecutter.project_name}}. -# -# {{cookiecutter.project_name}} is free software: you can redistribute it and/or modify -# it under the terms of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# {{cookiecutter.project_name}} is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} for more details. -# -# You should have received a copy of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} -# along with {{cookiecutter.project_name}}. If not, see . +# This file is part of {{cookiecutter.project_name}} <{{ cookiecutter.project_url }}> +# SPDX-License-Identifier: {% if cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}LGPL-3.0-or-later{% elif cookiecutter.open_source_license == 'Apache V2.0' -%}Apache-2.0{% elif cookiecutter.open_source_license == 'MIT' -%}MIT{% endif %} """Main program.""" -import logging +from loguru import logger import argparse import signal import sys @@ -34,7 +22,7 @@ def _split_lines(self, text, width): if text.startswith("R|"): return text[2:].splitlines() # this is the RawTextHelpFormatter._split_lines - return argparse.HelpFormatter._split_lines( # pylint: disable=protected-access + return argparse.HelpFormatter._split_lines( # pylint: disable=protected-access self, text, width ) @@ -53,7 +41,7 @@ def signal_handler(self, sig: int, frame): frame: the current stack frame """ # pylint: disable=unused-argument - logging.error("You pressed Ctrl+C") + logger.error("You pressed Ctrl+C") self.SIGINT = True sys.exit(2) @@ -92,11 +80,11 @@ def parse_cli() -> argparse.Namespace: default="conf/{{cookiecutter.project_slug}}.conf", help="The location of the configuration file (default: %(default)s)", ) - + parser.add_argument( - "--output_directory", - default="{{cookiecutter.project_slug}}-data", - help="The output directory where the data products are created (default: %(default)s)", + "--directory", + default="/app", + help="The base directory where the data products are read and saved (default: %(default)s)", ) parser.add_argument( @@ -118,23 +106,19 @@ def parse_cli() -> argparse.Namespace: def run(): """Main function that instanciates the library.""" - logger = logging.getLogger(__name__) handler = SigintHandler() signal.signal(signal.SIGINT, handler.signal_handler) try: options_cli = parse_cli() {{cookiecutter.project_slug}} = {{cookiecutter.project_class_lib}}( - options_cli.conf_file, - options_cli.output_directory, - level=options_cli.level, + **vars(options_cli) ) - logger.info({{cookiecutter.project_slug}}) + {{cookiecutter.project_slug}}.run() sys.exit(0) except Exception as error: # pylint: disable=broad-except - logging.exception(error) + logger.exception(error) sys.exit(1) - print("OK") if __name__ == "__main__": diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/_version.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/_version.py index 37d6d59..f2aa4b2 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/_version.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/_version.py @@ -1,33 +1,28 @@ -# {{cookiecutter.project_name}} - {{cookiecutter.project_short_description}} -# Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) -# -# This file is part of {{cookiecutter.project_name}}. -# -# {{cookiecutter.project_name}} is free software: you can redistribute it and/or modify -# it under the terms of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# {{cookiecutter.project_name}} is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} for more details. -# -# You should have received a copy of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License{% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License{% endif %} -# along with {{cookiecutter.project_name}}. If not, see . +# -*- coding: utf-8 -*- +# skeleton-python-binary - Command-line tool that generates project templates based on predefined Python project template +# Copyright (C) 2024 - Centre National d'Etudes Spatiales +# SPDX-License-Identifier: Apache-2.0 +# Auto-generated by skeleton-python-binary """Project metadata.""" -from pkg_resources import DistributionNotFound -from pkg_resources import get_distribution +import toml +import os -__name_soft__ = "{{cookiecutter.project_slug}}" -try: - __version__ = get_distribution(__name_soft__).version -except DistributionNotFound: - __version__ = "0.0.0" -__title__ = "{{cookiecutter.project_name}}" -__description__ = "{{cookiecutter.project_short_description}}" -__url__ = "https://github.com/pole-surfaces-planetaires/{{ cookiecutter.project_slug }}" -__author__ = "{{cookiecutter.full_name}}" -__author_email__ = "{{cookiecutter.email}}" -__license__ = "{{cookiecutter.open_source_license}}" +project_root = os.path.dirname(os.path.dirname(__file__)) +pyproject_path = os.path.join(project_root, "pyproject.toml") + +# Load metadata from pyproject.toml +with open(pyproject_path, "r") as file: + pyproject_content = toml.load(file) + +project_tool = pyproject_content.get("tool", {}) +project_metadata = project_tool.get("poetry", {}) + +__name_soft__ = project_metadata.get("name", "unknown") +__version__ = project_metadata.get("version", "0.0.0") +__title__ = project_metadata.get("name", "unknown") +__description__ = project_metadata.get("description", "") +__url__ = project_metadata.get("homepage", "") +__author__ = project_metadata.get("authors", [{}])[0] +__author_email__ = project_metadata.get("authors", [{}])[0] +__license__ = project_metadata.get("license", "") __copyright__ = "{{cookiecutter.year}}, {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}})" diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/custom_logging.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/custom_logging.py deleted file mode 100644 index bd98df6..0000000 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/custom_logging.py +++ /dev/null @@ -1,168 +0,0 @@ -# {{cookiecutter.project_name}} - {{cookiecutter.project_short_description}} -# Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) -# -# This file is part of {{cookiecutter.project_name}}. -# -# {{cookiecutter.project_name}} is free software: you can redistribute it and/or modify -# it under the terms of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# {{cookiecutter.project_name}} is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} for more details. -# -# You should have received a copy of the {% if cookiecutter.open_source_license == 'GNU General Public License v3' -%}GNU General Public License{% elif cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}GNU Lesser General Public License v3 {% elif cookiecutter.open_source_license == 'GNU Affero General Public License v3' -%}GNU Affero General Public License v3{% endif %} -# along with {{cookiecutter.project_name}}. If not, see . -"""Module for customizing ths logs.""" -import logging -from typing import Optional - - -class UtilsLogs: # pylint: disable=R0903 - """Utility class for logs.""" - - @staticmethod - def add_logging_level( - level_name: str, level_num: int, method_name: Optional[str] = None - ) -> None: - """Add a new logging level to the `logging` module. - - Parameters - ---------- - level_name: str - level name of the logging - level_num: int - level number related to the level name - method_name: Optional[str] - method for both `logging` itself and the class returned by - `logging.Logger` - - Returns - ------- - None - - Raises - ------ - AttributeError - If this levelName or methodName is already defined in the - logger. - - """ - if not method_name: - method_name = level_name.lower() - - def log_for_level(self, message, *args, **kwargs): - if self.isEnabledFor(level_num): - self._log( # pylint: disable=W0212 - level_num, message, args, **kwargs - ) - - def log_to_root(message, *args, **kwargs): - logging.log(level_num, message, *args, **kwargs) - - logging.addLevelName(level_num, level_name) - setattr(logging, level_name, level_num) - setattr(logging.getLoggerClass(), method_name, log_for_level) - setattr(logging, method_name, log_to_root) - - -class LogRecord(logging.LogRecord): # pylint: disable=R0903 - """Specific class to handle output in logs.""" - - def getMessage(self) -> str: - """Returns the message. - - Format the message according to the type of the message. - - Returns: - str: Returns the message - """ - msg = self.msg - if self.args: - if isinstance(self.args, dict): - msg = msg.format(**self.args) - else: - msg = msg.format(*self.args) - return msg - - -class CustomColorFormatter(logging.Formatter): - """Color formatter.""" - - UtilsLogs.add_logging_level("TRACE", 15) - # Reset - color_Off = "\033[0m" # Text Reset - - log_colors = { - logging.TRACE: "\033[0;36m", # type: ignore # pylint: disable=no-member # cyan - logging.DEBUG: "\033[1;34m", # blue - logging.INFO: "\033[0;32m", # green - logging.WARNING: "\033[1;33m", # yellow - logging.ERROR: "\033[1;31m", # red - logging.CRITICAL: "\033[1;41m", # red reverted - } - - def format(self, record) -> str: - """Format the log. - - Args: - record: the log record - - Returns: - str: the formatted log record - """ - record.levelname = "{}{}{}".format( - CustomColorFormatter.log_colors[record.levelno], - record.levelname, - CustomColorFormatter.color_Off, - ) - record.msg = "{}{}{}".format( - CustomColorFormatter.log_colors[record.levelno], - record.msg, - CustomColorFormatter.color_Off, - ) - - # Select the formatter according to the log if several handlers are - # attached to the logger - my_formatter = logging.Formatter - my_handler = None - handlers = logging.getLogger(__name__).handlers - for handler in handlers: - handler_level = handler.level - if ( - handler_level - == logging.getLogger(__name__).getEffectiveLevel() - ): - if handler.formatter: - my_formatter._fmt = ( # pylint: disable=W0212 - handler.formatter._fmt # pylint: disable=W0212 - ) - my_handler = handler - break - if my_handler is not None: - for handler in handlers: - if handler != my_handler: - logging.getLogger(__name__).removeHandler(handler) - return my_formatter.format(self, record) # type: ignore - - -class ShellColorFormatter(CustomColorFormatter): - """Shell Color formatter.""" - - def format(self, record) -> str: - """Format the log. - - Args: - record: the log record - - Returns: - str: the formatted log record - """ - record.msg = "{}{}{}".format( - CustomColorFormatter.log_colors[logging.INFO], - record.msg, - CustomColorFormatter.color_Off, - ) - return record.msg diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/logging.conf b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/logging.conf deleted file mode 100644 index 3f3c625..0000000 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/logging.conf +++ /dev/null @@ -1,89 +0,0 @@ -[loggers] -keys=root, {{cookiecutter.project_slug}} - -[handlers] -keys=devel,normal_colored, minimal_colored, devel_colored, screen - -[formatters] -keys=minimal,normal,debug,minimal_colored,normal_colored,debug_colored - - -########### -# Loggers # -########### -[logger_root] -handlers=minimal_colored -level=INFO - -[logger_{{cookiecutter.project_slug}}] -qualname={{cookiecutter.project_slug}} -level=INFO -handlers=minimal_colored -propagate=0 - -################ -# Log Handlers # -################ - -[handler_starting] -class=StreamHandler -level=NOTSET -formatter=minimal -args=(sys.stdout,) - -[handler_devel] -class=StreamHandler -level=NOTSET -formatter=debug -args=(sys.stdout,) - -[handler_minimal_colored] -class=StreamHandler -name=minimal_colored -level=INFO -formatter=minimal_colored -args=(sys.stdout,) - -[handler_normal_colored] -class=StreamHandler -level=INFO -formatter=normal_colored -args=(sys.stdout,) - -[handler_devel_colored] -class=StreamHandler -level=DEBUG -formatter=debug_colored -args=(sys.stdout,) - -[handler_screen] -class=StreamHandler -formatter=minimal -level=INFO -args=(sys.stdout,) - - -################## -# Log Formatters # -################## - -[formatter_minimal] -format=%(message)s - -[formatter_normal] -format=(%(name)s): %(asctime)s %(levelname)s %(message)s - -[formatter_debug] -format=(%(name)s:%(lineno)s) %(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s - -[formatter_minimal_colored] -class={{cookiecutter.project_slug}}.custom_logging.CustomColorFormatter -format=%(message)s - -[formatter_normal_colored] -class={{cookiecutter.project_slug}}.custom_logging.CustomColorFormatter -format=(%(name)s): %(asctime)s %(levelname)s %(message)s - -[formatter_debug_colored] -class={{cookiecutter.project_slug}}.custom_logging.CustomColorFormatter -format=(%(name)s:%(lineno)s) %(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/monitoring.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/monitoring.py index 6417693..af9d34b 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/monitoring.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/monitoring.py @@ -1,24 +1,10 @@ -# Copyright (C) 2020-2021 - Centre National d'Etudes Spatiales -# jean-christophe.malapert@cnes.fr -# -# This file is part of {{cookiecutter.project_slug}}. -# -# {{cookiecutter.project_slug}} is a free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 3.0 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, -# MA 02110-1301 USA +# -*- coding: utf-8 -*- +# skeleton-python-binary - Command-line tool that generates project templates based on predefined Python project template +# Copyright (C) 2024 - Centre National d'Etudes Spatiales +# SPDX-License-Identifier: Apache-2.0 +# Auto-generated by skeleton-python-binary """Some Utilities.""" -import logging +from loguru import logger import os import time import tracemalloc @@ -32,56 +18,64 @@ class UtilsMonitoring: # noqa: R0205 # pylint: disable:invalid_name @staticmethod def io_display( - func=None, input=True, output=True, level=15 + func=None, input=True, output=True, level="TRACE" ): # pylint: disable=W0622 - """Monitor the input/output of a function. + """ + Decorator to log the inputs and/or outputs of a function, with a configurable log level. - NB : Do not use this monitoring method on an __init__ if the class - implements __repr__ with attributes + Args: + func (Callable, optional): Function to decorate. Defaults to `None`, which allows the decorator to be used with parameters. + input (bool, optional): Indicates whether to log the inputs of the function. Defaults to `True`. + output (bool, optional): Indicates whether to log the outputs of the function. Defaults to `True`. + level (str, optional): The log level used for logging messages. Defaults to "TRACE". Can be any of the log levels supported by `loguru` ("TRACE", "DEBUG", "INFO", "SUCCESS", "WARNING", "ERROR", "CRITICAL"). - Parameters - ---------- - func: func - function to monitor (default: {None}) - input: bool - True when the function must monitor the input (default: {True}) - output: bool - True when the function must monitor the output (default: {True}) - level: int - Level from which the function must log - Returns - ------- - object : the result of the function - """ - if func is None: - return partial( - UtilsMonitoring.io_display, - input=input, - output=output, - level=level, - ) + Returns: + Callable: The decorated function that logs its inputs and/or outputs. - @wraps(func) - def wrapped(*args, **kwargs): - name = func.__qualname__ - logger = logging.getLogger(__name__ + "." + name) + Usage: + This decorator allows you to log the inputs and/or outputs of a function with a configurable log level. + It can be used for debugging or monitoring function execution by tracking the values of arguments and results. - if input and logger.getEffectiveLevel() >= level: - msg = f"Entering '{name}' (args={args}, kwargs={kwargs})" - logger.log(level, msg) + Example usage: - result = func(*args, **kwargs) + @UtilsMonitoring.io_display(input=True, output=False, level="DEBUG") + def add(a, b): + return a + b - if output and logger.getEffectiveLevel() >= level: - msg = f"Exiting '{name}' (result={result})" - logger.log(level, msg) + add(3, 4) - return result + In this example, only the inputs will be logged at the `DEBUG` level, and the output will not be logged. + + Notes: + - If `func` is `None`, the decorator is configured with the given parameters. + - The `input` and `output` parameters allow fine-grained control over whether the function's arguments or results are logged. + - The `level` parameter must be a valid log level in `loguru` and is converted to uppercase before use. + + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + # Obtenir le logger avec le niveau configuré + log_level = level.upper() + + # Logger les entrées si configuré + if input: + logger.log(log_level, f"Input in the function '{func.__name__}' with args={args}, kwargs={kwargs}") + + # Execute the function + result = func(*args, **kwargs) + + # Log the outputs if configured + if output: + logger.log(log_level, f"Output in the function '{func.__name__}' with result={result}") - return wrapped + return result + return wrapper + return decorator @staticmethod - def time_spend(func=None, level=logging.DEBUG, threshold_in_ms=1000): + def time_spend(func=None, level="DEBUG", threshold_in_ms=1000): """Monitor the performances of a function. Parameters @@ -103,8 +97,6 @@ def time_spend(func=None, level=logging.DEBUG, threshold_in_ms=1000): @wraps(func) def newfunc(*args, **kwargs): - name = func.__qualname__ - logger = logging.getLogger(__name__ + "." + name) start_time = time.time() result = func(*args, **kwargs) elapsed_time = time.time() - start_time @@ -114,69 +106,17 @@ def newfunc(*args, **kwargs): func.__qualname__, elapsed_time * 1000 ), ) - if float(elapsed_time) * 1000 > threshold_in_ms: - logger.warning( - "function [{}] is too long to compute : {:.2f} ms".format( - func.__qualname__, elapsed_time * 1000 - ) - ) - return result - - return newfunc - - @staticmethod - def size(func=None, level=logging.INFO): - """Monitor the number of records in a file. - - Parameters - ---------- - func: func - Function to monitor (default: {None}) - level: int - Level from which the monitoring starts (default: {logging.INFO}) - - Returns - ------- - object : the result of the function - """ - if func is None: - return partial(UtilsMonitoring.size, level=level) - - @wraps(func) - def newfunc(*args, **kwargs): - name = func.__qualname__ - logger = logging.getLogger(__name__ + "." + name) - filename = os.path.basename(args[1]) - logger.log(level, "Loading file '%s'", filename) - result = func(*args, **kwargs) - type_result = type(result) - if type_result in [type({}), type([])]: - nb_records = len(result) - else: - try: - nb_records = result.shape - except AttributeError: # noqa: W0703 - nb_records = None - - if nb_records is not None: - msg = ( - f"File '{filename}' loaded with {str(nb_records)} records" - ) - logger.info(msg) - else: - msg = f"Unable to load the number of records in file '{args[1]}' - type: {type_result}" # pylint: disable=line-too-long - logger.warning(msg) return result return newfunc @staticmethod - def measure_memory(func=None, level=logging.DEBUG): + def measure_memory(func=None, level="DEBUG"): """Measure the memory of the function Args: func (func, optional): Function to measure. Defaults to None. - level (int, optional): Level of the log. Defaults to logging.INFO. + level (int, optional): Level of the log. Defaults to DEBUG. Returns: object : the result of the function @@ -186,8 +126,6 @@ def measure_memory(func=None, level=logging.DEBUG): @wraps(func) def newfunc(*args, **kwargs): - name = func.__qualname__ - logger = logging.getLogger(__name__ + "." + name) tracemalloc.start() result = func(*args, **kwargs) current, peak = tracemalloc.get_traced_memory() diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}.py b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}.py index 6cec653..7d4b51e 100644 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}.py +++ b/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}/{{cookiecutter.project_slug}}.py @@ -1,23 +1,31 @@ +# -*- coding: utf-8 -*- +# {{cookiecutter.project_name}} - {{cookiecutter.project_description}} +# Copyright (C) {{cookiecutter.year}} - {{cookiecutter.institute}} ({{cookiecutter.full_name}} for {{cookiecutter.consortium_name}}) +# This file is part of {{cookiecutter.project_name}} <{{ cookiecutter.project_url }}> +# SPDX-License-Identifier: {% if cookiecutter.open_source_license == 'GNU Lesser General Public License v3' -%}LGPL-3.0-or-later{% elif cookiecutter.open_source_license == 'Apache V2.0' -%}Apache-2.0{% elif cookiecutter.open_source_license == 'MIT' -%}MIT{% endif %} """This module contains the library.""" -import logging +import sys import configparser +from loguru import logger from ._version import __name_soft__ - -logger = logging.getLogger(__name__) - +from py_sas_data_modeler.dm import DataClassFactory +from py_sas_data_modeler.io import HDF5StorageSasTask +from typing import Any +from typing import Dict class {{cookiecutter.project_class_lib}}: """The library""" - - def __init__(self, path_to_conf: str, directory: str, *args, **kwargs): + + def __init__(self, **kwargs): # pylint: disable=unused-argument - if "level" in kwargs: - {{cookiecutter.project_class_lib}}._parse_level(kwargs["level"]) - - self.__directory = directory - self.__config = configparser.ConfigParser() + path_to_conf = kwargs.get("conf_file", "conf/testmyproject.conf") + self.__config: configparser.ConfigParser = configparser.ConfigParser() self.__config.optionxform = str # type: ignore self.__config.read(path_to_conf) + + self.__level: str = kwargs.get("level", 'INFO') + {{cookiecutter.project_class_lib}}._parse_level(self.__level) + @staticmethod def _parse_level(level: str): @@ -27,24 +35,24 @@ def _parse_level(level: str): Args: level (str): level name """ - logger_main = logging.getLogger(__name_soft__) if level == "INFO": - logger_main.setLevel(logging.INFO) + loguru_level = "INFO" elif level == "DEBUG": - logger_main.setLevel(logging.DEBUG) + loguru_level = "DEBUG" elif level == "WARNING": - logger_main.setLevel(logging.WARNING) + loguru_level = "WARNING" elif level == "ERROR": - logger_main.setLevel(logging.ERROR) + loguru_level = "ERROR" elif level == "CRITICAL": - logger_main.setLevel(logging.CRITICAL) + loguru_level = "CRITICAL" elif level == "TRACE": - logger_main.setLevel(logging.TRACE) # type: ignore # pylint: disable=no-member - else: - logger_main.warning( - "Unknown level name : %s - setting level to INFO", level - ) - logger_main.setLevel(logging.INFO) + loguru_level = "TRACE" + else: + loguru_level = "INFO" + logger.log(loguru_level, f"Unknown level name : {level} - setting level to INFO") + + logger.remove() + logger.add(sys.stdout, level=loguru_level) @property def config(self) -> configparser.ConfigParser: @@ -54,12 +62,6 @@ def config(self) -> configparser.ConfigParser: :type: configparser.ConfigParser """ return self.__config - - @property - def directory(self) -> str: - """The output directory. - - :getter: Returns the output directory - :type: str - """ - return self.__directory + + def run(self, *args, **kwargs): + pass