From 9c6edd280692ebcf9f39fe11d5ea790f3eac8c3a Mon Sep 17 00:00:00 2001 From: sreyakumar <121137643+sreyakumar@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:48:20 -0700 Subject: [PATCH] Initial commit --- .flake8 | 6 + .github/ISSUE_TEMPLATE/bug_report.md | 38 +++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++ .github/ISSUE_TEMPLATE/user-story.md | 27 ++++ .github/workflows/init.yml | 52 +++++++ .github/workflows/tag_and_publish.yml | 89 +++++++++++ .github/workflows/test_and_lint.yml | 26 ++++ .gitignore | 139 ++++++++++++++++++ LICENSE | 21 +++ README.md | 100 +++++++++++++ doc_template/Makefile | 20 +++ doc_template/make.bat | 35 +++++ doc_template/source/_static/dark-logo.svg | 129 ++++++++++++++++ doc_template/source/_static/favicon.ico | Bin 0 -> 259838 bytes doc_template/source/_static/light-logo.svg | 128 ++++++++++++++++ doc_template/source/conf.py | 51 +++++++ doc_template/source/index.rst | 22 +++ pyproject.toml | 80 ++++++++++ setup.py | 4 + src/aind_library_template/__init__.py | 3 + src/aind_library_template/message_handlers.py | 34 +++++ tests/__init__.py | 1 + tests/test_example.py | 16 ++ tests/test_message_handler.py | 42 ++++++ 24 files changed, 1083 insertions(+) create mode 100644 .flake8 create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/user-story.md create mode 100644 .github/workflows/init.yml create mode 100644 .github/workflows/tag_and_publish.yml create mode 100644 .github/workflows/test_and_lint.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 doc_template/Makefile create mode 100644 doc_template/make.bat create mode 100644 doc_template/source/_static/dark-logo.svg create mode 100644 doc_template/source/_static/favicon.ico create mode 100644 doc_template/source/_static/light-logo.svg create mode 100644 doc_template/source/conf.py create mode 100644 doc_template/source/index.rst create mode 100644 pyproject.toml create mode 100644 setup.py create mode 100644 src/aind_library_template/__init__.py create mode 100644 src/aind_library_template/message_handlers.py create mode 100644 tests/__init__.py create mode 100644 tests/test_example.py create mode 100644 tests/test_message_handler.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..6d5ce4f --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +exclude = + .git, + __pycache__, + build +max-complexity = 10 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/user-story.md b/.github/ISSUE_TEMPLATE/user-story.md new file mode 100644 index 0000000..0a635a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/user-story.md @@ -0,0 +1,27 @@ +--- +name: User story +about: This template provides a basic structure for user story issues. +title: '' +labels: '' +assignees: '' + +--- + +# User story +As a ..., I want to ..., so I can ... + +*Ideally, this is in the issue title, but if not, you can put it here. If so, delete this section.* + +# Acceptance criteria +- [ ] This is something that can be verified to show that this user story is satisfied. + +# Sprint Ready Checklist +- [ ] 1. Acceptance criteria defined +- [ ] 2. Team understands acceptance criteria +- [ ] 3. Team has defined solution / steps to satisfy acceptance criteria +- [ ] 4. Acceptance criteria is verifiable / testable +- [ ] 5. External / 3rd Party dependencies identified +- [ ] 6. Ticket is prioritized and sized + +# Notes +*Add any helpful notes here.* diff --git a/.github/workflows/init.yml b/.github/workflows/init.yml new file mode 100644 index 0000000..0336013 --- /dev/null +++ b/.github/workflows/init.yml @@ -0,0 +1,52 @@ +# Workflow runs only once when the template is first used. +# File can be safely deleted after repo is initialized. +name: Initialize repository +on: + push: + branches: + - main + +jobs: + initialize-package: + name: Initialize the package + if: ${{github.event.repository.name != 'aind-library-template'}} + runs-on: ubuntu-latest + env: + REPO_NAME: ${{ github.event.repository.name }} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Rename package + run: | + pkg_name=$(echo "${REPO_NAME}" | tr - _) + current_description='description = "Prints messages to stdout. Simple boilerplate for libraries."' + new_description='description = "Generated from aind-library-template"' + readme_description='Template for a minimal, basic repository for an AIND library.' + new_readme_description='Generated from aind-library-template' + echo "Package Name ${pkg_name}" + mkdir src/${pkg_name} + touch src/${pkg_name}/__init__.py + echo '"""Init package"""' >> src/${pkg_name}/__init__.py + echo '__version__ = "0.0.0"' >> src/${pkg_name}/__init__.py + sed -i "s/aind_library_template/${pkg_name}/" pyproject.toml + sed -i "s/aind-library-template/${REPO_NAME}/" pyproject.toml + sed -i "s/aind_library_template/${pkg_name}/" doc_template/source/conf.py + sed -i "s/${current_description}/${new_description}/" pyproject.toml + sed -i "/pandas/d" pyproject.toml + sed -i "s/aind-library-template/${REPO_NAME}/" README.md + sed -i "s/${readme_description}/${new_readme_description}/" README.md + - name: Commit changes + uses: EndBug/add-and-commit@v9 + with: + default_author: github_actions + message: "ci: version bump [skip actions]" + add: '["pyproject.toml", "README.md", "src/*", "doc_template/source/conf.py"]' + remove: '["-r src/aind_library_template", "tests/test_message_handler.py"]' + - name: Add first tag + run: | + git tag v0.0.0 + git push origin v0.0.0 + - name: Disable workflow + run: | + gh workflow disable -R $GITHUB_REPOSITORY "${{ github.workflow }}" diff --git a/.github/workflows/tag_and_publish.yml b/.github/workflows/tag_and_publish.yml new file mode 100644 index 0000000..6418422 --- /dev/null +++ b/.github/workflows/tag_and_publish.yml @@ -0,0 +1,89 @@ +name: Tag and publish +on: + push: + branches: + - main +# Remove line 65 to enable automated semantic version bumps. +# Change line 71 from "if: false" to "if: true" to enable PyPI publishing. +# Requires that svc-aindscicomp be added as an admin to repo. +jobs: + update_badges: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v3 + with: + ref: ${{ env.DEFAULT_BRANCH }} + fetch-depth: 0 + token: ${{ secrets.SERVICE_TOKEN }} + - name: Set up Python 3.8 + uses: actions/setup-python@v3 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install -e .[dev] --no-cache-dir + - name: Get Python version and Update README.md + run: | + python_version=$(grep "requires-python" pyproject.toml | grep -o ">=[^\"]*") + python_badge=$(grep -o 'python-[^)]*' README.md) + new_python_badge="python-$python_version-blue?logo=python" + sed -i "s/$python_badge/$new_python_badge/g" README.md + - name: Get interrogate values and Update README.md + run: | + interrogate_val=$(interrogate . | grep -o 'actual: [0-9]*\.[0-9]*' | awk '{print $2}') + interrogate_badge=$(grep -o 'interrogate-[^)]*' README.md) + if (( $(echo "$interrogate_val >= 90.00" | bc -l) )); then + new_interrogate_badge="interrogate-$interrogate_val%25-brightgreen" + elif (( $(echo "$interrogate_val < 80.00" | bc -l) )); then + new_interrogate_badge="interrogate-$interrogate_val%25-red" + else + new_interrogate_badge="interrogate-$interrogate_val%25-yellow" + fi + sed -i "s/$interrogate_badge/$new_interrogate_badge/g" README.md + - name: Get Coverage values and Update README.md + run: | + coverage run -m unittest discover + coverage_val=$(coverage report | grep "^TOTAL" | grep -o '[0-9]\+%' | grep -o '[0-9]\+') + coverage_badge=$(grep -o "coverage-[^?]*" README.md) + if (( $(echo "$coverage_val >= 90.00" | bc -l) )); then + new_coverage_badge="coverage-$coverage_val%25-brightgreen" + elif (( $(echo "$coverage_val < 80.00" | bc -l) )); then + new_coverage_badge="coverage-$coverage_val%25-red" + else + new_coverage_badge="coverage-$coverage_val%25-yellow" + fi + sed -i "s/$coverage_badge/$new_coverage_badge/g" README.md + - name: Commit changes + uses: EndBug/add-and-commit@v9 + with: + default_author: github_actions + message: "ci: update badges [skip actions]" + add: '["README.md"]' + tag: + needs: update_badges + if: ${{github.event.repository.name == 'aind-library-template'}} + uses: AllenNeuralDynamics/aind-github-actions/.github/workflows/tag.yml@main + secrets: + SERVICE_TOKEN: ${{ secrets.SERVICE_TOKEN }} + publish: + needs: tag + if: false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Pull latest changes + run: git pull origin main + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install dependencies + run: | + pip install --upgrade setuptools wheel twine build + python -m build + twine check dist/* + - name: Publish on PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.AIND_PYPI_TOKEN }} diff --git a/.github/workflows/test_and_lint.yml b/.github/workflows/test_and_lint.yml new file mode 100644 index 0000000..c8d832d --- /dev/null +++ b/.github/workflows/test_and_lint.yml @@ -0,0 +1,26 @@ +name: Lint and run tests + +on: + pull_request: + branches: + - main + +jobs: + ci: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [ '3.8', '3.9', '3.10' ] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install -e .[dev] + - name: Run linter checks + run: flake8 . && interrogate --verbose . + - name: Run tests and coverage + run: coverage run -m unittest discover && coverage report diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06a56dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,139 @@ +# 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/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# MacOs +**/.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d8f2a22 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Allen Institute for Neural Dynamics + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5bfe53 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# aind-library-template + +[![License](https://img.shields.io/badge/license-MIT-brightgreen)](LICENSE) +![Code Style](https://img.shields.io/badge/code%20style-black-black) +[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) +![Interrogate](https://img.shields.io/badge/interrogate-100.0%25-brightgreen) +![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?logo=codecov) +![Python](https://img.shields.io/badge/python->=3.7-blue?logo=python) + + + +## Usage + - To use this template, click the green `Use this template` button and `Create new repository`. + - After github initially creates the new repository, please wait an extra minute for the initialization scripts to finish organizing the repo. + - To enable the automatic semantic version increments: in the repository go to `Settings` and `Collaborators and teams`. Click the green `Add people` button. Add `svc-aindscicomp` as an admin. Modify the file in `.github/workflows/tag_and_publish.yml` and remove the if statement in line 65. The semantic version will now be incremented every time a code is committed into the main branch. + - To publish to PyPI, enable semantic versioning and uncomment the publish block in `.github/workflows/tag_and_publish.yml`. The code will now be published to PyPI every time the code is committed into the main branch. + - The `.github/workflows/test_and_lint.yml` file will run automated tests and style checks every time a Pull Request is opened. If the checks are undesired, the `test_and_lint.yml` can be deleted. The strictness of the code coverage level, etc., can be modified by altering the configurations in the `pyproject.toml` file and the `.flake8` file. + +## Installation +To use the software, in the root directory, run +```bash +pip install -e . +``` + +To develop the code, run +```bash +pip install -e .[dev] +``` + +## Contributing + +### Linters and testing + +There are several libraries used to run linters, check documentation, and run tests. + +- Please test your changes using the **coverage** library, which will run the tests and log a coverage report: + +```bash +coverage run -m unittest discover && coverage report +``` + +- Use **interrogate** to check that modules, methods, etc. have been documented thoroughly: + +```bash +interrogate . +``` + +- Use **flake8** to check that code is up to standards (no unused imports, etc.): +```bash +flake8 . +``` + +- Use **black** to automatically format the code into PEP standards: +```bash +black . +``` + +- Use **isort** to automatically sort import statements: +```bash +isort . +``` + +### Pull requests + +For internal members, please create a branch. For external members, please fork the repository and open a pull request from the fork. We'll primarily use [Angular](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit) style for commit messages. Roughly, they should follow the pattern: +```text +(): +``` + +where scope (optional) describes the packages affected by the code changes and type (mandatory) is one of: + +- **build**: Changes that affect build tools or external dependencies (example scopes: pyproject.toml, setup.py) +- **ci**: Changes to our CI configuration files and scripts (examples: .github/workflows/ci.yml) +- **docs**: Documentation only changes +- **feat**: A new feature +- **fix**: A bugfix +- **perf**: A code change that improves performance +- **refactor**: A code change that neither fixes a bug nor adds a feature +- **test**: Adding missing tests or correcting existing tests + +### Semantic Release + +The table below, from [semantic release](https://github.com/semantic-release/semantic-release), shows which commit message gets you which release type when `semantic-release` runs (using the default configuration): + +| Commit message | Release type | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | +| `fix(pencil): stop graphite breaking when too much pressure applied` | ~~Patch~~ Fix Release, Default release | +| `feat(pencil): add 'graphiteWidth' option` | ~~Minor~~ Feature Release | +| `perf(pencil): remove graphiteWidth option`

