Repository: braingeneers/braingeneerspy (language: python)

Issue:
Optimize datasets to look for local (/public/groups/braingeneers) if it exists
@atspaeth @ashsrobbins Thinking I should make datasets.load_*() try and see if /public/groups/braingeneers/archive exists and if so read from it. Pro is that it will be much faster when running on GI servers, con is it pretty specific to our environment and the right way to scale is likely k8s jobs on PRP etc....

Opinions?


Discussion / hints:
Isn't it already really specific to our environment? We have hardcoded subpaths and are using the S3 endpoint environment variable as a basis for HTTP requests.

Maybe just make that origin URL configurable, and modify the two JSON-loading functions to allow filesystem paths in addition to URLs? It could default to accessing our dataset over HTTP like it does now, and people running on GI could change the configuration to get the speed boost.
Its specific behind the scene. As all the data (so far) is public anyone could run a colab using the library. I have coded the URLs to the CEPH endpoints (external, and now it checks if there is an internal one in case we're running on PRP). This would just add another optimization behind the scenes while retaining the public simplicity. I think I can make it so it will just choose the best automatically - maybe too much magic but at lease no one needs to then deal with the details (unless you are writing to the archive).

Fix patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..ab4422b
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,15 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+{
+	"image":"ghcr.io/braingeneers/research:latest",
+
+	"customizations": {
+		"vscode": {
+		  "extensions": [
+			"ms-toolsai.jupyter",
+			"ms-python.python",
+			"ms-vsliveshare.vsliveshare"
+		]
+		}
+	},
+	"postCreateCommand": "sh .devcontainer/post_create.sh"
+}
diff --git a/.devcontainer/post_create.sh b/.devcontainer/post_create.sh
new file mode 100644
index 0000000..664c1e2
--- /dev/null
+++ b/.devcontainer/post_create.sh
@@ -0,0 +1,7 @@
+# For writing commands that will be executed after the container is created
+
+# Uninstalls the braingeneerspy package (pre-installed in the research Docker image) from the environment
+python3 -m pip uninstall braingeneerspy
+
+# Installs a Python package located in the current directory in editable mode and includes all optional extras specified in the [all] section of braingeneers.
+python3 -m pip install -e ".[all]"
diff --git a/.git_archival.txt b/.git_archival.txt
new file mode 100644
index 0000000..8fb235d
--- /dev/null
+++ b/.git_archival.txt
@@ -0,0 +1,4 @@
+node: $Format:%H$
+node-date: $Format:%cI$
+describe-name: $Format:%(describe:tags=true,match=*[0-9]*)$
+ref-names: $Format:%D$
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..00a7b00
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,1 @@
+.git_archival.txt  export-subst
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
new file mode 100644
index 0000000..2560b1a
--- /dev/null
+++ b/.github/CONTRIBUTING.md
@@ -0,0 +1,101 @@
+See the [Scientific Python Developer Guide][spc-dev-intro] for a detailed
+description of best practices for developing scientific packages.
+
+[spc-dev-intro]: https://scientific-python-cookie.readthedocs.io/guide/intro
+
+# Quick development
+
+The fastest way to start with development is to use nox. If you don't have nox,
+you can use `pipx run nox` to run it without installing, or `pipx install nox`.
+If you don't have pipx (pip for applications), then you can install with with
+`pip install pipx` (the only case were installing an application with regular
+pip is reasonable). If you use macOS, then pipx and nox are both in brew, use
+`brew install pipx nox`.
+
+To use, run `nox`. This will lint and test using every installed version of
+Python on your system, skipping ones that are not installed. You can also run
+specific jobs:
+
+```console
+$ nox -s lint  # Lint only
+$ nox -s tests  # Python tests
+$ nox -s docs -- serve  # Build and serve the docs
+$ nox -s build  # Make an SDist and wheel
+```
+
+Nox handles everything for you, including setting up an temporary virtual
+environment for each run.
+
+# Setting up a development environment manually
+
+You can set up a development environment by running:
+
+```bash
+python3 -m venv .venv
+source ./.venv/bin/activate
+pip install -v -e .[dev]
+```
+
+If you have the
+[Python Launcher for Unix](https://github.com/brettcannon/python-launcher), you
+can instead do:
+
+```bash
+py -m venv .venv
+py -m install -v -e .[dev]
+```
+
+# Post setup
+
+You should prepare pre-commit, which will help you by checking that commits pass
+required checks:
+
+```bash
+pip install pre-commit # or brew install pre-commit on macOS
+pre-commit install # Will install a pre-commit hook into the git repo
+```
+
+You can also/alternatively run `pre-commit run` (changes only) or
+`pre-commit run --all-files` to check even without installing the hook.
+
+# Testing
+
+Use pytest to run the unit checks:
+
+```bash
+pytest
+```
+
+# Coverage
+
+Use pytest-cov to generate coverage reports:
+
+```bash
+pytest --cov=braingeneers
+```
+
+# Building docs
+
+You can build the docs using:
+
+```bash
+nox -s docs
+```
+
+You can see a preview with:
+
+```bash
+nox -s docs -- serve
+```
+
+# Pre-commit
+
+This project uses pre-commit for all style checking. While you can run it with
+nox, this is such an important tool that it deserves to be installed on its own.
+Install pre-commit and run:
+
+```bash
+pre-commit run -a
+```
+
+to check all files.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..6fddca0
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,7 @@
+version: 2
+updates:
+  # Maintain dependencies for GitHub Actions
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/.github/matchers/pylint.json b/.github/matchers/pylint.json
new file mode 100644
index 0000000..e3a6bd1
--- /dev/null
+++ b/.github/matchers/pylint.json
@@ -0,0 +1,32 @@
+{
+  "problemMatcher": [
+    {
+      "severity": "warning",
+      "pattern": [
+        {
+          "regexp": "^([^:]+):(\\d+):(\\d+): ([A-DF-Z]\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
+          "file": 1,
+          "line": 2,
+          "column": 3,
+          "code": 4,
+          "message": 5
+        }
+      ],
+      "owner": "pylint-warning"
+    },
+    {
+      "severity": "error",
+      "pattern": [
+        {
+          "regexp": "^([^:]+):(\\d+):(\\d+): (E\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
+          "file": 1,
+          "line": 2,
+          "column": 3,
+          "code": 4,
+          "message": 5
+        }
+      ],
+      "owner": "pylint-error"
+    }
+  ]
+}
diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml
new file mode 100644
index 0000000..904ba48
--- /dev/null
+++ b/.github/workflows/cd.yml
@@ -0,0 +1,101 @@
+name: CD
+
+on:
+  workflow_dispatch:
+  push:
+    branches:
+      - master
+      - main
+      - development
+  release:
+    types:
+      - published
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+env:
+  FORCE_COLOR: 3
+
+jobs:
+  dist:
+    name: Distribution build
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - name: Build sdist and wheel
+        run: pipx run build
+
+      - uses: actions/upload-artifact@v3
+        with:
+          path: dist
+
+      - name: Check products
+        run: pipx run twine check dist/*
+  
+  test-built-dist:
+    needs: [dist]
+    name: Test built distribution
+    runs-on: ubuntu-latest
+    permissions:
+      id-token: write
+    steps:
+      - uses: actions/setup-python@v4.7.0
+        name: Install Python
+        with:
+          python-version: '3.10'
+      - uses: actions/download-artifact@v3
+        with:
+          name: artifact
+          path: dist
+      - name: List contents of built dist
+        run: |
+          ls -ltrh
+          ls -ltrh dist
+      - name: Publish to Test PyPI
+        uses: pypa/gh-action-pypi-publish@v1.8.10
+        with:
+          repository-url: https://test.pypi.org/legacy/
+          verbose: true
+          skip-existing: true
+      - name: Check pypi packages
+        run: |
+          sleep 3
+          python -m pip install --upgrade pip
+
+          echo "=== Testing wheel file ==="
+          # Install wheel to get dependencies and check import
+          python -m pip install --extra-index-url https://test.pypi.org/simple --upgrade --pre braingeneers
+          python -c "import braingeneers; print(braingeneers.__version__)"
+          echo "=== Done testing wheel file ==="
+
+          echo "=== Testing source tar file ==="
+          # Install tar gz and check import
+          python -m pip uninstall --yes braingeneers
+          python -m pip install --extra-index-url https://test.pypi.org/simple --upgrade --pre --no-binary=:all: braingeneers
+          python -c "import braingeneers; print(braingeneers.__version__)"
+          echo "=== Done testing source tar file ==="
+
+
+  publish:
+    needs: [dist, test-built-dist]
+    name: Publish to PyPI
+    environment: pypi
+    permissions:
+      id-token: write
+    runs-on: ubuntu-latest
+    if: github.event_name == 'release' && github.event.action == 'published'
+
+    steps:
+      - uses: actions/download-artifact@v3
+        with:
+          name: artifact
+          path: dist
+
+      - uses: pypa/gh-action-pypi-publish@v1.8.10
+        if: startsWith(github.ref, 'refs/tags')
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..2581a72
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,50 @@
+name: CI
+
+on:
+  workflow_dispatch:
+  pull_request:
+  push:
+    branches:
+      - master
+      - main
+      - development
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+env:
+  FORCE_COLOR: 3
+
+jobs:
+  checks:
+    name: Check Python ${{ matrix.python-version }} on ${{ matrix.runs-on }}
+    runs-on: ${{ matrix.runs-on }}
+    continue-on-error: ${{ matrix.experimental }}
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: ["3.10", "3.11", "3.12"]
+        runs-on: [ubuntu-latest, macos-latest, windows-latest]
+        experimental: [false, false, true]
+
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          fetch-depth: 0
+
+      - uses: actions/setup-python@v4
+        with:
+          python-version: ${{ matrix.python-version }}
+          allow-prereleases: true
+
+      - name: Install package
+        run: python -m pip install .[test]
+
+      - name: Test package
+        run: >-
+          python -m pytest -ra --cov --cov-report=xml --cov-report=term
+          --durations=20
+
+      - name: Upload coverage report
+        uses: codecov/codecov-action@v3.1.4
diff --git a/.gitignore b/.gitignore
index 0f4949d..2a464b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,166 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+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/
+cover/
+
+# 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/
+*/build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .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
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__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/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# 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/
+
+# Version file
+**/_version.py
+
 .ipynb_checkpoints
 .coverage
 __pycache__
@@ -6,7 +169,6 @@ __pycache__
 .idea
 tmp/
 *.pyc
-build/
 **/.DS_Store
 dist/
 braingeneers/test/*
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..44678c9
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,78 @@
+ci:
+  autoupdate_commit_msg: "chore: update pre-commit hooks"
+  autofix_commit_msg: "style: pre-commit fixes"
+
+repos:
+  - repo: https://github.com/psf/black
+    rev: "23.7.0"
+    hooks:
+      - id: black-jupyter
+
+  - repo: https://github.com/asottile/blacken-docs
+    rev: "1.15.0"
+    hooks:
+      - id: blacken-docs
+        additional_dependencies: [black==23.7.0]
+
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: "v4.4.0"
+    hooks:
+      - id: check-added-large-files
+      - id: check-case-conflict
+      - id: check-merge-conflict
+      - id: check-symlinks
+      - id: check-yaml
+      - id: debug-statements
+      - id: end-of-file-fixer
+      - id: mixed-line-ending
+      - id: name-tests-test
+        args: ["--pytest-test-first"]
+      - id: requirements-txt-fixer
+      - id: trailing-whitespace
+
+  - repo: https://github.com/pre-commit/pygrep-hooks
+    rev: "v1.10.0"
+    hooks:
+      - id: rst-backticks
+      - id: rst-directive-colons
+      - id: rst-inline-touching-normal
+
+  - repo: https://github.com/pre-commit/mirrors-prettier
+    rev: "v3.0.0"
+    hooks:
+      - id: prettier
+        types_or: [yaml, markdown, html, css, scss, javascript, json]
+        args: [--prose-wrap=always]
+
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: "v0.0.277"
+    hooks:
+      - id: ruff
+        args: ["--fix", "--show-fixes"]
+
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: "v1.4.1"
+    hooks:
+      - id: mypy
+        files: src|tests
+        args: []
+        additional_dependencies:
+          - pytest
+
+  - repo: https://github.com/codespell-project/codespell
+    rev: "v2.2.5"
+    hooks:
+      - id: codespell
+
+  - repo: https://github.com/shellcheck-py/shellcheck-py
+    rev: "v0.9.0.5"
+    hooks:
+      - id: shellcheck
+
+  - repo: local
+    hooks:
+      - id: disallow-caps
+        name: Disallow improper capitalization
+        language: pygrep
+        entry: PyBind|Numpy|Cmake|CCache|Github|PyTest
+        exclude: .pre-commit-config.yaml
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..551f036
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,18 @@
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+version: 2
+
+build:
+  os: ubuntu-22.04
+  tools:
+    python: "3.11"
+sphinx:
+  configuration: docs/source/conf.py
+
+python:
+  install:
+    - method: pip
+      path: .
+      extra_requirements:
+        - docs
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..25bc954
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,8 @@
+The MIT License (MIT)
+Copyright (c) 2023, Braingeneers
+
+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
index 3131fc4..583c274 100644
--- a/README.md
+++ b/README.md
@@ -1,94 +1,90 @@
 # Braingeneers Python Utilities
 
-[This package][github] is supposed to collect, as well as make installable
-through Pip, all of the Python code and utilities that we develop as
-part of the Braingeneers project. There are five subpackages:
-  * `braingeneers.analysis` code for data analysis.
+[![ssec](https://img.shields.io/badge/SSEC-Project-purple?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAOCAQAAABedl5ZAAAACXBIWXMAAAHKAAABygHMtnUxAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAMNJREFUGBltwcEqwwEcAOAfc1F2sNsOTqSlNUopSv5jW1YzHHYY/6YtLa1Jy4mbl3Bz8QIeyKM4fMaUxr4vZnEpjWnmLMSYCysxTcddhF25+EvJia5hhCudULAePyRalvUteXIfBgYxJufRuaKuprKsbDjVUrUj40FNQ11PTzEmrCmrevPhRcVQai8m1PRVvOPZgX2JttWYsGhD3atbHWcyUqX4oqDtJkJiJHUYv+R1JbaNHJmP/+Q1HLu2GbNoSm3Ft0+Y1YMdPSTSwQAAAABJRU5ErkJggg==&style=plastic)](https://escience.washington.edu/wetai/)
+[![MIT License](https://badgen.net/badge/license/MIT/blue)](LICENSE)
+[![Documentation Status](https://readthedocs.org/projects/braingeneers/badge/?version=latest)](https://braingeneers.readthedocs.io/en/latest/?badge=latest)
 
-  * `braingeneers.data` all code for basic data access .
-    * `braingeneers.data.datasets_electrophysiology` contains methods which load and manipulate ephys data.
-    * `braingeneers.data.datasets_fluidics` contains methods which load and manipulate fluidics data.
-    * `braingeneers.data.datasets_imaging` contains methods which load and manipulate imaging data.
-    
-  * `braingeneers.iot` all code for IOT (internet of things) communication.
-    * `braingeneers.iot.messaging` a single interface for all messaging and inter-device data transfer functions (MQTT, redis, device state, etc.). A wetAI tutorial on this package exists.
-    
-  * `braingeneers.ml` all code related to ML (machine learning).
-    * `braingeneers.ml.ephys_dataloader` a high performance pytorch data loader for ephys data.
+## Getting Started
 
-  * `braigeneers.utils`  
-    * `braingeneers.utils.s3wrangler` a wrapper of `awswrangler.s3` for accessing PRP/S3. See section below for the documentation and examples.
-    * `braingeneers.utils.smart_open_braingeneers` a wrapper of `smart_open` for opening files on PRP/S3. See section below for the documentation and examples.
+Welcome to the **Braingeneers Python Utilities** repository! This package collects and provides various Python code and utilities developed as part of the Braingeneers project. The package adheres to the Python Package Authority (PyPA) standards for package structure and organization.
 
-[github]: https://www.github.com/braingeneers/braingeneerspy
+## Contribution
 
-## Installation / upgrade
+We welcome contributions from collaborators and researchers interested in our work. If you have improvements, suggestions, or new findings to share, please submit a pull request. Your contributions help advance our research and analysis efforts.
 
-Most dependencies are optional installations for this package. 
-Below are examples of various installation configurations.
+To get started with your development (or fork), click the "Open with GitHub Codespaces" button below to launch a fully configured development environment with all the necessary tools and extensions.
 
+[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/braingeneers/braingeneerspy?quickstart=1)
+
+Instruction on how to contribute to this project can be found in the [CONTRIBUTION.md](https://github.com/braingeneers/braingeneerspy/blob/development/.github/CONTRIBUTING.md).
+
+## Installation
+
+You can install `braingeneerspy` using `pip` with the following commands:
+
+### Install from PyPI (Recommended)
+
+```bash
+pip install braingeneerspy
 ```
-# Typical install (includes `iot`, `analysis`, and `data` access functions, skips `ml`, and lab-specific dependencies): 
-python -m pip install --force-reinstall git+https://github.com/braingeneers/braingeneerspy.git#egg=braingeneerspy[iot,analysis,data]
 
-# Full install (all optional dependencies included).
-python -m pip install --force-reinstall git+https://github.com/braingeneers/braingeneerspy.git#egg=braingeneerspy[all]
+### Install from GitHub
 
-# Minimum install (no optional dependencies, good for Raspberry PI builds).
-python -m pip install --force-reinstall git+https://github.com/braingeneers/braingeneerspy.git
+```bash
+pip install --force-reinstall git+https://github.com/braingeneers/braingeneerspy.git
 ```
 
-### macOS installation note:
-if install fails with ```no matches found: git+https://github.com/braingeneers/braingeneerspy.git#egg=braingeneerspy[all]```
-wrap quotes around the github address like so 
+### Install with Optional Dependencies
+
+You can install `braingeneerspy` with specific optional dependencies based on your needs. Use the following command examples:
+
+- Install with IoT, analysis, and data access functions (skips machine learning and lab-specific dependencies):
 
+```bash
+pip install "braingeneers[iot,analysis,data]"
 ```
-# Typical install (includes `iot`, `analysis`, and `data` access functions, skips `ml`, and lab-specific dependencies): 
-python -m pip install --force-reinstall 'git+https://github.com/braingeneers/braingeneerspy.git#egg=braingeneerspy[iot,analysis]'
 
-# Full install (all optional dependencies included).
-python -m pip install --force-reinstall 'git+https://github.com/braingeneers/braingeneerspy.git#egg=braingeneerspy[all]'
+- Install with all optional dependencies:
+
+```bash
+pip install "braingeneers[all]"
 ```
 
-### Optional dependency organization
+## Committing Changes to the Repo
+
+To make changes and publish them on GitHub, please refer to the [CONTRIBUTING.md](https://github.com/braingeneers/braingeneerspy/blob/development/.github/CONTRIBUTING.md) file for up-to-date guidelines.
+
+## Modules and Subpackages
 
-Dependencies are organized into optional groups of requirements. You can install all dependencies with `all`, 
-or install the minimum dependencies (by not specifying optional groups), 
-or some combination of dependencies you will use. Optional dependency groups are:
+`braingeneerspy` includes several subpackages and modules, each serving a specific purpose within the Braingeneers project:
 
- - *Unspecified*: Minimal packages for data access will be installed.
- - `all`: All optional dependencies will be included.
- - `iot`: IOT dependencies such as AWS, Redis packages will be installed.
- - `analysis`: Dependencies for data analysis routines, plotting tools, math libraries, etc.
- - `ml`: Machine Learning dependencies such as `torch` will be installed.
- - `hengenlab`: Hengenlab data loader specific packages such as `neuraltoolkit` will be installed.
+- `braingeneers.analysis`: Contains code for data analysis.
+- `braingeneers.data`: Provides code for basic data access, including subpackages for handling electrophysiology, fluidics, and imaging data.
+- `braingeneers.iot`: Offers code for Internet of Things (IoT) communication, including a messaging interface.
+- `braingeneers.ml`: Contains code related to machine learning, such as a high-performance PyTorch data loader for electrophysiology data.
+- `braingeneers.utils`: Provides utility functions, including S3 access and smart file opening.
 
-### Committing changes to the repo
+## S3 Access and Configuration
 
-To publish changes made to the `braingeneerspy` package on github, please follow these steps. 
- 1. Update the `version` variable in `setup.py`. 
- 2. To then receive the updated `braingeneerspy` package on your personal computer 
- 3. Run one of the pip install commands listed above.
+### `braingeneers.utils.s3wrangler`
 
-## braingeneers.utils.s3wrangler
-Extends the `awswrangler.s3 package` for Braingeneers/PRP access.
-See API documentation: https://aws-data-wrangler.readthedocs.io/en/2.4.0-docs/api.html#amazon-s3
+This module extends the `awswrangler.s3 package` for Braingeneers/PRP access. For API documentation and usage examples, please visit the [official documentation](https://aws-sdk-pandas.readthedocs.io/en/stable/).
+
+Here's a basic usage example:
 
-Usage examples:
 ```python
 import braingeneers.utils.s3wrangler as wr
 
-# get all UUIDs from s3://braingeneers/ephys/
+# Get all UUIDs from s3://braingeneers/ephys/
 uuids = wr.list_directories('s3://braingeneers/ephys/')
 print(uuids)
 ```
 
-## braingeneers.utils.smart_open_braingeneers
-Configures smart_open for braingeneers use on PRP/S3. When importing this version of `smart_open` 
-braingeneers defaults will be autoconfigured. Note that `smart_open` supports both local and S3 files, 
-so it can be used for all files, not just S3 file access.
+### `braingeneers.utils.smart_open_braingeneers`
+
+This module configures `smart_open` for Braingeneers use on PRP/S3. When importing this version of `smart_open`, Braingeneers defaults will be autoconfigured. Note that `smart_open` supports both local and S3 files, so it can be used for all files, not just S3 file access.
 
-Basic usage example (copy/paste this to test your setup), if it works you will see a helpful bit of advice printed to the screen:
+Here's a basic usage example:
 
 ```python
 import braingeneers.utils.smart_open_braingeneers as smart_open
@@ -97,32 +93,31 @@ with smart_open.open('s3://braingeneersdev/test_file.txt', 'r') as f:
     print(f.read())
 ```
 
-You may also safely replace Python's default `open` function with `smart_open.open`, 
-`smart_open` supports both local and remote files:
+You can also safely replace Python's default `open` function with `smart_open.open`:
 
 ```python
 import braingeneers.utils.smart_open_braingeneers as smart_open
 
 open = smart_open.open
 ```
-### Non-standard S3 endpoints:
 
-`smart_open` and `s3wrangler` are pre-configured by default to the standard braingeneers S3 endpoint,
-no configuration is necessary. If you would like to utilize a different S3 service you can specify a
-new custom `ENDPOINT`, this can be a local path or an endpoint URL for another S3 service (s3wrangler
-only supports S3 services, not local paths, `smart_open` supports local paths).
+## Customizing S3 Endpoints
+
+By default, `smart_open` and `s3wrangler` are pre-configured for the standard Braingeneers S3 endpoint. However, you can specify a custom `ENDPOINT` if you'd like to use a different S3 service. This can be a local path or an endpoint URL for another S3 service (note that `s3wrangler` only supports S3 services, not local paths, while `smart_open` supports local paths).
 
-- Set an environment variable `ENDPOINT` with the new endpoint. Unix based example:`export ENDPOINT="https://s3-west.nrp-nautilus.io"`
-- Call `braingeneers.set_default_endpoint(endpoint: str)` and `braingeneers.get_default_endpoint()`. 
-  These functions will update both `smart_open` and `s3wrangler` (if it's an S3 endpoint, 
-  local path endpoints are ignored by s3wrangler)
+To set a custom endpoint, follow these steps:
 
-When running a job on the PRP you can use the PRP internal S3 endpoint,
-which is faster than the default external endpoint (this will only work on jobs run in the PRP 
-environment). Add the following environment variable to your job YAML file.
-This will set the environment variable ENDPOINT_URL which overrides the
-default external PPR/S3 endpoint, which is used if you don't set this variable.
-Setting this environment variable can also be used to set an endpoint other than the PRP/S3.
+1. Set an environment variable `ENDPOINT` with the new endpoint. For example, on Unix-based systems:
+
+   ```bash
+   export ENDPOINT="https://s3-west.nrp-nautilus.io"
+   ```
+
+2. Call `braingeneers.set_default_endpoint(endpoint: str)` and `braingeneers.get_default_endpoint()`. These functions will update both `smart_open` and `s3wrangler` (if it's an S3 endpoint, local path endpoints are ignored by `s3wrangler`).
+
+### Using the PRP Internal S3 Endpoint
+
+When running a job on the PRP, you can use the PRP internal S3 endpoint, which is faster than the default external endpoint. To do this, add the following environment variable to your job YAML file:
 
 ```yaml
 spec:
@@ -137,5 +132,22 @@ spec:
             value: "http://rook-ceph-rgw-nautiluss3.rook"
 ```
 
-Notes:
-- There were version conflicts between 4.2.0 and 5.1.0 of smart_open. This configuration has been tested to work with 5.1.0.
+Please note that this will only work on jobs run in the PRP environment. Setting the `ENDPOINT` environment variable can also be used to specify an endpoint other than the PRP/S3.
+
+## Documentation
+
+The docs directory has been set up using `sphinx-build -M html docs/source/ docs/build/` to create a base project Documentation structure. You can add inline documentation (NumPy style) to further enrich our project's documentation. To render the documentation locally, navigate to the `docs/build/html` folder in the terminal and run `python3 -m http.server`.
+
+## Working in Codespaces
+
+### Project Structure
+
+- **src/:** This folder contains scripts and notebooks representing completed work by the team.
+
+- **pyproject.toml:** This file follows the guidelines from [PyPA](https://packaging.python.org/tutorials/packaging-projects/) for documenting project setup information.
+
+### Customizing the Devcontainer
+
+The `devcontainer.json` file allows you to customize your Codespace container and VS Code environment using extensions. You can add more extensions to tailor the environment to your specific needs. Explore the VS Code extensions marketplace for additional tools that may enhance your workflow.
+
+For more information about Braingeneers, visit our [website](https://braingeneers.ucsc.edu/).
diff --git a/docs/source/conf.py b/docs/source/conf.py
new file mode 100644
index 0000000..88da678
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import importlib.metadata
+
+project = "braingeneers"
+copyright = "2023, Braingeneers"
+author = "Braingeneers"
+version = release = importlib.metadata.version("braingeneers")
+
+extensions = [
+    "myst_parser",
+    "sphinx.ext.autodoc",
+    "sphinx.ext.intersphinx",
+    "sphinx.ext.mathjax",
+    "sphinx.ext.napoleon",
+    "sphinx_autodoc_typehints",
+    "sphinx_copybutton",
+]
+
+source_suffix = [".rst", ".md"]
+exclude_patterns = [
+    "_build",
+    "**.ipynb_checkpoints",
+    "Thumbs.db",
+    ".DS_Store",
+    ".env",
+    ".venv",
+]
+
+html_theme = "furo"
+
+myst_enable_extensions = [
+    "colon_fence",
+]
+
+intersphinx_mapping = {
+    "python": ("https://docs.python.org/3", None),
+}
+
+nitpick_ignore = [
+    ("py:class", "_io.StringIO"),
+    ("py:class", "_io.BytesIO"),
+]
+
+always_document_param_types = True
diff --git a/docs/source/index.md b/docs/source/index.md
new file mode 100644
index 0000000..5873f0a
--- /dev/null
+++ b/docs/source/index.md
@@ -0,0 +1,17 @@
+# braingeneers
+
+```{toctree}
+:maxdepth: 2
+:hidden:
+
+```
+
+```{include} ../README.md
+:start-after: <!-- SPHINX-START -->
+```
+
+## Indices and tables
+
+- {ref}`genindex`
+- {ref}`modindex`
+- {ref}`search`
diff --git a/noxfile.py b/noxfile.py
new file mode 100644
index 0000000..3532266
--- /dev/null
+++ b/noxfile.py
@@ -0,0 +1,116 @@
+from __future__ import annotations
+
+import argparse
+import shutil
+from pathlib import Path
+
+import nox
+
+DIR = Path(__file__).parent.resolve()
+
+nox.options.sessions = ["lint", "pylint", "tests"]
+
+
+@nox.session
+def lint(session: nox.Session) -> None:
+    """
+    Run the linter.
+    """
+    session.install("pre-commit")
+    session.run("pre-commit", "run", "--all-files", *session.posargs)
+
+
+@nox.session
+def pylint(session: nox.Session) -> None:
+    """
+    Run PyLint.
+    """
+    # This needs to be installed into the package environment, and is slower
+    # than a pre-commit check
+    session.install(".", "pylint")
+    session.run("pylint", "src", *session.posargs)
+
+
+@nox.session
+def tests(session: nox.Session) -> None:
+    """
+    Run the unit and regular tests. Use --cov to activate coverage.
+    """
+    session.install(".[test]")
+    session.run("pytest", *session.posargs)
+
+
+@nox.session
+def docs(session: nox.Session) -> None:
+    """
+    Build the docs. Pass "--serve" to serve.
+    """
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--serve", action="store_true", help="Serve after building")
+    parser.add_argument(
+        "-b", dest="builder", default="html", help="Build target (default: html)"
+    )
+    args, posargs = parser.parse_known_args(session.posargs)
+
+    if args.builder != "html" and args.serve:
+        session.error("Must not specify non-HTML builder with --serve")
+
+    session.install(".[docs]")
+    session.chdir("docs")
+
+    if args.builder == "linkcheck":
+        session.run(
+            "sphinx-build", "-b", "linkcheck", ".", "_build/linkcheck", *posargs
+        )
+        return
+
+    session.run(
+        "sphinx-build",
+        "-n",  # nitpicky mode
+        "-T",  # full tracebacks
+        "-W",  # Warnings as errors
+        "--keep-going",  # See all errors
+        "-b",
+        args.builder,
+        ".",
+        f"_build/{args.builder}",
+        *posargs,
+    )
+
+    if args.serve:
+        session.log("Launching docs at http://localhost:8000/ - use Ctrl-C to quit")
+        session.run("python", "-m", "http.server", "8000", "-d", "_build/html")
+
+
+@nox.session
+def build_api_docs(session: nox.Session) -> None:
+    """
+    Build (regenerate) API docs.
+    """
+
+    session.install("sphinx")
+    session.chdir("docs")
+    session.run(
+        "sphinx-apidoc",
+        "-o",
+        "api/",
+        "--module-first",
+        "--no-toc",
+        "--force",
+        "../src/braingeneers",
+    )
+
+
+@nox.session
+def build(session: nox.Session) -> None:
+    """
+    Build an SDist and wheel.
+    """
+
+    build_p = DIR.joinpath("build")
+    if build_p.exists():
+        shutil.rmtree(build_p)
+
+    session.install("build")
+    session.run("python", "-m", "build")
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..72cf3f9
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,206 @@
+[build-system]
+requires = ["hatchling", "hatch-vcs"]
+build-backend = "hatchling.build"
+
+
+[project]
+name = "braingeneers"
+authors = [
+  { name = "Braingeneers", email = "me@example.com" },
+]
+description = "Braingeneers Python utilities"
+readme = "README.md"
+requires-python = ">=3.10"
+classifiers = [
+  "Development Status :: 3 - Alpha",
+  "Intended Audience :: Science/Research",
+  "Intended Audience :: Developers",
+  "License :: OSI Approved :: BSD License",
+  "Operating System :: OS Independent",
+  "Programming Language :: Python",
+  "Programming Language :: Python :: 3",
+  "Programming Language :: Python :: 3 :: Only",
+  "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
+  "Topic :: Scientific/Engineering",
+  "Typing :: Typed",
+]
+dynamic = ["version"]
+dependencies = [
+  "typing_extensions >=4.6; python_version<'3.11'",
+  'deprecated',
+  'requests',
+  'numpy',
+  'tenacity',
+  'boto3',
+]
+
+[tool.hatch.build.hooks.vcs]
+version-file = "src/braingeneers/_version.py"
+
+[tool.hatch.version.raw-options]
+local_scheme = "no-local-version"
+
+[project.optional-dependencies]
+all = [
+  'braingeneers[data]',
+  'braingeneers[analysis]',
+  'braingeneers[ml]',
+  'braingeneers[iot]',
+  'braingeneers[hengenlab]',
+  'braingeneers[test]',
+  'braingeneers[docs]',
+]
+data = [
+  'h5py',
+  'braingeneers-smart-open==2023.10.6',  # 'smart_open>=5.1.0',  the hash version fixes the bytes from-to range header issue.
+  'awswrangler==3.*',
+  'pandas',
+  'nptyping',
+  'paho-mqtt'
+]
+iot = [
+  'redis',
+  'schedule',
+  'paho-mqtt'
+]
+analysis = [
+  'scipy>=1.10.0',
+  'pandas',
+  'powerlaw',
+  'matplotlib',
+  # Both of these dependencies are required for read_phy_files
+  'awswrangler==3.*',
+  'braingeneers-smart-open==2023.10.6',  # 'smart_open>=5.1.0',  the hash version fixes the bytes from-to range header issue.
+]
+ml = [
+  'torch',
+  'scikit-learn',
+]
+hengenlab = [
+  'neuraltoolkit==0.3.1',  # channel mapping information
+]
+test = [
+  "pytest >=6",
+  "pytest-cov >=3",
+]
+docs = [
+  "sphinx>=4.0",
+  "myst_parser>=0.13",
+  "sphinx_book_theme>=0.1.0",
+  "sphinx_copybutton",
+  "sphinx_autodoc_typehints",
+  "furo",
+]
+
+[project.urls]
+Homepage = "https://github.com/braingeneers/braingeneerspy"
+"Bug Tracker" = "https://github.com/braingeneers/braingeneerspy/issues"
+Discussions = "https://github.com/braingeneers/braingeneerspy/discussions"
+Changelog = "https://github.com/braingeneers/braingeneerspy/releases"
+
+[tool.hatch]
+metadata.allow-direct-references = true
+version.source = "vcs"
+envs.default.dependencies = [
+  "pytest",
+  "pytest-cov",
+]
+
+
+[tool.pytest.ini_options]
+minversion = "6.0"
+addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"]
+xfail_strict = true
+filterwarnings = [
+  "error",
+  "ignore:(ast.Str|Attribute s|ast.NameConstant|ast.Num) is deprecated:DeprecationWarning:_pytest",
+]
+log_cli_level = "INFO"
+testpaths = [
+  "tests",
+]
+
+
+[tool.coverage]
+run.source = ["braingeneers"]
+port.exclude_lines = [
+  'pragma: no cover',
+  '\.\.\.',
+  'if typing.TYPE_CHECKING:',
+]
+
+[tool.mypy]
+files = ["src", "tests"]
+python_version = "3.10"
+warn_unused_configs = true
+strict = true
+show_error_codes = true
+enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
+warn_unreachable = true
+disallow_untyped_defs = false
+disallow_incomplete_defs = false
+
+[[tool.mypy.overrides]]
+module = "braingeneers.*"
+disallow_untyped_defs = true
+disallow_incomplete_defs = true
+
+
+[tool.ruff]
+select = [
+  "E", "F", "W", # flake8
+  "B",           # flake8-bugbear
+  "I",           # isort
+  "ARG",         # flake8-unused-arguments
+  "C4",          # flake8-comprehensions
+  "EM",          # flake8-errmsg
+  "ICN",         # flake8-import-conventions
+  "ISC",         # flake8-implicit-str-concat
+  "G",           # flake8-logging-format
+  "PGH",         # pygrep-hooks
+  "PIE",         # flake8-pie
+  "PL",          # pylint
+  "PT",          # flake8-pytest-style
+  "PTH",         # flake8-use-pathlib
+  "RET",         # flake8-return
+  "RUF",         # Ruff-specific
+  "SIM",         # flake8-simplify
+  "T20",         # flake8-print
+  "UP",          # pyupgrade
+  "YTT",         # flake8-2020
+  "EXE",         # flake8-executable
+  "NPY",         # NumPy specific rules
+  "PD",          # pandas-vet
+]
+extend-ignore = [
+  "PLR",    # Design related pylint codes
+  "E501",   # Line too long
+]
+typing-modules = ["braingeneers._compat.typing"]
+src = ["src"]
+unfixable = [
+  "T20",  # Removes print statements
+  "F841", # Removes unused variables
+]
+exclude = []
+flake8-unused-arguments.ignore-variadic-names = true
+isort.required-imports = ["from __future__ import annotations"]
+
+[tool.ruff.per-file-ignores]
+"tests/**" = ["T20"]
+"noxfile.py" = ["T20"]
+
+
+[tool.pylint]
+py-version = "3.10"
+ignore-paths= ["src/braingeneers/_version.py"]
+reports.output-format = "colorized"
+similarities.ignore-imports = "yes"
+messages_control.disable = [
+  "design",
+  "fixme",
+  "line-too-long",
+  "missing-module-docstring",
+  "wrong-import-position",
+]
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 5dbb782..0000000
--- a/setup.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from setuptools import setup, find_packages
-from os import path
-import itertools
-import braingeneers.utils.configure as config
-
-
-here = path.abspath(path.dirname(__file__))
-with open(path.join(here, 'README.md'), encoding='utf-8') as f:
-    long_description = f.read()
-
-# DEPENDENCIES:
-#   Dependencies are listed in configure.py, they are imported from braingeneers.configure.dependencies
-#   dynamically. Please edit dependencies in configure.py, not here. This was done so that the dependency
-#   list is not duplicated. It is also necessary to allow the dependency list to be validated at runtime.
-setup(
-    name='braingeneerspy',
-    version='0.1.14',
-    python_requires='>=3.10.0',  # needed for ordered dictionaries; also this bug in 3.9 and below https://bugs.python.org/issue42853
-    description='Braingeneers Python utilities',
-    long_description=long_description,
-    long_description_content_type='text/markdown',
-    url='https://github.com/braingeneers/braingeneerspy',
-    author='Braingeneers',
-    # non python files are excluded by default
-    package_data={'braingeneers': ['data/mxw_h5_plugin/**/*']},
-    include_package_data=True,
-    classifiers=[
-        'Development Status :: 3 - Alpha',
-        'Intended Audience :: Braingeneers, collaborators',
-        'Programming Language :: Python :: 3',
-        'License :: MIT',
-    ],
-    packages=find_packages(exclude=()),
-    # Edit these fields using the `dependencies` variable above, no changes here are necessary
-    install_requires=config.DEPENDENCIES['minimal'],
-    extras_require={
-        'all': list(itertools.chain(*config.DEPENDENCIES.values())),              # all dependencies included
-        **{k: v for k, v in config.DEPENDENCIES.items() if k != 'minimal'},    # each dependency group
-    },
-)
diff --git a/braingeneers/__init__.py b/src/braingeneers/__init__.py
similarity index 85%
rename from braingeneers/__init__.py
rename to src/braingeneers/__init__.py
index 4eb6a99..a013382 100644
--- a/braingeneers/__init__.py
+++ b/src/braingeneers/__init__.py
@@ -1,9 +1,20 @@
-import braingeneers
-import braingeneers.utils
-from braingeneers.utils.configure import \
-    set_default_endpoint, get_default_endpoint, skip_unittest_if_offline
+"""
+Copyright (c) 2023 Braingeneers. All rights reserved.
+
+braingeneers: braingeneerspy
+"""
+
+from __future__ import annotations
 import warnings
 
+from . import utils
+from .utils.configure import \
+    set_default_endpoint, get_default_endpoint, skip_unittest_if_offline
+from ._version import version as VERSION  # noqa
+
+__version__ = VERSION
+
+__all__ = ("set_default_endpoint", "get_default_endpoint", "skip_unittest_if_offline", "utils")
 
 # Deprecated imports are allowed for backwards compatibility.
 # This code should be removed in the future. This was added 27apr2022 by David Parks.
diff --git a/braingeneers/analysis/__init__.py b/src/braingeneers/analysis/__init__.py
similarity index 100%
rename from braingeneers/analysis/__init__.py
rename to src/braingeneers/analysis/__init__.py
diff --git a/braingeneers/analysis/analysis.py b/src/braingeneers/analysis/analysis.py
similarity index 100%
rename from braingeneers/analysis/analysis.py
rename to src/braingeneers/analysis/analysis.py
diff --git a/braingeneers/analysis/visualize_maxwell.py b/src/braingeneers/analysis/visualize_maxwell.py
similarity index 100%
rename from braingeneers/analysis/visualize_maxwell.py
rename to src/braingeneers/analysis/visualize_maxwell.py
diff --git a/braingeneers/data/__init__.py b/src/braingeneers/data/__init__.py
similarity index 100%
rename from braingeneers/data/__init__.py
rename to src/braingeneers/data/__init__.py
diff --git a/braingeneers/data/datasets.py b/src/braingeneers/data/datasets.py
similarity index 100%
rename from braingeneers/data/datasets.py
rename to src/braingeneers/data/datasets.py
diff --git a/braingeneers/data/datasets_electrophysiology.py b/src/braingeneers/data/datasets_electrophysiology.py
similarity index 100%
rename from braingeneers/data/datasets_electrophysiology.py
rename to src/braingeneers/data/datasets_electrophysiology.py
diff --git a/braingeneers/data/datasets_fluidics.py b/src/braingeneers/data/datasets_fluidics.py
similarity index 100%
rename from braingeneers/data/datasets_fluidics.py
rename to src/braingeneers/data/datasets_fluidics.py
diff --git a/braingeneers/data/datasets_imaging.py b/src/braingeneers/data/datasets_imaging.py
similarity index 100%
rename from braingeneers/data/datasets_imaging.py
rename to src/braingeneers/data/datasets_imaging.py
diff --git a/braingeneers/data/datasets_neuron.py b/src/braingeneers/data/datasets_neuron.py
similarity index 100%
rename from braingeneers/data/datasets_neuron.py
rename to src/braingeneers/data/datasets_neuron.py
diff --git a/braingeneers/data/mxw_h5_plugin/Linux/libcompression.so b/src/braingeneers/data/mxw_h5_plugin/Linux/libcompression.so
similarity index 100%
rename from braingeneers/data/mxw_h5_plugin/Linux/libcompression.so
rename to src/braingeneers/data/mxw_h5_plugin/Linux/libcompression.so
diff --git a/braingeneers/data/mxw_h5_plugin/Mac_arm64/libcompression.dylib b/src/braingeneers/data/mxw_h5_plugin/Mac_arm64/libcompression.dylib
similarity index 100%
rename from braingeneers/data/mxw_h5_plugin/Mac_arm64/libcompression.dylib
rename to src/braingeneers/data/mxw_h5_plugin/Mac_arm64/libcompression.dylib
diff --git a/braingeneers/data/mxw_h5_plugin/Mac_x86_64/libcompression.dylib b/src/braingeneers/data/mxw_h5_plugin/Mac_x86_64/libcompression.dylib
similarity index 100%
rename from braingeneers/data/mxw_h5_plugin/Mac_x86_64/libcompression.dylib
rename to src/braingeneers/data/mxw_h5_plugin/Mac_x86_64/libcompression.dylib
diff --git a/braingeneers/data/mxw_h5_plugin/Windows/compression.dll b/src/braingeneers/data/mxw_h5_plugin/Windows/compression.dll
similarity index 100%
rename from braingeneers/data/mxw_h5_plugin/Windows/compression.dll
rename to src/braingeneers/data/mxw_h5_plugin/Windows/compression.dll
diff --git a/braingeneers/data/transforms/__init__.py b/src/braingeneers/data/transforms/__init__.py
similarity index 100%
rename from braingeneers/data/transforms/__init__.py
rename to src/braingeneers/data/transforms/__init__.py
diff --git a/braingeneers/iot/__init__.py b/src/braingeneers/iot/__init__.py
similarity index 100%
rename from braingeneers/iot/__init__.py
rename to src/braingeneers/iot/__init__.py
diff --git a/braingeneers/iot/gui.py b/src/braingeneers/iot/gui.py
similarity index 100%
rename from braingeneers/iot/gui.py
rename to src/braingeneers/iot/gui.py
diff --git a/braingeneers/iot/messaging.py b/src/braingeneers/iot/messaging.py
similarity index 100%
rename from braingeneers/iot/messaging.py
rename to src/braingeneers/iot/messaging.py
diff --git a/braingeneers/iot/shadows.py b/src/braingeneers/iot/shadows.py
similarity index 100%
rename from braingeneers/iot/shadows.py
rename to src/braingeneers/iot/shadows.py
diff --git a/braingeneers/iot/shadows_dev_playground.py b/src/braingeneers/iot/shadows_dev_playground.py
similarity index 100%
rename from braingeneers/iot/shadows_dev_playground.py
rename to src/braingeneers/iot/shadows_dev_playground.py
diff --git a/braingeneers/iot/simple.py b/src/braingeneers/iot/simple.py
similarity index 100%
rename from braingeneers/iot/simple.py
rename to src/braingeneers/iot/simple.py
diff --git a/braingeneers/ml/__init__.py b/src/braingeneers/ml/__init__.py
similarity index 100%
rename from braingeneers/ml/__init__.py
rename to src/braingeneers/ml/__init__.py
diff --git a/braingeneers/ml/ephys_dataloader.py b/src/braingeneers/ml/ephys_dataloader.py
similarity index 100%
rename from braingeneers/ml/ephys_dataloader.py
rename to src/braingeneers/ml/ephys_dataloader.py
diff --git a/src/braingeneers/py.typed b/src/braingeneers/py.typed
new file mode 100644
index 0000000..e69de29
diff --git a/braingeneers/utils/__init__.py b/src/braingeneers/utils/__init__.py
similarity index 100%
rename from braingeneers/utils/__init__.py
rename to src/braingeneers/utils/__init__.py
diff --git a/braingeneers/utils/common_utils.py b/src/braingeneers/utils/common_utils.py
similarity index 100%
rename from braingeneers/utils/common_utils.py
rename to src/braingeneers/utils/common_utils.py
diff --git a/braingeneers/utils/configure.py b/src/braingeneers/utils/configure.py
similarity index 100%
rename from braingeneers/utils/configure.py
rename to src/braingeneers/utils/configure.py
diff --git a/braingeneers/utils/memoize_s3.py b/src/braingeneers/utils/memoize_s3.py
similarity index 100%
rename from braingeneers/utils/memoize_s3.py
rename to src/braingeneers/utils/memoize_s3.py
diff --git a/braingeneers/utils/numpy_s3_memmap.py b/src/braingeneers/utils/numpy_s3_memmap.py
similarity index 100%
rename from braingeneers/utils/numpy_s3_memmap.py
rename to src/braingeneers/utils/numpy_s3_memmap.py
diff --git a/braingeneers/utils/s3wrangler/__init__.py b/src/braingeneers/utils/s3wrangler/__init__.py
similarity index 100%
rename from braingeneers/utils/s3wrangler/__init__.py
rename to src/braingeneers/utils/s3wrangler/__init__.py
diff --git a/braingeneers/utils/smart_open_braingeneers/__init__.py b/src/braingeneers/utils/smart_open_braingeneers/__init__.py
similarity index 100%
rename from braingeneers/utils/smart_open_braingeneers/__init__.py
rename to src/braingeneers/utils/smart_open_braingeneers/__init__.py


Test patch:
diff --git a/braingeneers/analysis/analysis_test.py b/src/braingeneers/analysis/analysis_test.py
similarity index 100%
rename from braingeneers/analysis/analysis_test.py
rename to src/braingeneers/analysis/analysis_test.py
diff --git a/braingeneers/data/datasets_electrophysiology_test.py b/src/braingeneers/data/datasets_electrophysiology_test.py
similarity index 100%
rename from braingeneers/data/datasets_electrophysiology_test.py
rename to src/braingeneers/data/datasets_electrophysiology_test.py
diff --git a/braingeneers/iot/messaging_test.py b/src/braingeneers/iot/messaging_test.py
similarity index 100%
rename from braingeneers/iot/messaging_test.py
rename to src/braingeneers/iot/messaging_test.py
diff --git a/braingeneers/utils/configure_test.py b/src/braingeneers/utils/configure_test.py
similarity index 100%
rename from braingeneers/utils/configure_test.py
rename to src/braingeneers/utils/configure_test.py
diff --git a/braingeneers/utils/memoize_s3_test.py b/src/braingeneers/utils/memoize_s3_test.py
similarity index 100%
rename from braingeneers/utils/memoize_s3_test.py
rename to src/braingeneers/utils/memoize_s3_test.py
diff --git a/braingeneers/utils/numpy_s3_memmap_test.py b/src/braingeneers/utils/numpy_s3_memmap_test.py
similarity index 100%
rename from braingeneers/utils/numpy_s3_memmap_test.py
rename to src/braingeneers/utils/numpy_s3_memmap_test.py
diff --git a/braingeneers/utils/s3wrangler/s3wrangler_test.py b/src/braingeneers/utils/s3wrangler/s3wrangler_test.py
similarity index 100%
rename from braingeneers/utils/s3wrangler/s3wrangler_test.py
rename to src/braingeneers/utils/s3wrangler/s3wrangler_test.py
diff --git a/braingeneers/utils/smart_open_braingeneers/smart_open_braingeneers_test.py b/src/braingeneers/utils/smart_open_braingeneers/smart_open_braingeneers_test.py
similarity index 100%
rename from braingeneers/utils/smart_open_braingeneers/smart_open_braingeneers_test.py
rename to src/braingeneers/utils/smart_open_braingeneers/smart_open_braingeneers_test.py
diff --git a/tests/test_package.py b/tests/test_package.py
new file mode 100644
index 0000000..e8a50b8
--- /dev/null
+++ b/tests/test_package.py
@@ -0,0 +1,7 @@
+from __future__ import annotations
+
+import braingeneers as m
+
+
+def test_version():
+    assert m.__version__