diff --git a/.github/workflows/code.yml b/.github/workflows/code.yml index 10fa469..eb906a5 100644 --- a/.github/workflows/code.yml +++ b/.github/workflows/code.yml @@ -136,7 +136,7 @@ jobs: shell: bash -l {0} run: | set -vxeuo pipefail - ipython -c "from instrument.startup import *; RE(make_devices())" + ipython -c "from bits.demo_instrument.startup import *; RE(make_devices())" # https://coveralls-python.readthedocs.io/en/latest/usage/configuration.html#github-actions-support coveralls: diff --git a/.github/workflows/init_repo.sh b/.github/workflows/init_repo.sh index 57bcf62..2f96fc8 100644 --- a/.github/workflows/init_repo.sh +++ b/.github/workflows/init_repo.sh @@ -6,14 +6,18 @@ echo $full_name repo=$(echo $full_name | awk -F '/' '{print $2}') -original_repo="BITS" +# Sanitize the repo name to be a valid Python package name +sanitized_repo=$(echo "$repo" | sed 's/[^a-zA-Z0-9_-]/_/g') +original_repo="BITS" -echo $repo +echo $sanitized_repo echo $original_repo -sed -i "s/$original_repo/$repo/g" README.md +sed -i "s/$original_repo/$sanitized_repo/g" README.md +# Call the create_new_instrument function +python3 -c "from bits.utils import create_new_instrument; create_new_instrument('$sanitized_repo')" rm -rf .github/workflows/init_repo.sh rm -rf .github/workflows/init_repo.yml diff --git a/.github/workflows/init_repo.yml b/.github/workflows/init_repo.yml index ec91497..aba332d 100644 --- a/.github/workflows/init_repo.yml +++ b/.github/workflows/init_repo.yml @@ -11,9 +11,19 @@ jobs: fetch-depth: 0 ref: ${{ github.head_ref }} - - name: Run init_repo.sh - run: | - bash .github/workflows/init_repo.sh ${{ github.repository }} + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + + # - name: Install dependencies + # run: | + # python -m pip install --upgrade pip + # pip install -e . + + # - name: Run init_repo.sh + # run: | + # bash .github/workflows/init_repo.sh ${{ github.repository }} - uses: stefanzweifel/git-auto-commit-action@v5 with: diff --git a/.github/workflows/template-sync.yml b/.github/workflows/template-sync.yml index 25ba77b..73d12ec 100644 --- a/.github/workflows/template-sync.yml +++ b/.github/workflows/template-sync.yml @@ -1,35 +1,35 @@ -name: template_sync - -# see: https://github.com/AndreasAugustin/actions-template-sync - -on: - # cronjob trigger - schedule: - # 12:10 AM GMT on the first day of every month - - cron: "10 0 1 * *" - # manual trigger - workflow_dispatch: - -env: - TEMPLATE_REPO: BCDA-APS/BITS - -jobs: - repo-sync: - if: ${{ github.repository != 'bcda-aps/bits' }} - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - # To use this repository's private action, you must check out the repository - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT - - - name: actions-template-sync - uses: AndreasAugustin/actions-template-sync@v2 - with: - source_repo_path: ${{ env.TEMPLATE_REPO }} - upstream_branch: main +name: template_sync + +# see: https://github.com/AndreasAugustin/actions-template-sync + +on: + # cronjob trigger + schedule: + # 12:10 AM GMT on the first day of every month + - cron: "10 0 1 * *" + # manual trigger + workflow_dispatch: + +env: + TEMPLATE_REPO: BCDA-APS/BITS + +jobs: + repo-sync: + if: ${{ github.repository != 'bcda-aps/bits' }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + # To use this repository's private action, you must check out the repository + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT + + - name: actions-template-sync + uses: AndreasAugustin/actions-template-sync@v2 + with: + source_repo_path: ${{ env.TEMPLATE_REPO }} + upstream_branch: main diff --git a/.github/workflows/template_sync.yml b/.github/workflows/template_sync.yml new file mode 100644 index 0000000..73d12ec --- /dev/null +++ b/.github/workflows/template_sync.yml @@ -0,0 +1,35 @@ +name: template_sync + +# see: https://github.com/AndreasAugustin/actions-template-sync + +on: + # cronjob trigger + schedule: + # 12:10 AM GMT on the first day of every month + - cron: "10 0 1 * *" + # manual trigger + workflow_dispatch: + +env: + TEMPLATE_REPO: BCDA-APS/BITS + +jobs: + repo-sync: + if: ${{ github.repository != 'bcda-aps/bits' }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + # To use this repository's private action, you must check out the repository + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.GH_PAT }} # `GH_PAT` is a secret that contains your PAT + + - name: actions-template-sync + uses: AndreasAugustin/actions-template-sync@v2 + with: + source_repo_path: ${{ env.TEMPLATE_REPO }} + upstream_branch: main diff --git a/.templatesyncignore b/.templatesyncignore new file mode 100644 index 0000000..d8d0928 --- /dev/null +++ b/.templatesyncignore @@ -0,0 +1,6 @@ +.all-contributorsrc +.github/dependabot.yml +README.md +Dockerfile +SECURITY.md +src/new_instrument diff --git a/README.md b/README.md index ac9b3cb..893ccea 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,14 @@ cd BITS Set up the development environment. ```bash -export ENV_NAME=bits +export ENV_NAME=BITS_env + conda create -y -n $ENV_NAME python=3.11 pyepics conda activate $ENV_NAME pip install -e ."[all]" ``` -## IPython console +## IPython console Start To start the bluesky instrument session in a ipython execute the next command in a terminal: @@ -31,23 +32,17 @@ To start the bluesky instrument session in a ipython execute the next command in ipython ``` -Inside the ipython console execute: - -```py -from instrument.startup import * -``` - -## Jupyter notebook - +## Jupyter Notebook Start Start JupyterLab, a Jupyter notebook server, or a notebook, VSCode. -Start the data acquisition: +## Starting the BITS Package ```py from instrument.startup import * +RE(make_devices()) # create all the ophyd-style control devices ``` -## Sim Plan Demo +## Run Sim Plan Demo To run some simulated plans that ensure the installation worked as expected please run the next commands inside an ipython session or a jupyter notebook diff --git a/docs/source/api/configs.rst b/docs/source/api/configs.rst index 7b9d26e..1258fc7 100644 --- a/docs/source/api/configs.rst +++ b/docs/source/api/configs.rst @@ -14,7 +14,7 @@ section. Various constants and terms used to configure the instrument package. -.. literalinclude:: ../../../src/instrument/configs/iconfig.yml +.. literalinclude:: ../../../src/bits/demo_instrument/configs/iconfig.yml :language: yaml :linenos: @@ -27,7 +27,7 @@ Declarations of the ophyd (and ophyd-like) devices and signals used by the instrument package. Configuration is used by the guarneri package to create the objects. -.. literalinclude:: ../../../src/instrument/configs/devices.yml +.. literalinclude:: ../../../src/bits/demo_instrument/configs/devices.yml :language: yaml :linenos: @@ -39,6 +39,6 @@ objects. Declarations of the ophyd (and ophyd-like) devices and signals only available when Bluesky is used at the APS. -.. literalinclude:: ../../../src/instrument/configs/devices_aps_only.yml +.. literalinclude:: ../../../src/bits/demo_instrument/configs/devices_aps_only.yml :language: yaml :linenos: diff --git a/docs/source/conf.py b/docs/source/conf.py index 53baf1b..e0ade6f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,12 +6,12 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -import instrument +import bits -project = "instrument" +project = "BITS" copyright = "2023-2025, APS BCDA" author = "APS BCDA" -version = instrument.__version__ +version = bits.__version__ release = version.split("+")[0] if "+" in version: release += "..." diff --git a/docs/source/demo.ipynb b/docs/source/demo.ipynb index 9102ffb..2e13d27 100644 --- a/docs/source/demo.ipynb +++ b/docs/source/demo.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -65,7 +65,7 @@ } ], "source": [ - "from instrument.startup import * # noqa" + "from bits.demo_instrument.startup import * # noqa" ] }, { diff --git a/docs/source/guides/index.rst b/docs/source/guides/index.rst index 34d6d5e..7846a67 100644 --- a/docs/source/guides/index.rst +++ b/docs/source/guides/index.rst @@ -9,5 +9,5 @@ Guides show how to use certain features of this instrument. :maxdepth: 2 :glob: - *dm* + dm template_sync diff --git a/docs/source/guides/template_sync.rst b/docs/source/guides/template_sync.rst index e074c39..7f4a9dc 100644 --- a/docs/source/guides/template_sync.rst +++ b/docs/source/guides/template_sync.rst @@ -15,7 +15,8 @@ Overview -------- This document describes a method to synchronize the new instrument repo -with the template repo. The method relies on a GitHub workflow +with the template repo. The method relies on a GitHub +`workflow `__ to generate a new pull request whenever the template is updated. The workflow can be run on demand or as a periodic task (default is once a month). The workflow is installed in the new instrument's @@ -25,16 +26,16 @@ When the workflow is run, it compares the new instrument's repo with the template repo. If differences are identified, the workflow creates a new branch in the new instrument's repo and then creates a new pull request to merge that branch with ``main``. Additional configuration is -necessary to grant permission for the workflow to create a branch and PR. +necessary to grant permission for the workflow to create a branch and +PR. -Permission is provided through a GitHub Personal Access Token (PAT). [#]_ -For this purpose, the PAT settings [#settings]_ should allow ``write`` permission +Permission is provided through a GitHub Personal Access Token +(`PAT `__). +For this purpose, the PAT +`settings `__ should allow ``write`` (includes ``read``) permission for ``workflow``. It is possible to limit a PAT to a single GitHub repo (a good idea for this use case). -.. [#] https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens -.. [#settings] https://github.com/settings/tokens - Example instrument repository +++++++++++++++++++++++++++++ diff --git a/pyproject.toml b/pyproject.toml index 91c0623..c25cb8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" copyright = "2014-2025, APS" [project] -name = "instrument" +name = "bits" dynamic = ["version"] description = "Model of a Bluesky Data Acquisition Instrument in console, notebook, & queueserver." authors = [ @@ -71,14 +71,12 @@ doc = [ "sphinx", ] -all = ["instrument[dev,doc]"] +all = ["bits[dev,doc]"] [project.urls] "Homepage" = "https://BCDA-APS.github.io/BITS/" "Bug Tracker" = "https://github.com/BCDA-APS/BITS/issues" -# [project.scripts] -# instrument = "instrument.app:main" [tool.black] line-length = 115 @@ -221,10 +219,10 @@ skip-magic-trailing-comma = false line-ending = "auto" [tool.setuptools] -package-dir = {"instrument" = "src/instrument"} +package-dir = {"bits" = "src/bits"} [tool.setuptools.package-data] "*" = ["*.yml"] [tool.setuptools_scm] -write_to = "src/instrument/_version.py" +write_to = "src/bits/_version.py" diff --git a/src/instrument/__init__.py b/src/bits/__init__.py similarity index 93% rename from src/instrument/__init__.py rename to src/bits/__init__.py index 9ae4231..c53940a 100644 --- a/src/instrument/__init__.py +++ b/src/bits/__init__.py @@ -6,7 +6,7 @@ configure_logging() -__package__ = "instrument" +__package__ = "bits" try: from setuptools_scm import get_version diff --git a/src/instrument/callbacks/__init__.py b/src/bits/callbacks/__init__.py similarity index 100% rename from src/instrument/callbacks/__init__.py rename to src/bits/callbacks/__init__.py diff --git a/src/instrument/callbacks/nexus_data_file_writer.py b/src/bits/callbacks/nexus_data_file_writer.py similarity index 71% rename from src/instrument/callbacks/nexus_data_file_writer.py rename to src/bits/callbacks/nexus_data_file_writer.py index 1f06ede..23e61c8 100644 --- a/src/instrument/callbacks/nexus_data_file_writer.py +++ b/src/bits/callbacks/nexus_data_file_writer.py @@ -14,9 +14,9 @@ import logging -from ..core.run_engine_init import RE -from ..utils.aps_functions import host_on_aps_subnet -from ..utils.config_loaders import iconfig +from bits.core.run_engine_init import RE +from bits.utils.aps_functions import host_on_aps_subnet +from bits.utils.config_loaders import iconfig logger = logging.getLogger(__name__) logger.bsdev(__file__) @@ -48,9 +48,13 @@ def get_sample_title(self): nxwriter = MyNXWriter() # create the callback instance """The NeXus file writer object.""" -if "NEXUS_DATA_FILES" in iconfig: +if iconfig.get("NEXUS_DATA_FILES", {}).get("ENABLE", False): RE.subscribe(nxwriter.receiver) # write data to NeXus files -nxwriter.file_extension = iconfig.get("FILE_EXTENSION", "hdf") -warn_missing = iconfig.get("WARN_MISSING", False) +nxwriter.file_extension = iconfig.get("NEXUS_DATA_FILES", {}).get( + "FILE_EXTENSION", "hdf" +) +print("\n\n\n") +print(nxwriter.file_extension) +warn_missing = iconfig.get("NEXUS_DATA_FILES", {}).get("WARN_MISSING", False) nxwriter.warn_on_missing_content = warn_missing diff --git a/src/instrument/callbacks/spec_data_file_writer.py b/src/bits/callbacks/spec_data_file_writer.py similarity index 91% rename from src/instrument/callbacks/spec_data_file_writer.py rename to src/bits/callbacks/spec_data_file_writer.py index 722023c..99057fb 100644 --- a/src/instrument/callbacks/spec_data_file_writer.py +++ b/src/bits/callbacks/spec_data_file_writer.py @@ -17,15 +17,14 @@ import apstools.callbacks import apstools.utils -from ..core.run_engine_init import RE -from ..utils.config_loaders import iconfig +from bits.core.run_engine_init import RE +from bits.utils.config_loaders import iconfig logger = logging.getLogger(__name__) logger.bsdev(__file__) -DEFAULT_FILE_EXTENSION = "dat" -file_extension = iconfig.get("FILE_EXTENSION", DEFAULT_FILE_EXTENSION) +file_extension = iconfig.get("NEXUS_DATA_FILES", {}).get("FILE_EXTENSION", "dat") def spec_comment(comment, doc=None): @@ -77,7 +76,7 @@ def newSpecFile(title, scan_id=None, RE=None): # make the SPEC file in current working directory (assumes is writable) specwriter.newfile(specwriter.spec_filename) -if "SPEC_DATA_FILES" in iconfig: +if iconfig.get("SPEC_DATA_FILES", {}).get("ENABLE", False): RE.subscribe(specwriter.receiver) # write data to SPEC files logger.info("SPEC data file: %s", specwriter.spec_filename.resolve()) diff --git a/src/instrument/core/__init__.py b/src/bits/core/__init__.py similarity index 100% rename from src/instrument/core/__init__.py rename to src/bits/core/__init__.py diff --git a/src/instrument/core/best_effort_init.py b/src/bits/core/best_effort_init.py similarity index 88% rename from src/instrument/core/best_effort_init.py rename to src/bits/core/best_effort_init.py index 137f955..b4b7f72 100644 --- a/src/instrument/core/best_effort_init.py +++ b/src/bits/core/best_effort_init.py @@ -11,8 +11,8 @@ from bluesky.callbacks.best_effort import BestEffortCallback -from ..utils.config_loaders import iconfig -from ..utils.helper_functions import running_in_queueserver +from bits.utils.config_loaders import iconfig +from bits.utils.helper_functions import running_in_queueserver logger = logging.getLogger(__name__) logger.bsdev(__file__) diff --git a/src/instrument/core/catalog_init.py b/src/bits/core/catalog_init.py similarity index 92% rename from src/instrument/core/catalog_init.py rename to src/bits/core/catalog_init.py index b1d9c42..29bc721 100644 --- a/src/instrument/core/catalog_init.py +++ b/src/bits/core/catalog_init.py @@ -10,7 +10,7 @@ import databroker -from ..utils.config_loaders import iconfig +from bits.utils.config_loaders import iconfig logger = logging.getLogger(__name__) logger.bsdev(__file__) diff --git a/src/instrument/core/run_engine_init.py b/src/bits/core/run_engine_init.py similarity index 79% rename from src/instrument/core/run_engine_init.py rename to src/bits/core/run_engine_init.py index aac5aae..f68bc53 100644 --- a/src/instrument/core/run_engine_init.py +++ b/src/bits/core/run_engine_init.py @@ -12,15 +12,15 @@ import bluesky from bluesky.utils import ProgressBarManager -from ..utils.config_loaders import iconfig -from ..utils.controls_setup import connect_scan_id_pv -from ..utils.controls_setup import set_control_layer -from ..utils.controls_setup import set_timeouts -from ..utils.metadata import MD_PATH -from ..utils.metadata import re_metadata -from ..utils.stored_dict import StoredDict -from .best_effort_init import bec -from .catalog_init import cat +from bits.core.best_effort_init import bec +from bits.core.catalog_init import cat +from bits.utils.config_loaders import iconfig +from bits.utils.controls_setup import connect_scan_id_pv +from bits.utils.controls_setup import set_control_layer +from bits.utils.controls_setup import set_timeouts +from bits.utils.metadata import MD_PATH +from bits.utils.metadata import re_metadata +from bits.utils.stored_dict import StoredDict logger = logging.getLogger(__name__) logger.bsdev(__file__) diff --git a/src/bits/demo_instrument/README.md b/src/bits/demo_instrument/README.md new file mode 100644 index 0000000..fd8960d --- /dev/null +++ b/src/bits/demo_instrument/README.md @@ -0,0 +1 @@ +## Demo Instrument diff --git a/src/instrument/beamline/__init__.py b/src/bits/demo_instrument/__init__.py similarity index 100% rename from src/instrument/beamline/__init__.py rename to src/bits/demo_instrument/__init__.py diff --git a/src/instrument/configs/__init__.py b/src/bits/demo_instrument/configs/__init__.py similarity index 100% rename from src/instrument/configs/__init__.py rename to src/bits/demo_instrument/configs/__init__.py diff --git a/src/instrument/configs/devices.yml b/src/bits/demo_instrument/configs/devices.yml similarity index 96% rename from src/instrument/configs/devices.yml rename to src/bits/demo_instrument/configs/devices.yml index b9c12be..1c89449 100644 --- a/src/instrument/configs/devices.yml +++ b/src/bits/demo_instrument/configs/devices.yml @@ -1,6 +1,6 @@ # Guarneri-style device YAML configuration -instrument.devices.factories.predefined_device: +bits.utils.sim_creator.predefined_device: - {creator: ophyd.sim.motor, name: sim_motor} - {creator: ophyd.sim.noisy_det, name: sim_det} diff --git a/src/instrument/configs/devices_aps_only.yml b/src/bits/demo_instrument/configs/devices_aps_only.yml similarity index 100% rename from src/instrument/configs/devices_aps_only.yml rename to src/bits/demo_instrument/configs/devices_aps_only.yml diff --git a/src/instrument/configs/iconfig.yml b/src/bits/demo_instrument/configs/iconfig.yml similarity index 79% rename from src/instrument/configs/iconfig.yml rename to src/bits/demo_instrument/configs/iconfig.yml index fb50aca..e9d205a 100644 --- a/src/instrument/configs/iconfig.yml +++ b/src/bits/demo_instrument/configs/iconfig.yml @@ -11,7 +11,7 @@ DATABROKER_CATALOG: &databroker_catalog temp ### RunEngine configuration RUN_ENGINE: DEFAULT_METADATA: - beamline_id: instrument + beamline_id: demo_instrument instrument_name: Most Glorious Scientific Instrument proposal_id: commissioning databroker_catalog: *databroker_catalog @@ -27,27 +27,31 @@ RUN_ENGINE: ### The progress bar is nice to see, ### except when it clutters the output in Jupyter notebooks. - ### Default: True + ### Default: False USE_PROGRESS_BAR: false # Command-line tools, such as %wa, %ct, ... -USE_BLUESKY_MAGICS: True +USE_BLUESKY_MAGICS: true ### Best Effort Callback Configurations -### Defaults: all true (except no plots in queueserver) -# BEC: -# BASELINE: false -# HEADING: false -# PLOTS: false -# TABLE: false +### Defaults: all true +### except no plots in queueserver +BEC: + BASELINE: true + HEADING: true + PLOTS: false + TABLE: true ### Support for known output file formats. ### Uncomment to use. If undefined, will not write that type of file. ### Each callback should apply its configuration from here. -# NEXUS_DATA_FILES: -# FILE_EXTENSION: hdf -# WARN_MISSING_CONTENT: true +NEXUS_DATA_FILES: + ENABLE: false + FILE_EXTENSION: hdf + WARN_MISSING_CONTENT: true + SPEC_DATA_FILES: + ENABLE: true FILE_EXTENSION: dat ### APS Data Management @@ -63,8 +67,8 @@ APS_DEVICES_FILE: devices_aps_only.yml OPHYD: ### Control layer for ophyd to communicate with EPICS. ### Default: PyEpics - ### Choices: "PyEpics" or "caproto" - # CONTROL_LAYER: caproto + ### Choices: "PyEpics" or "caproto" # caproto is not yet supported + CONTROL_LAYER: PyEpics ### default timeouts (seconds) TIMEOUTS: @@ -73,4 +77,5 @@ OPHYD: PV_CONNECTION: *TIMEOUT # Control detail of exception traces in IPython (console and notebook). +# Options are: Minimal, Plain, Verbose XMODE_DEBUG_LEVEL: Minimal diff --git a/src/instrument/configs/logging.yml b/src/bits/demo_instrument/configs/logging.yml similarity index 100% rename from src/instrument/configs/logging.yml rename to src/bits/demo_instrument/configs/logging.yml diff --git a/src/instrument/devices/__init__.py b/src/bits/demo_instrument/devices/__init__.py similarity index 100% rename from src/instrument/devices/__init__.py rename to src/bits/demo_instrument/devices/__init__.py diff --git a/src/instrument/plans/__init__.py b/src/bits/demo_instrument/plans/__init__.py similarity index 100% rename from src/instrument/plans/__init__.py rename to src/bits/demo_instrument/plans/__init__.py diff --git a/src/instrument/plans/dm_plans.py b/src/bits/demo_instrument/plans/dm_plans.py similarity index 100% rename from src/instrument/plans/dm_plans.py rename to src/bits/demo_instrument/plans/dm_plans.py diff --git a/src/instrument/plans/sim_plans.py b/src/bits/demo_instrument/plans/sim_plans.py similarity index 97% rename from src/instrument/plans/sim_plans.py rename to src/bits/demo_instrument/plans/sim_plans.py index 424269d..50796c1 100644 --- a/src/instrument/plans/sim_plans.py +++ b/src/bits/demo_instrument/plans/sim_plans.py @@ -15,7 +15,7 @@ from bluesky import plan_stubs as bps from bluesky import plans as bp -from ..utils.controls_setup import oregistry +from bits.utils.controls_setup import oregistry logger = logging.getLogger(__name__) logger.bsdev(__file__) diff --git a/src/instrument/startup.py b/src/bits/demo_instrument/startup.py similarity index 50% rename from src/instrument/startup.py rename to src/bits/demo_instrument/startup.py index 5c59b62..f79de5b 100644 --- a/src/instrument/startup.py +++ b/src/bits/demo_instrument/startup.py @@ -11,19 +11,20 @@ import logging -from .core.best_effort_init import bec # noqa: F401 -from .core.best_effort_init import peaks # noqa: F401 -from .core.catalog_init import cat # noqa: F401 -from .core.run_engine_init import RE # noqa: F401 -from .core.run_engine_init import sd # noqa: F401 -from .devices import * # noqa: F403 -from .plans import * # noqa: F403 +from bits.core.best_effort_init import bec # noqa: F401 +from bits.core.best_effort_init import peaks # noqa: F401 +from bits.core.catalog_init import cat # noqa: F401 +from bits.core.run_engine_init import RE # noqa: F401 +from bits.core.run_engine_init import sd # noqa: F401 # Bluesky data acquisition setup -from .utils.config_loaders import iconfig -from .utils.helper_functions import register_bluesky_magics -from .utils.helper_functions import running_in_queueserver -from .utils.make_devices_yaml import make_devices # noqa: F401 +from bits.utils.config_loaders import iconfig +from bits.utils.helper_functions import register_bluesky_magics +from bits.utils.helper_functions import running_in_queueserver +from bits.utils.make_devices_yaml import make_devices # noqa: F401 + +# User specific imports +from .plans import * # noqa: F403 logger = logging.getLogger(__name__) logger.bsdev(__file__) @@ -32,13 +33,13 @@ register_bluesky_magics() # Configure the session with callbacks, devices, and plans. -if iconfig.get("NEXUS_DATA_FILES") is not None: - from .callbacks.nexus_data_file_writer import nxwriter # noqa: F401 +if iconfig.get("NEXUS_DATA_FILES", {}).get("ENABLE", False): + from bits.callbacks.nexus_data_file_writer import nxwriter # noqa: F401 -if iconfig.get("SPEC_DATA_FILES") is not None: - from .callbacks.spec_data_file_writer import newSpecFile # noqa: F401 - from .callbacks.spec_data_file_writer import spec_comment # noqa: F401 - from .callbacks.spec_data_file_writer import specwriter # noqa: F401 +if iconfig.get("SPEC_DATA_FILES", {}).get("ENABLE", False): + from bits.callbacks.spec_data_file_writer import newSpecFile # noqa: F401 + from bits.callbacks.spec_data_file_writer import spec_comment # noqa: F401 + from bits.callbacks.spec_data_file_writer import specwriter # noqa: F401 # These imports must come after the above setup. if running_in_queueserver(): @@ -55,4 +56,4 @@ from bluesky import plan_stubs as bps # noqa: F401 from bluesky import plans as bp # noqa: F401 - from .utils.controls_setup import oregistry # noqa: F401 + from bits.utils.controls_setup import oregistry # noqa: F401 diff --git a/src/instrument/tests/__init__.py b/src/bits/tests/__init__.py similarity index 100% rename from src/instrument/tests/__init__.py rename to src/bits/tests/__init__.py diff --git a/src/instrument/tests/conftest.py b/src/bits/tests/conftest.py similarity index 88% rename from src/instrument/tests/conftest.py rename to src/bits/tests/conftest.py index db19437..7dc5a5c 100644 --- a/src/instrument/tests/conftest.py +++ b/src/bits/tests/conftest.py @@ -13,8 +13,8 @@ import pytest -from ..startup import RE -from ..startup import make_devices +from bits.demo_instrument.startup import RE +from bits.demo_instrument.startup import make_devices @pytest.fixture(scope="session") diff --git a/src/instrument/tests/test_device_factories.py b/src/bits/tests/test_device_factories.py similarity index 93% rename from src/instrument/tests/test_device_factories.py rename to src/bits/tests/test_device_factories.py index 39588a3..a350297 100644 --- a/src/instrument/tests/test_device_factories.py +++ b/src/bits/tests/test_device_factories.py @@ -2,8 +2,8 @@ import pytest -from ..devices.factories import motors -from ..devices.factories import predefined_device +from bits.utils.sim_creator import motors +from bits.utils.sim_creator import predefined_device @pytest.mark.parametrize( diff --git a/src/instrument/tests/test_general.py b/src/bits/tests/test_general.py similarity index 74% rename from src/instrument/tests/test_general.py rename to src/bits/tests/test_general.py index c808c3c..590c178 100644 --- a/src/instrument/tests/test_general.py +++ b/src/bits/tests/test_general.py @@ -6,16 +6,16 @@ import pytest -from ..plans.sim_plans import sim_count_plan -from ..plans.sim_plans import sim_print_plan -from ..plans.sim_plans import sim_rel_scan_plan -from ..startup import bec -from ..startup import cat -from ..startup import iconfig -from ..startup import peaks -from ..startup import running_in_queueserver -from ..startup import sd -from ..startup import specwriter +from bits.demo_instrument.plans.sim_plans import sim_count_plan +from bits.demo_instrument.plans.sim_plans import sim_print_plan +from bits.demo_instrument.plans.sim_plans import sim_rel_scan_plan +from bits.demo_instrument.startup import bec +from bits.demo_instrument.startup import cat +from bits.demo_instrument.startup import iconfig +from bits.demo_instrument.startup import peaks +from bits.demo_instrument.startup import running_in_queueserver +from bits.demo_instrument.startup import sd +from bits.demo_instrument.startup import specwriter def test_startup(runengine_with_devices: object) -> None: @@ -60,7 +60,9 @@ def test_iconfig() -> None: """ Test the instrument configuration. """ - version: str = iconfig.get("ICONFIG_VERSION", "0.0.0") + version: str = iconfig.get( + "ICONFIG_VERSION", "0.0.0" + ) # TODO: Will anyone ever have a wrong catalog version? assert version >= "2.0.0" cat_name: str = iconfig.get("DATABROKER_CATALOG") diff --git a/src/instrument/tests/test_stored_dict.py b/src/bits/tests/test_stored_dict.py similarity index 96% rename from src/instrument/tests/test_stored_dict.py rename to src/bits/tests/test_stored_dict.py index 9972240..c52b8c6 100644 --- a/src/instrument/tests/test_stored_dict.py +++ b/src/bits/tests/test_stored_dict.py @@ -9,8 +9,8 @@ import pytest -from ..utils.config_loaders import load_config_yaml -from ..utils.stored_dict import StoredDict +from bits.utils.config_loaders import load_config_yaml +from bits.utils.stored_dict import StoredDict def luftpause(delay=0.05): @@ -76,6 +76,7 @@ def test_StoredDict(md_file): # Add another key. sdict["bee"] = "bumble" sdict.flush() + print(f"\n\nthis is the md_file: {md_file}\n\n") luftpause() assert len(open(md_file).read().splitlines()) == 5 diff --git a/src/instrument/utils/__init__.py b/src/bits/utils/__init__.py similarity index 100% rename from src/instrument/utils/__init__.py rename to src/bits/utils/__init__.py diff --git a/src/instrument/utils/aps_functions.py b/src/bits/utils/aps_functions.py similarity index 100% rename from src/instrument/utils/aps_functions.py rename to src/bits/utils/aps_functions.py diff --git a/src/instrument/utils/config_loaders.py b/src/bits/utils/config_loaders.py similarity index 90% rename from src/instrument/utils/config_loaders.py rename to src/bits/utils/config_loaders.py index 0322211..60ac78d 100644 --- a/src/instrument/utils/config_loaders.py +++ b/src/bits/utils/config_loaders.py @@ -17,7 +17,9 @@ logger = logging.getLogger(__name__) logger.bsdev(__file__) instrument_path = pathlib.Path(__file__).parent.parent -DEFAULT_ICONFIG_YML_FILE = instrument_path / "configs" / "iconfig.yml" +DEFAULT_ICONFIG_YML_FILE = ( + instrument_path / "demo_instrument" / "configs" / "iconfig.yml" +) ICONFIG_MINIMUM_VERSION = "2.0.0" @@ -30,7 +32,7 @@ def load_config_yaml(iconfig_yml=None) -> dict: iconfig_yml: str Name of the YAML file to be loaded. The name can be absolute or relative to the current working directory. - Default: ``INSTRUMENT/configs/iconfig.yml`` + Default: ``INSTRUMENT/demo_instrument/configs/iconfig.yml`` """ if iconfig_yml is None: diff --git a/src/instrument/utils/controls_setup.py b/src/bits/utils/controls_setup.py similarity index 100% rename from src/instrument/utils/controls_setup.py rename to src/bits/utils/controls_setup.py diff --git a/src/bits/utils/create_new_instrument.py b/src/bits/utils/create_new_instrument.py new file mode 100644 index 0000000..37f7867 --- /dev/null +++ b/src/bits/utils/create_new_instrument.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +""" +Script to create a new instrument based on the 'src/bits/demo_instrument' template +folder. + +This script copies the template instrument folder into a new directory with the +provided instrument name (which must be a valid Python package name) and updates +the pyproject.toml file with an entry under [tool.instruments] reflecting the +new instrument's relative path. + +The script includes a version flag and can be executed directly without needing to +install the bits package. +""" + +__version__ = "1.0.0" + +import argparse +import logging +import re +import shutil +import sys +from pathlib import Path +from typing import Any + +try: + import toml +except ImportError: + print( + "The 'toml' package is required to run this script. " + "Please install it via 'pip install toml'." + ) + sys.exit(1) + + +def copy_instrument(template_dir: Path, destination_dir: Path) -> None: + """ + Copy the template instrument folder to a new destination. + + Args: + template_dir (Path): Path to the template instrument directory. + destination_dir (Path): Path to the new instrument directory. + + Raises: + Exception: Propagates any exception raised during copying. + """ + shutil.copytree(str(template_dir), str(destination_dir)) + + +def update_pyproject( + pyproject_path: Path, instrument_name: str, instrument_path: Path +) -> None: + """ + Update the pyproject.toml file by adding a new instrument entry. + + If the [tool.instruments] section does not exist, it will be created. + + Args: + pyproject_path (Path): Path to the pyproject.toml file. + instrument_name (str): The name of the new instrument. + instrument_path (Path): The path to the new instrument directory. + """ + with pyproject_path.open("r", encoding="utf-8") as file: + config: dict[str, Any] = toml.load(file) + + if "tool" not in config or not isinstance(config["tool"], dict): + config["tool"] = {} + + if "instruments" not in config["tool"] or not isinstance( + config["tool"]["instruments"], dict + ): + config["tool"]["instruments"] = {} + + # Store the instrument path relative to the pyproject.toml location. + relative_path: str = str( + instrument_path.resolve().relative_to(pyproject_path.parent.resolve()) + ) + config["tool"]["instruments"][instrument_name] = {"path": relative_path} + + with pyproject_path.open("w", encoding="utf-8") as file: + toml.dump(config, file) + + +def main() -> None: + """ + Main function to create a new instrument based on a template and update + pyproject.toml. + + This function parses command-line arguments (including a --version flag) to ensure + the script + can be run standalone without needing to install the entire bits package. + Validates that the new instrument name is a valid Python package name (lowercase + letters, + digits, and underscores, starting with a letter). + """ + parser = argparse.ArgumentParser( + description="Create a new instrument from the 'src/bits/demo_instrument' " + "template." + ) + parser.add_argument( + "name", + type=str, + help="Name of the new instrument (this will be used as the new directory " + "name and must be a valid package name).", + ) + parser.add_argument( + "--template", + type=str, + default="src/bits/demo_instrument", + help="Path to the template instrument directory " + "(default: src/bits/demo_instrument).", + ) + parser.add_argument( + "--dest", + type=str, + default=".", + help="Destination directory where the new instrument folder will be created " + "(default: current directory).", + ) + parser.add_argument( + "--version", + action="version", + version=f"%(prog)s {__version__}", + help="Show the version of this script and exit.", + ) + args = parser.parse_args() + + # Validate that the instrument name is a valid Python package name. + if re.fullmatch(r"[a-z][_a-z0-9]*", args.name) is None: + logging.error( + "Invalid instrument name '%s'. Please use a valid Python package name " + "(lowercase letters, digits, and underscores, starting with a letter).", + args.name, + ) + sys.exit(1) + + template_path: Path = Path(args.template).resolve() + destination_parent: Path = Path(args.dest).resolve() + new_instrument_dir: Path = destination_parent / args.name + + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)s: %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + logging.info( + "Creating new instrument '%s' from template '%s' to destination '%s'.", + args.name, + template_path, + new_instrument_dir, + ) + + if not template_path.exists(): + logging.error("Template directory '%s' does not exist.", template_path) + sys.exit(1) + + if new_instrument_dir.exists(): + logging.error("Destination directory '%s' already exists.", new_instrument_dir) + sys.exit(1) + + try: + copy_instrument(template_path, new_instrument_dir) + logging.info("Copied template to '%s'.", new_instrument_dir) + except Exception as exc: + logging.error("Error copying instrument: %s", exc) + sys.exit(1) + + pyproject_path: Path = Path("pyproject.toml").resolve() + if not pyproject_path.exists(): + logging.error("pyproject.toml not found in the current directory!") + sys.exit(1) + + try: + update_pyproject(pyproject_path, args.name, new_instrument_dir) + logging.info("Updated pyproject.toml with new instrument '%s'.", args.name) + except Exception as exc: + logging.error("Error updating pyproject.toml: %s", exc) + sys.exit(1) + + logging.info("Instrument '%s' created successfully.", args.name) + + +if __name__ == "__main__": + main() diff --git a/src/instrument/utils/helper_functions.py b/src/bits/utils/helper_functions.py similarity index 100% rename from src/instrument/utils/helper_functions.py rename to src/bits/utils/helper_functions.py diff --git a/src/instrument/utils/logging_setup.py b/src/bits/utils/logging_setup.py similarity index 98% rename from src/instrument/utils/logging_setup.py rename to src/bits/utils/logging_setup.py index 22f7f4b..16ce9ab 100644 --- a/src/instrument/utils/logging_setup.py +++ b/src/bits/utils/logging_setup.py @@ -26,7 +26,9 @@ BRIEF_DATE = "%a-%H:%M:%S" BRIEF_FORMAT = "%(levelname)-.1s %(asctime)s.%(msecs)03d: %(message)s" -DEFAULT_CONFIG_FILE = pathlib.Path(__file__).parent.parent / "configs" / "logging.yml" +DEFAULT_CONFIG_FILE = ( + pathlib.Path(__file__).parent.parent / "demo_instrument" / "configs" / "logging.yml" +) # Add your custom logging level at the top-level, before configure_logging() diff --git a/src/instrument/utils/make_devices_yaml.py b/src/bits/utils/make_devices_yaml.py similarity index 91% rename from src/instrument/utils/make_devices_yaml.py rename to src/bits/utils/make_devices_yaml.py index ab2f649..57c734f 100644 --- a/src/instrument/utils/make_devices_yaml.py +++ b/src/bits/utils/make_devices_yaml.py @@ -21,16 +21,16 @@ from apstools.utils import dynamic_import from bluesky import plan_stubs as bps -from .aps_functions import host_on_aps_subnet -from .config_loaders import iconfig -from .config_loaders import load_config_yaml -from .controls_setup import oregistry # noqa: F401 +from bits.utils.aps_functions import host_on_aps_subnet +from bits.utils.config_loaders import iconfig +from bits.utils.config_loaders import load_config_yaml +from bits.utils.controls_setup import oregistry # noqa: F401 logger = logging.getLogger(__name__) logger.bsdev(__file__) -configs_path = pathlib.Path(__file__).parent.parent / "configs" +configs_path = pathlib.Path(__file__).parent.parent / "demo_instrument" / "configs" main_namespace = sys.modules["__main__"] local_control_devices_file = iconfig["DEVICES_FILE"] aps_control_devices_file = iconfig["APS_DEVICES_FILE"] diff --git a/src/instrument/utils/metadata.py b/src/bits/utils/metadata.py similarity index 96% rename from src/instrument/utils/metadata.py rename to src/bits/utils/metadata.py index 3f743eb..64305c5 100644 --- a/src/instrument/utils/metadata.py +++ b/src/bits/utils/metadata.py @@ -28,7 +28,8 @@ import pysumreg import spec2nexus -from .config_loaders import iconfig +import bits +from bits.utils.config_loaders import iconfig logger = logging.getLogger(__name__) logger.bsdev(__file__) @@ -52,6 +53,7 @@ python=sys.version.split(" ")[0], pysumreg=pysumreg.__version__, spec2nexus=spec2nexus.__version__, + bits=bits.__version__, ) RE_CONFIG = iconfig.get("RUN_ENGINE", {}) diff --git a/src/instrument/devices/factories.py b/src/bits/utils/sim_creator.py similarity index 100% rename from src/instrument/devices/factories.py rename to src/bits/utils/sim_creator.py diff --git a/src/instrument/utils/stored_dict.py b/src/bits/utils/stored_dict.py similarity index 100% rename from src/instrument/utils/stored_dict.py rename to src/bits/utils/stored_dict.py