`BREAKING CHANGE: The graphiteWidth option has been removed.`
`The default graphite width of 10mm is always used for performance reasons.` | ~~Major~~ Breaking Release
(Note that the `BREAKING CHANGE: ` token must be in the footer of the commit) | + +### Documentation +To generate the rst files source files for documentation, run +```bash +sphinx-apidoc -o doc_template/source/ src +``` +Then to create the documentation HTML files, run +```bash +sphinx-build -b html doc_template/source/ doc_template/build/html +``` +More info on sphinx installation can be found [here](https://www.sphinx-doc.org/en/master/usage/installation.html). diff --git a/doc_template/Makefile b/doc_template/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/doc_template/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/doc_template/make.bat b/doc_template/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/doc_template/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/doc_template/source/_static/dark-logo.svg b/doc_template/source/_static/dark-logo.svg new file mode 100644 index 0000000..dcc68fb --- /dev/null +++ b/doc_template/source/_static/dark-logo.svg @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc_template/source/_static/favicon.ico b/doc_template/source/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4cec150488e1d2c857ce6e4c1f73b0f048a6bc42 GIT binary patch literal 259838 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc_template/source/conf.py b/doc_template/source/conf.py new file mode 100644 index 0000000..68ed126 --- /dev/null +++ b/doc_template/source/conf.py @@ -0,0 +1,51 @@ +"""Configuration file for the Sphinx documentation builder.""" +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path Setup -------------------------------------------------------------- +from os.path import dirname, abspath +from pathlib import Path +from datetime import date +from aind_library_template import __version__ as package_version + +INSTITUTE_NAME = "Allen Institute for Neural Dynamics" + +current_year = date.today().year + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = Path(dirname(dirname(dirname(abspath(__file__))))).name +copyright = f"{current_year}, {INSTITUTE_NAME}" +author = INSTITUTE_NAME +release = package_version + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [ + "sphinx.ext.duration", + "sphinx.ext.doctest", + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", +] +templates_path = ["_templates"] +exclude_patterns = [] + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "furo" +html_static_path = ["_static"] +html_favicon = "_static/favicon.ico" +html_theme_options = { + "light_logo": "light-logo.svg", + "dark_logo": "dark-logo.svg", +} + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = False diff --git a/doc_template/source/index.rst b/doc_template/source/index.rst new file mode 100644 index 0000000..07adcad --- /dev/null +++ b/doc_template/source/index.rst @@ -0,0 +1,22 @@ +.. Doc Template documentation master file, created by + sphinx-quickstart on Wed Aug 17 15:36:32 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + + +Welcome to this repository's documentation! +=========================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + modules + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f1b6607 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,80 @@ +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[project] +name = "aind-library-template" +description = "Prints messages to stdout. Simple boilerplate for libraries." +license = {text = "MIT"} +requires-python = ">=3.7" +authors = [ + {name = "Allen Institute for Neural Dynamics"} +] +classifiers = [ + "Programming Language :: Python :: 3" +] +readme = "README.md" +dynamic = ["version"] + +dependencies = [ + 'pandas' +] + +[project.optional-dependencies] +dev = [ + 'black', + 'coverage', + 'flake8', + 'interrogate', + 'isort', + 'Sphinx', + 'furo' +] + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.dynamic] +version = {attr = "aind_library_template.__version__"} + +[tool.black] +line-length = 79 +target_version = ['py36'] +exclude = ''' + +( + /( + \.eggs # exclude a few common directories in the + | \.git # root of the project + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | _build + | build + | dist + )/ + | .gitignore +) +''' + +[tool.coverage.run] +omit = ["*__init__*"] +source = ["aind_library_template", "tests"] + +[tool.coverage.report] +exclude_lines = [ + "if __name__ == .__main__.:", + "from", + "import", + "pragma: no cover" +] +fail_under = 100 + +[tool.isort] +line_length = 79 +profile = "black" + +[tool.interrogate] +exclude = ["setup.py", "docs", "build"] +fail-under = 100 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7f1a176 --- /dev/null +++ b/setup.py @@ -0,0 +1,4 @@ +from setuptools import setup + +if __name__ == "__main__": + setup() diff --git a/src/aind_library_template/__init__.py b/src/aind_library_template/__init__.py new file mode 100644 index 0000000..6df6820 --- /dev/null +++ b/src/aind_library_template/__init__.py @@ -0,0 +1,3 @@ +"""Simple package to demo project structure. +""" +__version__ = "0.3.4" diff --git a/src/aind_library_template/message_handlers.py b/src/aind_library_template/message_handlers.py new file mode 100644 index 0000000..cd13cf3 --- /dev/null +++ b/src/aind_library_template/message_handlers.py @@ -0,0 +1,34 @@ +""" Module to handle printing messages to stdout. +""" + +import logging + +import pandas as pd + + +class MessageHandler: + """ + Class to handle messages. + """ + + def __init__(self, msg): + """ + Args: + msg (str): Message to handle. + """ + self.msg = msg + + def log_msg(self): + """Simply logs the message.""" + logging.info(self.msg) + + def msg_as_df(self, col_name="message"): + """Returns message as a dataframe. + Args: + col_name (str, optional): Column name for message. + Defaults to None. + + Returns: + pandas DataFrame + """ + return pd.DataFrame.from_dict({col_name: [self.msg]}) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..816e430 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Testing library""" diff --git a/tests/test_example.py b/tests/test_example.py new file mode 100644 index 0000000..06e9e0d --- /dev/null +++ b/tests/test_example.py @@ -0,0 +1,16 @@ +"""Example test template.""" + +import unittest + + +class ExampleTest(unittest.TestCase): + """Example Test Class""" + + def test_assert_example(self): + """Example of how to test the truth of a statement.""" + + self.assertTrue(1 == 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_message_handler.py b/tests/test_message_handler.py new file mode 100644 index 0000000..75cd784 --- /dev/null +++ b/tests/test_message_handler.py @@ -0,0 +1,42 @@ +"""Tests aind_library_template printers methods.""" + +import unittest + +import pandas as pd + +from aind_library_template import message_handlers + + +class MessageHandlerTest(unittest.TestCase): + """Tests MessageHandler methods.""" + + my_msg = "Hello World!!" + p = message_handlers.MessageHandler(my_msg) + + def test_log_msg(self): + """Tests that the log_msg method logs a message.""" + with self.assertLogs() as captured: + self.p.log_msg() + + self.assertEqual(len(captured.records), 1) + self.assertEqual(captured.records[0].getMessage(), self.my_msg) + + def test_msg_as_df(self): + """Tests that the message gets returned as a pandas DataFrame.""" + + # df from msg with default col_name + df1 = self.p.msg_as_df() + # df from msg with non-default col_name + df2 = self.p.msg_as_df(col_name="non_default") + + # Expected outputs + expected_df1 = pd.DataFrame.from_dict({"message": [self.my_msg]}) + expected_df2 = pd.DataFrame.from_dict({"non_default": [self.my_msg]}) + + self.assertTrue(df1.equals(expected_df1)) + self.assertTrue(df2.equals(expected_df2)) + self.assertTrue(not df1.equals(expected_df2)) + + +if __name__ == "__main__": + unittest.main()