Feat/release workflow #114
@@ -0,0 +1,46 @@
|
||||
name: "Install"
|
||||
description: "Setup system and python environment and install repo"
|
||||
inputs:
|
||||
python_version:
|
||||
required: false
|
||||
default: "3.12"
|
||||
description: "Python version to use"
|
||||
index_username:
|
||||
required: false
|
||||
default: ""
|
||||
description: "Username for the private psi package index"
|
||||
index_password:
|
||||
required: false
|
||||
default: ""
|
||||
description: "Token/password for the private psi package index"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Libraries
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libgl1 \
|
||||
libegl1 \
|
||||
libxkbcommon-x11-0 \
|
||||
libdbus-1-3 \
|
||||
libxcb-cursor0 \
|
||||
libx11-xcb1
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ inputs.python_version }}
|
||||
|
||||
- name: Install repo
|
||||
shell: bash
|
||||
env:
|
||||
UV_INDEX_PSI_USERNAME: ${{ inputs.index_username }}
|
||||
UV_INDEX_PSI_PASSWORD: ${{ inputs.index_password }}
|
||||
run: |
|
||||
pip install uv
|
||||
uv venv
|
||||
source .venv/bin/activate
|
||||
uv pip install -e .[test]
|
||||
@@ -1,223 +0,0 @@
|
||||
name: Build and Publish
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
branches:
|
||||
- master
|
||||
- deploy
|
||||
- 'feature/**'
|
||||
- gitea-pages
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- deploy
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
||||
env:
|
||||
DEPLOY_BRANCH: gitea-pages
|
||||
PREVIEWS_DIR: previews
|
||||
PYTHONPATH: src
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-test-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-test-
|
||||
|
||||
- name: Create virtual environment
|
||||
run: |
|
||||
python -m venv venv
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
PIP_INDEX_URL: https://${{ secrets.GITEA_USER }}:${{ secrets.GITEA_TOKEN }}@gitea.psi.ch/api/packages/mx/pypi/simple/
|
||||
PIP_EXTRA_INDEX_URL: https://pypi.org/simple
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
pip install pytest pytest-cov pytest-mock pytest-qt pytest-asyncio
|
||||
|
||||
pip install -e .
|
||||
|
||||
- name: Install system dependencies for PySide6
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libgl1 \
|
||||
libegl1 \
|
||||
libxkbcommon-x11-0 \
|
||||
libdbus-1-3 \
|
||||
libxcb-cursor0 \
|
||||
libx11-xcb1
|
||||
|
||||
- name: Debug Python env
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
which python
|
||||
which pytest
|
||||
python -m pip list | grep pytest
|
||||
|
||||
- name: Run tests with pytest
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
BEAMLINE: SIMULATED
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
|
||||
python -m pytest --cov=aare --cov-report=xml tests/unit
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }}
|
||||
|
||||
- name: Set Up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Set Up Python Environment
|
||||
run: |
|
||||
python3 -m venv venv # Create a virtual environment
|
||||
source venv/bin/activate
|
||||
pip install twine build
|
||||
|
||||
- name: Build the package
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python -m build
|
||||
|
||||
- name: Upload Package to Gitea PyPI
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
TWINE_USERNAME: "__token__" # Username for Twine when using token-based auth
|
||||
TWINE_PASSWORD: ${{ secrets.PIP_REPOSITORY_API_TOKEN }} # Use the secret for authentication
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
twine upload --repository-url https://gitea.psi.ch/api/packages/mx/pypi \
|
||||
dist/*
|
||||
|
||||
docs:
|
||||
name: Build and Deploy Docs
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build, test]
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # needed for pushing branches/tags
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Build Sphinx HTML
|
||||
working-directory: docs
|
||||
run: |
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
pip install -e .. --no-deps
|
||||
|
||||
sphinx-apidoc -o modules/ ../src/aare --separate --module-first --force --remove-old
|
||||
|
||||
sphinx-build -b html . _build/html
|
||||
|
||||
- name: Deploy to gitea-pages branch (Gitea)
|
||||
env:
|
||||
GIT_USER: "Martin Appleby (Gitea)"
|
||||
GIT_EMAIL: "martin.appleby@psi.ch"
|
||||
DEPLOY_TOKEN: ${{ secrets.DOCS_DEPLOY_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
git config --global user.name "$GIT_USER"
|
||||
git config --global user.email "$GIT_EMAIL"
|
||||
|
||||
# Deploy strategy: create/update branch gitea-pages and replace its contents
|
||||
# with the generated daq/docs/_build/html. This keeps only the documentation
|
||||
# on the gitea-pages branch.
|
||||
# Confirm build output exists (relative to repository root)
|
||||
BUILD_DIR="docs/_build/html"
|
||||
echo "Checking build output at: $BUILD_DIR"
|
||||
|
||||
if [ ! -d "$BUILD_DIR" ] || [ -z "$(ls -A "$BUILD_DIR")" ]; then
|
||||
echo "ERROR: $BUILD_DIR does not exist or is empty"
|
||||
echo "Listing docs/:"
|
||||
ls -la docs || true
|
||||
echo "Listing docs/_build:"
|
||||
ls -la docs/_build || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Work in a temporary clone to avoid touching the runner workspace
|
||||
TMPDIR=$(mktemp -d)
|
||||
REPO_URL="https://$DEPLOY_TOKEN@gitea.psi.ch/${{ github.repository }}"
|
||||
git clone "$REPO_URL" "$TMPDIR"
|
||||
cd "$TMPDIR"
|
||||
|
||||
# Create or switch to orphan branch gitea-pages
|
||||
if git rev-parse --verify --quiet gitea-pages >/dev/null; then
|
||||
git checkout gitea-pages
|
||||
# Make sure working tree matches branch (hard reset)
|
||||
git fetch --prune origin gitea-pages || true
|
||||
git reset --hard origin/gitea-pages || git reset --hard
|
||||
else
|
||||
git checkout --orphan gitea-pages
|
||||
fi
|
||||
# Remove all files tracked in the branch (leave .git)
|
||||
git rm -rf . || true
|
||||
# Also remove untracked files to ensure clean branch state
|
||||
git clean -fdx || true
|
||||
|
||||
# Copy built HTML into the clone root
|
||||
echo "Copying built HTML into branch root..."
|
||||
(cd "${GITHUB_WORKSPACE}/${BUILD_DIR}" && tar -cf - .) | tar -xf - -C "$TMPDIR"
|
||||
|
||||
# Ensure index.html exists in destination as sanity check
|
||||
if [ ! -f "$TMPDIR/index.html" ] && [ ! -f "$TMPDIR/index.htm" ]; then
|
||||
# If site root index is not present, try to check subfolder
|
||||
echo "Warning: no index.html found at branch root after copy. Listing files:"
|
||||
ls -la "$TMPDIR" || true
|
||||
fi
|
||||
|
||||
git add --all
|
||||
if git diff --quiet --cached; then
|
||||
echo "No changes to deploy"
|
||||
exit 0
|
||||
fi
|
||||
COMMIT_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
|
||||
git commit -m "docs: (${COMMIT_DATE}) | auto-update documentation from ${GITHUB_SHA}"
|
||||
git push --force-with-lease origin gitea-pages
|
||||
@@ -0,0 +1,51 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup
|
||||
uses: ./.gitea/actions/install
|
||||
with:
|
||||
python_version: "3.12"
|
||||
index_username: ${{ secrets.GITEA_USER }}
|
||||
index_password: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Format
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
ruff format --check
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: "lint"
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup
|
||||
uses: ./.gitea/actions/install
|
||||
with:
|
||||
python_version: ${{ matrix.python-version }}
|
||||
index_username: ${{ secrets.GITEA_USER }}
|
||||
index_password: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Run Pytest with Coverage
|
||||
env:
|
||||
QT_QPA_PLATFORM: offscreen
|
||||
BEAMLINE: SIMULATED
|
||||
run: |
|
||||
source .venv/bin/activate
|
||||
pytest --cov=aare --cov-config=./pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail ./tests/unit
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build and Publish
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-release-${{ github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
outputs:
|
||||
release_made: ${{ steps.release_step.outputs.release_made }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Setup | Checkout Repository on Release Branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Setup | Force release branch to be at workflow sha
|
||||
run: |
|
||||
git reset --hard ${{ github.sha }}
|
||||
- name: Evaluate | Verify upstream has NOT changed
|
||||
shell: bash
|
||||
run: |
|
||||
set +o pipefail
|
||||
|
||||
UPSTREAM_BRANCH_NAME="$(git status -sb | head -n 1 | cut -d' ' -f2 | grep -E '\.{3}' | cut -d'.' -f4)"
|
||||
printf '%s\n' "Upstream branch name: $UPSTREAM_BRANCH_NAME"
|
||||
|
||||
set -o pipefail
|
||||
|
||||
if [ -z "$UPSTREAM_BRANCH_NAME" ]; then
|
||||
printf >&2 '%s\n' "::error::Unable to determine upstream branch name!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch "${UPSTREAM_BRANCH_NAME%%/*}"
|
||||
|
||||
if ! UPSTREAM_SHA="$(git rev-parse "$UPSTREAM_BRANCH_NAME")"; then
|
||||
printf >&2 '%s\n' "::error::Unable to determine upstream branch sha!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
HEAD_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
if [ "$HEAD_SHA" != "$UPSTREAM_SHA" ]; then
|
||||
printf >&2 '%s\n' "[HEAD SHA] $HEAD_SHA != $UPSTREAM_SHA [UPSTREAM SHA]"
|
||||
printf >&2 '%s\n' "::error::Upstream has changed, aborting release..."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "Verified upstream branch has not changed, continuing with release..."
|
||||
|
||||
- name: Semantic Version Release
|
||||
id: release_step
|
||||
env:
|
||||
TWINE_USERNAME: "__token__"
|
||||
TWINE_PASSWORD: ${{ secrets.MX_GITEA_BOT_TOKEN }}
|
||||
MX_GITEA_BOT_TOKEN: ${{ secrets.MX_GITEA_BOT_TOKEN }}
|
||||
run: |
|
||||
pip install python-semantic-release==9.* wheel build twine
|
||||
semantic-release --config ./ci/semantic_release.toml version
|
||||
if [ ! -d dist ]; then echo No release will be made; echo "release_made=false" >> "$GITHUB_OUTPUT"; exit 0; fi
|
||||
echo "release_made=true" >> "$GITHUB_OUTPUT"
|
||||
twine upload dist/* --repository-url https://gitea.psi.ch/api/packages/mx/pypi
|
||||
semantic-release publish
|
||||
@@ -1,4 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
## Hand-written changelog is now superseded by the generated one in CHANGELOG.md; preserved for posterity.
|
||||
|
||||
## 0.3.2 (unreleased)
|
||||
- **Authentication & Sessions**:
|
||||
- Migrated DAQ server URLs to HTTPS with per-beamline configurable certificate paths (cacert) and mTLS; GUI now validates certificates and handles self-signed certs / SSL errors in `DAQWorker` and `SSEClient`.
|
||||
@@ -0,0 +1,35 @@
|
||||
[tool.semantic_release]
|
||||
build_command = "python -m build"
|
||||
version_toml = [
|
||||
"./pyproject.toml:project.version",
|
||||
]
|
||||
|
||||
[tool.semantic_release.commit_author]
|
||||
env = "GIT_COMMIT_AUTHOR"
|
||||
default = "semantic-release <semantic-release>"
|
||||
|
||||
[tool.semantic_release.commit_parser_options]
|
||||
allowed_tags = [
|
||||
"build",
|
||||
"chore",
|
||||
"ci",
|
||||
"docs",
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"style",
|
||||
"refactor",
|
||||
"test",
|
||||
]
|
||||
minor_tags = ["feat"]
|
||||
patch_tags = ["fix", "perf"]
|
||||
default_bump_level = 0
|
||||
|
||||
[tool.semantic_release.remote]
|
||||
type = "gitea"
|
||||
domain = "gitea.psi.ch"
|
||||
token = { env = "MX_GITEA_BOT_TOKEN" }
|
||||
|
||||
[tool.semantic_release.publish]
|
||||
dist_glob_patterns = ["dist/*"]
|
||||
upload_to_vcs_release = true
|
||||
+2
-1
@@ -41,7 +41,8 @@ test = [
|
||||
"pytest-mock==3.14.0",
|
||||
"pytest-qt==4.4.0",
|
||||
"pytest-asyncio==0.25.3",
|
||||
"pytest-timeout"
|
||||
"pytest-timeout",
|
||||
"ruff==0.15.*"
|
||||
]
|
||||
|
||||
[lint]
|
||||
|
||||
@@ -20,7 +20,7 @@ def decode_bulk(value: Any):
|
||||
|
||||
|
||||
def fetch_key(r: redis.Redis, key: bytes) -> Tuple[str, Any]:
|
||||
key_s = decode_bulk(key)
|
||||
decode_bulk(key)
|
||||
t = r.type(key)
|
||||
if isinstance(t, bytes):
|
||||
t = t.decode()
|
||||
|
||||
@@ -229,7 +229,7 @@ class AareWrapper:
|
||||
experiment_params_payload = ExperimentParametersCreate(
|
||||
type="standard", beamline_parameters=beamline_params, sample_id=s.db_id
|
||||
)
|
||||
response = self.__sample_api.create_experiment_parameters_for_sample(
|
||||
self.__sample_api.create_experiment_parameters_for_sample(
|
||||
sample_id=s.db_id, experiment_parameters_create=experiment_params_payload
|
||||
)
|
||||
# logger.debug("Experiment parameters created:", response)
|
||||
@@ -291,7 +291,7 @@ class AareWrapper:
|
||||
experiment_params_payload = ExperimentParametersCreate(
|
||||
type="standard", beamline_parameters=beamline_params, sample_id=s.db_id
|
||||
)
|
||||
response = self.__sample_api.create_experiment_parameters_for_sample(
|
||||
self.__sample_api.create_experiment_parameters_for_sample(
|
||||
sample_id=s.db_id, experiment_parameters_create=experiment_params_payload
|
||||
)
|
||||
# logger.info("Experiment parameters created:", response)
|
||||
|
||||
@@ -42,11 +42,8 @@ from aarecommon.models.models import (
|
||||
)
|
||||
from aare.daq.config_model import LocalContactConfigModel
|
||||
|
||||
#TODO WHAT SHOULD THIS BE? This should be in the YAMl file it is beamline specific
|
||||
ABR_POS_MOUNT = AerotechCoordinate(
|
||||
at_mm=Coordinate(x=0, y=0, z=0),
|
||||
omega_deg=0
|
||||
)
|
||||
# TODO WHAT SHOULD THIS BE? This should be in the YAMl file it is beamline specific
|
||||
ABR_POS_MOUNT = AerotechCoordinate(at_mm=Coordinate(x=0, y=0, z=0), omega_deg=0)
|
||||
|
||||
|
||||
# TODO WHAT SHOULD THIS BE? This should be in the YAMl file it is beamline specific
|
||||
@@ -1261,7 +1258,9 @@ class BeamlineConfig:
|
||||
logger.warning(f"Failed to read Local Contact config from Redis: {e}")
|
||||
return default
|
||||
|
||||
def set_local_contact_config(self, config: LocalContactConfigModel | dict) -> LocalContactConfigModel:
|
||||
def set_local_contact_config(
|
||||
self, config: LocalContactConfigModel | dict
|
||||
) -> LocalContactConfigModel:
|
||||
validated = LocalContactConfigModel.model_validate(config)
|
||||
try:
|
||||
redis_key = f"{self.__bl}:local_contact_config"
|
||||
|
||||
+33
-23
@@ -1385,8 +1385,9 @@ class AareDAQ:
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _execute_rotation_sequence(self, rotation_request: RotationScanRequest) -> CompletedRotationScan | None:
|
||||
def _execute_rotation_sequence(
|
||||
self, rotation_request: RotationScanRequest
|
||||
) -> CompletedRotationScan | None:
|
||||
"""
|
||||
Execute rotation scan.
|
||||
|
||||
@@ -1574,19 +1575,21 @@ class AareDAQ:
|
||||
self.last_time = end - start
|
||||
|
||||
def _expand_macros(self, name: str) -> str:
|
||||
name = name.replace('{date}', datetime.now().strftime('%Y%m%d'))
|
||||
name = name.replace('{sample}', self.sample.sample_name)
|
||||
name = name.replace('{CrystalName}', self.sample.sample_name)
|
||||
name = name.replace('{puck}', self.sample.puck_name)
|
||||
name = name.replace('{position}', f"{self.sample.pin:02d}")
|
||||
name = name.replace('{sample_id}', f"{self.sample.db_id}")
|
||||
name = name.replace('{beamline}', f"{self._beamline.value.lower()}")
|
||||
name = name.replace('{prefix}', f"{self.sample.puck_name}/{self.sample.pin:02d}/{self.sample.sample_name}")
|
||||
#TODO work out why clean_filename is removing slahses
|
||||
#name = clean_filename(name)
|
||||
name = name.replace("{date}", datetime.now().strftime("%Y%m%d"))
|
||||
name = name.replace("{sample}", self.sample.sample_name)
|
||||
name = name.replace("{CrystalName}", self.sample.sample_name)
|
||||
name = name.replace("{puck}", self.sample.puck_name)
|
||||
name = name.replace("{position}", f"{self.sample.pin:02d}")
|
||||
name = name.replace("{sample_id}", f"{self.sample.db_id}")
|
||||
name = name.replace("{beamline}", f"{self._beamline.value.lower()}")
|
||||
name = name.replace(
|
||||
"{prefix}", f"{self.sample.puck_name}/{self.sample.pin:02d}/{self.sample.sample_name}"
|
||||
)
|
||||
# TODO work out why clean_filename is removing slahses
|
||||
# name = clean_filename(name)
|
||||
return name
|
||||
|
||||
def spreadsheet_params(self) -> tuple[Optional[SimpleScanParameters], str|None]:
|
||||
def spreadsheet_params(self) -> tuple[Optional[SimpleScanParameters], str | None]:
|
||||
file_prefix = None
|
||||
|
||||
if self.status.sample is None:
|
||||
@@ -1979,7 +1982,7 @@ class AareDAQ:
|
||||
self.save_screenshot_db(sample_id, screenshot_name)
|
||||
|
||||
if request.transmission is not None and request.transmission != self.__devs.transmission:
|
||||
logger.info(f'requesting transmission to move to {request.transmission}')
|
||||
logger.info(f"requesting transmission to move to {request.transmission}")
|
||||
self.__devs.transmission = request.transmission
|
||||
|
||||
start_pos = getattr(request, "start", None)
|
||||
@@ -2700,7 +2703,9 @@ class AareDAQ:
|
||||
|
||||
if raster_params.filename is not None:
|
||||
logger.info(f"Using filename {raster_params.filename}")
|
||||
sample_prefix = "{filename}/{prefix}".format(filename=raster_params.filename, prefix=sample.sample_name)
|
||||
sample_prefix = "{filename}/{prefix}".format(
|
||||
filename=raster_params.filename, prefix=sample.sample_name
|
||||
)
|
||||
|
||||
raster_grid = RasterGridRequest(
|
||||
exp_time_s=raster_params.exp_time_s,
|
||||
@@ -2767,17 +2772,20 @@ class AareDAQ:
|
||||
|
||||
update_sample = self.sample
|
||||
if (
|
||||
update_sample is not None
|
||||
and update_sample.db_id is not None
|
||||
and update_sample.db_id == sample.db_id
|
||||
update_sample is not None
|
||||
and update_sample.db_id is not None
|
||||
and update_sample.db_id == sample.db_id
|
||||
):
|
||||
logger.info(f"Updating sample info {sample.db_id} old run_number"
|
||||
f" sample.run_number {sample.run_number} new run_number {update_sample.run_number}")
|
||||
|
||||
logger.info(
|
||||
f"Updating sample info {sample.db_id} old run_number"
|
||||
f" sample.run_number {sample.run_number} new run_number {update_sample.run_number}"
|
||||
)
|
||||
|
||||
if params.filename is not None:
|
||||
logger.info(f"Using filename {params.filename}")
|
||||
sample_prefix = "{filename}/{prefix}".format(filename=params.filename, prefix=sample.sample_name)
|
||||
sample_prefix = "{filename}/{prefix}".format(
|
||||
filename=params.filename, prefix=sample.sample_name
|
||||
)
|
||||
|
||||
rotation_request = RotationScanRequest(
|
||||
start_omega_deg=start_omega,
|
||||
@@ -3492,7 +3500,9 @@ class AareDAQ:
|
||||
finally:
|
||||
self.__cfg.state_busy = False
|
||||
|
||||
def fluorimeter_take_spectrum(self, fm: FluorescenceSpectrumParameterModel) -> FluorescenceSpectrumOutputModel:
|
||||
def fluorimeter_take_spectrum(
|
||||
self, fm: FluorescenceSpectrumParameterModel
|
||||
) -> FluorescenceSpectrumOutputModel:
|
||||
self.__cfg.try_set_busy(timeout=360)
|
||||
|
||||
try:
|
||||
|
||||
@@ -3,7 +3,6 @@ from dataclasses import dataclass
|
||||
from math import ceil, floor
|
||||
from typing import Callable
|
||||
|
||||
import cv2
|
||||
from aarecommon.config.beamline import cfg_get
|
||||
from aarecommon.config.logger_events import (
|
||||
geom_log_context,
|
||||
@@ -112,7 +111,6 @@ def get_ml_bounding_box(
|
||||
preferred_class=(3, 0), return_image=True, return_bundle_meta=True
|
||||
)
|
||||
m = prediction_result.box
|
||||
bundle_image = prediction_result.image
|
||||
|
||||
log_ml_bundle_meta(
|
||||
logger,
|
||||
|
||||
@@ -53,7 +53,6 @@ class FaceDetectionService:
|
||||
beam_y = geom.beam_location_pxl.y
|
||||
beam_x = geom.beam_location_pxl.x
|
||||
|
||||
x1 = model.box.top_x
|
||||
y1 = model.box.top_y
|
||||
y2 = model.box.bottom_y
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import hmac
|
||||
import importlib
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
@@ -9,7 +8,6 @@ import time
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator, Optional
|
||||
|
||||
import cv2
|
||||
import uvicorn
|
||||
from aarecommon.config.beamline import mx_beamline
|
||||
from aarecommon.config.logger import get_uvicorn_logging_config, setup_logger
|
||||
|
||||
@@ -256,7 +256,7 @@ class JFJochWrapper:
|
||||
|
||||
def detector(self) -> jfjoch_client.models.DetectorListElement:
|
||||
try:
|
||||
l = self.__api.config_select_detector_get()
|
||||
detector_list = self.__api.config_select_detector_get()
|
||||
except Exception as e:
|
||||
self._raise_jfjoch_error(
|
||||
f"JFJoch detector configuration retrieval failed: {e}",
|
||||
@@ -265,7 +265,7 @@ class JFJochWrapper:
|
||||
endpoint="config_select_detector_get",
|
||||
)
|
||||
|
||||
if len(l.detectors) == 0:
|
||||
if len(detector_list.detectors) == 0:
|
||||
raise JFJochCommunicationError(
|
||||
"JFJoch returned no configured detectors",
|
||||
operation="GET",
|
||||
@@ -275,7 +275,7 @@ class JFJochWrapper:
|
||||
)
|
||||
|
||||
try:
|
||||
return l.detectors[l.current_id]
|
||||
return detector_list.detectors[detector_list.current_id]
|
||||
except Exception as e:
|
||||
self._raise_jfjoch_error(
|
||||
"JFJoch returned an invalid selected detector entry",
|
||||
|
||||
@@ -220,7 +220,6 @@ class Smargon(object):
|
||||
sleep(poll_time)
|
||||
|
||||
def wait_aerotech(self, timeout=60.0, tol=0.01, poll_time=0.01):
|
||||
target = self.target
|
||||
timeout = timeout + time()
|
||||
while time() < timeout:
|
||||
if self.target_aerotech.eq(self.readback_aerotech, tol):
|
||||
|
||||
@@ -120,11 +120,7 @@ class PShellTellBackend:
|
||||
if e.response is not None and e.response.text:
|
||||
msg = e.response.text.strip()
|
||||
raise TellCommunicationError(
|
||||
msg,
|
||||
base_url=self._url,
|
||||
endpoint=endpoint,
|
||||
operation=operation,
|
||||
critical=True,
|
||||
msg, base_url=self._url, endpoint=endpoint, operation=operation, critical=True
|
||||
) from e
|
||||
|
||||
def eval(self, expr: str):
|
||||
|
||||
@@ -359,7 +359,9 @@ class TellClient:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Exception occurred: {e}")
|
||||
raise TellCommunicationError(message=f"Error during mount {segment}{puck}-{sample}: {e}", critical=True)
|
||||
raise TellCommunicationError(
|
||||
message=f"Error during mount {segment}{puck}-{sample}: {e}", critical=True
|
||||
)
|
||||
|
||||
def unmount(self, force=False, wait=False, timeout=360.0):
|
||||
if self.is_busy():
|
||||
@@ -582,7 +584,7 @@ if __name__ == "__main__":
|
||||
state = tell_client.get_robot_state()
|
||||
manual_mode = tell_client.is_manual_mode()
|
||||
print("state: ", state)
|
||||
print("is manual mode True: ", manual_mode == True)
|
||||
print("is manual mode True: ", manual_mode)
|
||||
print(tell_client.backend.eval("is_manual_mode()&"))
|
||||
print(tell_client.is_door_closed())
|
||||
# print("release safety: ", tell_client.backend.eval('release_safety()&'))
|
||||
|
||||
@@ -14,11 +14,17 @@ def wait_position(motor, target, tolerance=None, timeout=60.0):
|
||||
elif isinstance(position, (bytes, str)):
|
||||
tst = "'%s' == '%s'"
|
||||
if isinstance(position, bytes):
|
||||
ltst = lambda x, y, z: (
|
||||
x.lower() == y.lower().encode() if isinstance(y, str) else x.lower() == y.lower()
|
||||
)
|
||||
|
||||
def ltst(x, y, z):
|
||||
return (
|
||||
x.lower() == y.lower().encode()
|
||||
if isinstance(y, str)
|
||||
else x.lower() == y.lower()
|
||||
)
|
||||
else:
|
||||
ltst = lambda x, y, z: x.lower() == y.lower()
|
||||
|
||||
def ltst(x, y, z):
|
||||
return x.lower() == y.lower()
|
||||
else:
|
||||
raise RuntimeError("BUG: could not understand arguments: ")
|
||||
|
||||
|
||||
+17
-21
@@ -1,4 +1,3 @@
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
@@ -20,24 +19,24 @@ logger = setup_logger(LOGGER_NAME)
|
||||
|
||||
def main():
|
||||
"""Wrapped gui as main function to make tests easier"""
|
||||
splash = None
|
||||
try:
|
||||
basedir = os.path.dirname(__file__)
|
||||
icon_path = os.path.join(basedir, "graphics/aaregui_logo.svg")
|
||||
banner_path = os.path.join(basedir, "graphics/aare_banner.png")
|
||||
splash_pix = QtGui.QPixmap(banner_path)
|
||||
splash = LoadingSplashScreen(splash_pix)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load resources for splash screen: {e}")
|
||||
sys.exit(1)
|
||||
try:
|
||||
# define application
|
||||
app = QApplication(sys.argv)
|
||||
app.setWindowIcon(QtGui.QIcon(icon_path))
|
||||
app.setApplicationName("AareGUI")
|
||||
app.setApplicationVersion("0.3.1")
|
||||
app.setOrganizationName("PSI")
|
||||
app.setOrganizationDomain("psi.ch")
|
||||
|
||||
# set icon
|
||||
basedir = os.path.dirname(__file__)
|
||||
icon_path = os.path.join(basedir, "graphics/aaregui_logo.svg")
|
||||
app.setWindowIcon(QtGui.QIcon(icon_path))
|
||||
|
||||
# show splash screen
|
||||
banner_path = os.path.join(basedir, "graphics/aare_banner.png")
|
||||
splash_pix = QtGui.QPixmap(banner_path)
|
||||
splash = LoadingSplashScreen(splash_pix)
|
||||
splash.show()
|
||||
splash.set_progress(10, "Initializing Application...")
|
||||
|
||||
@@ -200,16 +199,13 @@ def main():
|
||||
logger.error(f"Error starting GUI: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
|
||||
try:
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"Fatal Error",
|
||||
f"An error occurred during startup. See console for details."
|
||||
f"\nPlease check the server is running and your network connection."
|
||||
f"\n\n{str(e)}\n\n",
|
||||
)
|
||||
except:
|
||||
pass
|
||||
QMessageBox.critical(
|
||||
None,
|
||||
"Fatal Error",
|
||||
f"An error occurred during startup. See console for details."
|
||||
f"\nPlease check the server is running and your network connection."
|
||||
f"\n\n{str(e)}\n\n",
|
||||
)
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@@ -1852,7 +1852,7 @@ class MainWindow(QMainWindow):
|
||||
logger.critical(f"Automation critical failure: {message}")
|
||||
|
||||
is_detector_failure = self._is_detector_critical_failure(message)
|
||||
banner_message = (
|
||||
(
|
||||
self._detector_error_banner_text(automation=True, message=message)
|
||||
if is_detector_failure
|
||||
else f"Automation halted: {message}"
|
||||
@@ -2493,6 +2493,6 @@ class MainWindow(QMainWindow):
|
||||
"Idle timeout reached, but GUI remains open because beamline is busy or automation is active."
|
||||
)
|
||||
return
|
||||
#TODO deny communication from GUI to DAQ while IDLE for too long rather than kill the GUI
|
||||
# TODO deny communication from GUI to DAQ while IDLE for too long rather than kill the GUI
|
||||
logger.warning("Closing GUI after inactivity timeout.")
|
||||
self.close()
|
||||
|
||||
@@ -90,9 +90,9 @@ class SampleQueueSpreadsheet(QAbstractTableModel):
|
||||
row = parent.row()
|
||||
|
||||
try:
|
||||
l = SampleShortInfoList.model_validate_json(data.text())
|
||||
sample_data = SampleShortInfoList.model_validate_json(data.text())
|
||||
self.beginResetModel()
|
||||
for sample in l.s:
|
||||
for sample in sample_data.s:
|
||||
updated_row = row
|
||||
updated_samples = []
|
||||
for i in range(len(self.samples)):
|
||||
|
||||
@@ -180,12 +180,12 @@ class UserSampleSpreadsheet(QAbstractTableModel):
|
||||
def mimeData(self, indexes):
|
||||
mime_data = QMimeData()
|
||||
|
||||
l = SampleShortInfoList(s=[])
|
||||
sample_data = SampleShortInfoList(s=[])
|
||||
|
||||
for i in sorted(set(index.row() for index in indexes)):
|
||||
l.s.append(self.__sorted_samples[i])
|
||||
sample_data.s.append(self.__sorted_samples[i])
|
||||
|
||||
mime_data.setText(l.model_dump_json())
|
||||
mime_data.setText(sample_data.model_dump_json())
|
||||
return mime_data
|
||||
|
||||
def get_id(self, row: int) -> SampleShortInfo:
|
||||
|
||||
@@ -78,7 +78,7 @@ class FaceDetectionPanel(QWidget):
|
||||
samples = data.get("samples", []) or []
|
||||
running = bool(data.get("running", False))
|
||||
angle = data.get("current_angle_deg")
|
||||
status = data.get("status", "")
|
||||
data.get("status", "")
|
||||
|
||||
if running:
|
||||
if self._manual_run_requested:
|
||||
|
||||
@@ -86,14 +86,16 @@ class FilePathPanel(QWidget):
|
||||
|
||||
def _expand_macros(self, base: str, rn: int) -> str:
|
||||
name = f"{base}_{rn:03d}"
|
||||
name = name.replace('{date}', self.__formatted_date)
|
||||
name = name.replace('{sample}', self.__sample_name)
|
||||
name = name.replace('{CrystalName}', self.__sample_name)
|
||||
name = name.replace('{puck}', self.__puck_name)
|
||||
name = name.replace('{position}', f"{self.__puck_pos:02d}")
|
||||
name = name.replace('{sample_id}', f"{self.__sample_id}")
|
||||
name = name.replace('{beamline}', f"{self.__beamline}")
|
||||
name = name.replace('{prefix}', f"{self.__puck_name}/{self.__puck_pos:02d}/{self.__sample_name}")
|
||||
name = name.replace("{date}", self.__formatted_date)
|
||||
name = name.replace("{sample}", self.__sample_name)
|
||||
name = name.replace("{CrystalName}", self.__sample_name)
|
||||
name = name.replace("{puck}", self.__puck_name)
|
||||
name = name.replace("{position}", f"{self.__puck_pos:02d}")
|
||||
name = name.replace("{sample_id}", f"{self.__sample_id}")
|
||||
name = name.replace("{beamline}", f"{self.__beamline}")
|
||||
name = name.replace(
|
||||
"{prefix}", f"{self.__puck_name}/{self.__puck_pos:02d}/{self.__sample_name}"
|
||||
)
|
||||
return name
|
||||
|
||||
def _effective_dataset_base(self, base_no_run: str) -> str:
|
||||
|
||||
@@ -22,7 +22,7 @@ from PySide6.QtWidgets import (
|
||||
QTabWidget,
|
||||
QTextEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
from aare.gui.constants import LOGGER_NAME
|
||||
@@ -333,11 +333,22 @@ class LocalContactPanel(QFrame):
|
||||
tools_layout.addWidget(tools)
|
||||
|
||||
tools_layout.addWidget(
|
||||
self._make_button("Load BEC user macros", self._daq.bec_load_user_macros, "Loading BEC user macros."))
|
||||
self._make_button(
|
||||
"Load BEC user macros", self._daq.bec_load_user_macros, "Loading BEC user macros."
|
||||
)
|
||||
)
|
||||
tools_layout.addWidget(
|
||||
self._make_button("Show BEC user macros", self._daq.bec_list_all_user_macros, "Listing BEC user macros."))
|
||||
self._make_button(
|
||||
"Show BEC user macros",
|
||||
self._daq.bec_list_all_user_macros,
|
||||
"Listing BEC user macros.",
|
||||
)
|
||||
)
|
||||
tools_layout.addWidget(
|
||||
self._make_button("Show BEC position devices", self._daq.bec_list_all_devices, "Listing BEC devices."))
|
||||
self._make_button(
|
||||
"Show BEC position devices", self._daq.bec_list_all_devices, "Listing BEC devices."
|
||||
)
|
||||
)
|
||||
tools_layout.addWidget(
|
||||
self._make_button(
|
||||
"Reinitialise BEC planner/devices",
|
||||
|
||||
@@ -31,15 +31,14 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
raster_alpha_changed = Signal(int)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
raster_mgr: RasterGridManager,
|
||||
diffraction: DiffractionGeometry,
|
||||
parent=None,
|
||||
self, raster_mgr: RasterGridManager, diffraction: DiffractionGeometry, parent=None
|
||||
):
|
||||
super().__init__(diffraction=diffraction,
|
||||
default_dtz=raster_mgr.active_grid.dtz,
|
||||
default_transmission=raster_mgr.active_grid.transmission,
|
||||
parent=parent)
|
||||
super().__init__(
|
||||
diffraction=diffraction,
|
||||
default_dtz=raster_mgr.active_grid.dtz,
|
||||
default_transmission=raster_mgr.active_grid.transmission,
|
||||
parent=parent,
|
||||
)
|
||||
|
||||
self._previous_sample_was_none_raster = True
|
||||
|
||||
|
||||
@@ -39,8 +39,13 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
super().__init__(
|
||||
parent=parent,
|
||||
diffraction=diffraction,
|
||||
default_dtz=cfg_get("daq.data_collection_settings.default_rotation_settings.dtz", default_dtz),
|
||||
default_transmission=cfg_get("daq.data_collection_settings.default_rotation_settings.transmission", default_transmission),
|
||||
default_dtz=cfg_get(
|
||||
"daq.data_collection_settings.default_rotation_settings.dtz", default_dtz
|
||||
),
|
||||
default_transmission=cfg_get(
|
||||
"daq.data_collection_settings.default_rotation_settings.transmission",
|
||||
default_transmission,
|
||||
),
|
||||
)
|
||||
|
||||
self.__beamline_state = None
|
||||
@@ -71,8 +76,12 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
self._layout.addWidget(QLabel("°", parent=self), 5, 4)
|
||||
|
||||
self._layout.addWidget(QLabel("Image time", parent=self), 6, 0)
|
||||
default_screening_exp_time = cfg_get("daq.data_collection_settings.default_screening_settings.exp_time_s", 0.1)
|
||||
self.screening_image_time_enter = NumberLineEdit(0.0005, 10.0, default_screening_exp_time, decimals=4, parent=self)
|
||||
default_screening_exp_time = cfg_get(
|
||||
"daq.data_collection_settings.default_screening_settings.exp_time_s", 0.1
|
||||
)
|
||||
self.screening_image_time_enter = NumberLineEdit(
|
||||
0.0005, 10.0, default_screening_exp_time, decimals=4, parent=self
|
||||
)
|
||||
self._layout.addWidget(self.screening_image_time_enter, 6, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("s", parent=self), 6, 4)
|
||||
|
||||
@@ -96,22 +105,32 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
)
|
||||
|
||||
self._layout.addWidget(QLabel("Total angle", parent=self), 10, 0)
|
||||
default_steps = cfg_get("daq.data_collection_settings.default_rotation_settings.steps", 1800)
|
||||
default_increment_omega = cfg_get("daq.data_collection_settings.default_rotation_settings.increment_omega_deg", 0.2)
|
||||
default_total_angle = default_steps*default_increment_omega
|
||||
self.total_angle = DbOverrideLineEdit(0, 9999.0, default=default_total_angle, decimals=3, parent=self)
|
||||
default_steps = cfg_get(
|
||||
"daq.data_collection_settings.default_rotation_settings.steps", 1800
|
||||
)
|
||||
default_increment_omega = cfg_get(
|
||||
"daq.data_collection_settings.default_rotation_settings.increment_omega_deg", 0.2
|
||||
)
|
||||
default_total_angle = default_steps * default_increment_omega
|
||||
self.total_angle = DbOverrideLineEdit(
|
||||
0, 9999.0, default=default_total_angle, decimals=3, parent=self
|
||||
)
|
||||
self._layout.addWidget(self.total_angle, 10, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 10, 4)
|
||||
self._register_override_field(self.total_angle)
|
||||
|
||||
self._layout.addWidget(QLabel("Image angle", parent=self), 11, 0)
|
||||
self.image_angle = DbOverrideLineEdit(0, 10.0, default=default_increment_omega, decimals=3, parent=self)
|
||||
self.image_angle = DbOverrideLineEdit(
|
||||
0, 10.0, default=default_increment_omega, decimals=3, parent=self
|
||||
)
|
||||
self._layout.addWidget(self.image_angle, 11, 1, 1, 3)
|
||||
self._layout.addWidget(QLabel("°", parent=self), 11, 4)
|
||||
self._register_override_field(self.image_angle)
|
||||
# TODO add protection on X10SA to prevent too short exposure time/ too high detector rep rate
|
||||
self._layout.addWidget(QLabel("Image time", parent=self), 12, 0)
|
||||
default_image_exp_time = cfg_get("daq.data_collection_settings.default_rotation_settings.exp_time_s", 0.01)
|
||||
default_image_exp_time = cfg_get(
|
||||
"daq.data_collection_settings.default_rotation_settings.exp_time_s", 0.01
|
||||
)
|
||||
self.image_time_enter = DbOverrideLineEdit(
|
||||
0.0005, 10.0, default=default_image_exp_time, decimals=4, parent=self
|
||||
)
|
||||
@@ -145,7 +164,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
|
||||
self.measurement_button.clicked.connect(self.run_measurement)
|
||||
self._layout.addWidget(self.measurement_button, 16, 0, 1, 6)
|
||||
self._reset_to_defaults()
|
||||
|
||||
|
||||
@Slot()
|
||||
def run_screening(self):
|
||||
if self.__beamline_state != BeamlineStateEnum.SampleAlignment:
|
||||
|
||||
@@ -196,7 +196,7 @@ class ScanSettingsPanel(QWidget):
|
||||
else:
|
||||
if not self._previous_sample_was_none:
|
||||
logger.info("Sample cleared, resetting parameters to defaults")
|
||||
#self._reset_to_defaults()
|
||||
# self._reset_to_defaults()
|
||||
self._previous_sample_was_none = True # Mark that sample is now None
|
||||
|
||||
self._sample = None
|
||||
|
||||
@@ -55,7 +55,7 @@ class InteractiveChartView(QChartView):
|
||||
def mousePressEvent(self, event: QMouseEvent) -> None:
|
||||
if event.button() == Qt.MouseButton.LeftButton:
|
||||
click_pos = event.position()
|
||||
chart_pos = self.chart().mapToValue(click_pos.toPoint())
|
||||
self.chart().mapToValue(click_pos.toPoint())
|
||||
|
||||
if self._panel._try_select_series_at_point(click_pos.toPoint()):
|
||||
event.accept()
|
||||
|
||||
@@ -140,8 +140,12 @@ class RasterGridManager(QObject):
|
||||
smargon_top_left=self.__geom.smargon,
|
||||
grid_size_mm=Coordinate(x=0.8 * self.__beam_size_mm.x, y=0.8 * self.__beam_size_mm.y),
|
||||
omega_deg=self.__geom.omega_deg,
|
||||
exp_time_s=cfg_get("daq.data_collection_settings.default_raster_settings.exp_time_s", 0.01),
|
||||
transmission=cfg_get("daq.data_collection_settings.default_raster_settings.transmission", 1.0),
|
||||
exp_time_s=cfg_get(
|
||||
"daq.data_collection_settings.default_raster_settings.exp_time_s", 0.01
|
||||
),
|
||||
transmission=cfg_get(
|
||||
"daq.data_collection_settings.default_raster_settings.transmission", 1.0
|
||||
),
|
||||
dtz=cfg_get("daq.data_collection_settings.default_raster_settings.dtz", 200.0),
|
||||
)
|
||||
self.__completed_grids: List[CompletedRasterGridElem] = []
|
||||
|
||||
@@ -3,7 +3,6 @@ from aarecommon.models.rotation_scan import CompletedRotationScan
|
||||
from PySide6.QtCore import QObject, Signal, Slot
|
||||
|
||||
|
||||
|
||||
class RotationScanManager(QObject):
|
||||
file_ready = Signal(str, int)
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class JFJochDBusClient(QObject):
|
||||
path = "/sls/mx/data/" + name + "_master.h5"
|
||||
print(f"Load dataset: {path} image: {number}")
|
||||
if self._ensure_interface():
|
||||
reply = self.__interface.call("LoadFile", path, number, 1)
|
||||
self.__interface.call("LoadFile", path, number, 1)
|
||||
|
||||
@Slot()
|
||||
def load_online(self):
|
||||
@@ -73,4 +73,4 @@ class JFJochDBusClient(QObject):
|
||||
return
|
||||
|
||||
if self._ensure_interface():
|
||||
reply = self.__interface.call("LoadFile", self.__detector_url, -1, 1)
|
||||
self.__interface.call("LoadFile", self.__detector_url, -1, 1)
|
||||
|
||||
@@ -554,13 +554,13 @@ class TutorialManager(QObject):
|
||||
|
||||
return None
|
||||
|
||||
def _run_setup_actions(self, step: TutorialStep) -> None:
|
||||
def _run_setup_actions(self, step: TutorialStepDefinition) -> None:
|
||||
if self.context is None:
|
||||
return
|
||||
for action in step.setup_actions:
|
||||
self.action_executor.execute_action(action, self.context)
|
||||
|
||||
def _run_cleanup_actions(self, step: TutorialStep) -> None:
|
||||
def _run_cleanup_actions(self, step: TutorialStepDefinition) -> None:
|
||||
if self.context is None:
|
||||
return
|
||||
for action in step.cleanup_actions:
|
||||
|
||||
@@ -14,6 +14,7 @@ from aare.gui.tutorials.tutorial_models import (
|
||||
TutorialContext,
|
||||
TutorialEvent,
|
||||
TutorialScenario,
|
||||
TutorialStepDefinition,
|
||||
TutorialTarget,
|
||||
TutorialTextRef,
|
||||
)
|
||||
@@ -188,7 +189,11 @@ class CompletionEvaluator:
|
||||
self._event_bus = event_bus
|
||||
|
||||
def is_step_complete(
|
||||
self, step: TutorialStep, context: TutorialContext, *, target_clicked: bool = False
|
||||
self,
|
||||
step: TutorialStepDefinition,
|
||||
context: TutorialContext,
|
||||
*,
|
||||
target_clicked: bool = False,
|
||||
) -> bool:
|
||||
if step.completion is None:
|
||||
return True
|
||||
|
||||
@@ -151,7 +151,8 @@ def test_grid_padding_fraction_scales(monkeypatch):
|
||||
|
||||
def test_crystal_union_extends_grid_only_when_enabled(monkeypatch):
|
||||
# crystal extends well beyond the loop_face box on +x
|
||||
mlbox = lambda: _fake_mlbox(loop_face=(150, 150, 300, 300), crystals=[(350, 150, 520, 300)])
|
||||
def mlbox():
|
||||
return _fake_mlbox(loop_face=(150, 150, 300, 300), crystals=[(350, 150, 520, 300)])
|
||||
|
||||
def cfg(enabled):
|
||||
return lambda k, d=None: (
|
||||
|
||||
@@ -23,12 +23,7 @@ with patch.dict("os.environ", {"JWT_AAREDAQ_KEY": "test_secret"}):
|
||||
respond_to_baton_request,
|
||||
)
|
||||
from aarecommon.errors.exception_handler import AuthenticationException, UserRightsException
|
||||
from aarecommon.models.auth import (
|
||||
BatonHolderInfo,
|
||||
BatonRequest,
|
||||
BatonRequestStatus,
|
||||
BatonStatus,
|
||||
)
|
||||
from aarecommon.models.auth import BatonHolderInfo, BatonRequest, BatonRequestStatus, BatonStatus
|
||||
from aarecommon.models.models import SessionsStateEnum
|
||||
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ def test_beamcenter_fit_no_converge():
|
||||
# beamcenter_fit might still find some contour if there's enough noise,
|
||||
# but curve_fit might fail to converge
|
||||
# If it doesn't converge, it returns None now.
|
||||
result = beamcenter_fit(image)
|
||||
beamcenter_fit(image)
|
||||
# It might actually return a result if it finds a random blob,
|
||||
# but we want to test the failure path.
|
||||
# To truly force non-convergence we might need a more extreme case,
|
||||
|
||||
@@ -2,7 +2,6 @@ import os
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import numpy as np
|
||||
|
||||
os.environ["JWT_AAREDAQ_KEY"] = "test_key_for_unit_testing"
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from aare.devices.experimental_hutch_shutter import ExperimentalHutchShutter
|
||||
|
||||
@patch("aare.devices.experimental_hutch_shutter.PV")
|
||||
def test_shutter_init(mock_pv):
|
||||
shutter = ExperimentalHutchShutter(MXBeamline.X06DA)
|
||||
ExperimentalHutchShutter(MXBeamline.X06DA)
|
||||
assert mock_pv.call_count == 3
|
||||
# Check if PV names are correct
|
||||
args = [call.args[0] for call in mock_pv.call_args_list]
|
||||
|
||||
@@ -8,7 +8,7 @@ from aare.devices.filter_transmission import FilterTransmission
|
||||
|
||||
@patch("aare.devices.filter_transmission.PV")
|
||||
def test_filter_init(mock_pv):
|
||||
filters = FilterTransmission(MXBeamline.X06DA)
|
||||
FilterTransmission(MXBeamline.X06DA)
|
||||
assert mock_pv.call_count == 3
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ def test_timer_box_auto_accept(qtbot):
|
||||
|
||||
assert box.result() == QMessageBox.StandardButton.Yes
|
||||
|
||||
|
||||
@pytest.mark.timeout(10)
|
||||
def test_ring_current_low_check_ok(qtbot):
|
||||
# Should return True immediately if current is high enough
|
||||
|
||||
Reference in New Issue
Block a user