Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2c400ac
Run pydantic migrator on murfey and fix issues that arise
stephen-riggs Jan 4, 2024
ab06896
No more procrunner
stephen-riggs Jan 4, 2024
32495bc
Optionals got lost in rebase
stephen-riggs Aug 30, 2024
bd28c7b
This change seems wrong
stephen-riggs Aug 30, 2024
9f31673
Merge main, with pyproject changes
stephen-riggs Oct 8, 2024
685b511
Pydantic deprecation warning bits
stephen-riggs Oct 9, 2024
4eb3cb7
sqlalchemy2 change to metadata
stephen-riggs Oct 11, 2024
d940ee3
More pydantic 2 model dumps
stephen-riggs Oct 14, 2024
ef33445
Pydantic doesn't like things starting model_
stephen-riggs Oct 14, 2024
72533df
Require zocalo>=1
stephen-riggs Oct 15, 2024
ce1728f
merge in main
stephen-riggs Oct 31, 2024
e0b0411
Unnecessary movement of components
stephen-riggs Oct 31, 2024
7dfa6ae
Catch up main commits
stephen-riggs Feb 10, 2025
846f7b5
Ran pydantic update tool on clem files
stephen-riggs Feb 10, 2025
ed8aa3c
Merge in main
stephen-riggs Jun 16, 2025
613244a
Apply 1 to 2 conversion with possible path to str problems
stephen-riggs Jun 16, 2025
585099b
Merged recent changes from 'main' branch
tieneupin Jun 25, 2025
61e2192
Added 'pydantic-settings' to list of dependencies
tieneupin Jun 25, 2025
417f766
Added 'field_validator' functions for the 'calibrations' and 'softwar…
tieneupin Jun 25, 2025
883b761
Pydantic v2 doesn't convert floats into ints, so added a 'field_valid…
tieneupin Jun 25, 2025
d0554ae
Replaced 'model_search_directory' with 'picking_model_search_directory'
tieneupin Jun 25, 2025
e419ba3
Changed 'atlas_pixel_size' from int to float in 'DCGroupParameters' m…
tieneupin Jun 26, 2025
1e73985
Log traceback when encountering exceptions in Contexts
tieneupin Jun 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ classifiers = [
dependencies = [
"backports.entry_points_selectable",
"defusedxml", # For safely parsing XML files
"pydantic<2", # Locked to <2 by cygwin terminal
"pydantic>=2",
"pydantic-settings",
"requests",
"rich",
"werkzeug",
Expand All @@ -47,7 +48,7 @@ client = [
"websocket-client",
]
developer = [
"bump-my-version<0.11.0", # Version control
"bump-my-version", # Version control
"ipykernel", # Enable interactive coding with VS Code and Jupyter Notebook
"pre-commit", # Formatting, linting, type checking, etc.
"pytest", # Test code functionality
Expand All @@ -61,7 +62,7 @@ server = [
"aiohttp",
"cryptography",
"fastapi[standard]",
"ispyb", # Responsible for setting requirements for SQLAlchemy and mysql-connector-python; v10.0.0: sqlalchemy <2, mysql-connector-python >=8.0.32
"ispyb>=10.2.4", # Responsible for setting requirements for SQLAlchemy and mysql-connector-python;
"jinja2",
"mrcfile",
"numpy<2",
Expand All @@ -73,7 +74,7 @@ server = [
"sqlalchemy[postgresql]", # Add as explicit dependency
"sqlmodel",
"stomp-py<=8.1.0", # 8.1.1 (released 2024-04-06) doesn't work with our project
"zocalo",
"zocalo>=1",
]
[project.urls]
Bug-Tracker = "https://github.com/DiamondLightSource/python-murfey/issues"
Expand Down
3 changes: 2 additions & 1 deletion src/murfey/client/contexts/spa.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,8 @@ def post_transfer(
except Exception as e:
# try to continue if position information gathering fails so that movie is processed anyway
logger.warning(
f"Unable to register foil hole for {str(file_transferred_to)}. Exception: {str(e)}"
f"Unable to register foil hole for {str(file_transferred_to)}. Exception: {str(e)}",
exc_info=True,
)
foil_hole = None

Expand Down
10 changes: 7 additions & 3 deletions src/murfey/client/contexts/tomo.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
)

except Exception as e:
logger.error(f"ERROR {e}, {self.data_collection_parameters}")
logger.error(f"ERROR {e}, {self.data_collection_parameters}", exc_info=True)

Check warning on line 176 in src/murfey/client/contexts/tomo.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/client/contexts/tomo.py#L176

Added line #L176 was not covered by tests

def _file_transferred_to(
self, environment: MurfeyInstanceEnvironment, source: Path, file_path: Path
Expand Down Expand Up @@ -533,7 +533,9 @@
try:
for_parsing = xml.read()
except Exception:
logger.warning(f"Failed to parse file {metadata_file}")
logger.warning(

Check warning on line 536 in src/murfey/client/contexts/tomo.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/client/contexts/tomo.py#L536

Added line #L536 was not covered by tests
f"Failed to parse file {metadata_file}", exc_info=True
)
return OrderedDict({})
data = xmltodict.parse(for_parsing)
try:
Expand Down Expand Up @@ -628,7 +630,9 @@
/ int(mdoc_metadata["num_eer_frames"])
)
except Exception as e:
logger.error(f"Exception encountered in metadata gathering: {str(e)}")
logger.error(

Check warning on line 633 in src/murfey/client/contexts/tomo.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/client/contexts/tomo.py#L633

Added line #L633 was not covered by tests
f"Exception encountered in metadata gathering: {str(e)}", exc_info=True
)
return OrderedDict({})

return mdoc_metadata
6 changes: 2 additions & 4 deletions src/murfey/client/instance_environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from typing import Dict, List, NamedTuple, Optional
from urllib.parse import ParseResult

from pydantic import BaseModel
from pydantic import BaseModel, ConfigDict

from murfey.client.watchdir import DirWatcher

Expand Down Expand Up @@ -56,9 +56,7 @@ class MurfeyInstanceEnvironment(BaseModel):
samples: Dict[Path, SampleInfo] = {}
rsync_url: str = ""

class Config:
validate_assignment: bool = True
arbitrary_types_allowed: bool = True
model_config = ConfigDict(arbitrary_types_allowed=True)

def clear(self):
self.sources = []
Expand Down
2 changes: 1 addition & 1 deletion src/murfey/client/tui/screens.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@
try:
convert = lambda x: None if x == "None" else x
validated = model(**{k: convert(v) for k, v in form.items()})
log.info(validated.dict())
log.info(validated.model_dump())

Check warning on line 199 in src/murfey/client/tui/screens.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/client/tui/screens.py#L199

Added line #L199 was not covered by tests
return True
except (AttributeError, ValidationError) as e:
log.warning(f"Form validation failed: {str(e)}")
Expand Down
4 changes: 2 additions & 2 deletions src/murfey/instrument_server/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,14 @@ def register_processing_parameters(
session_id: MurfeySessionID, proc_param_block: ProcessingParameterBlock
):
data_collection_parameters[proc_param_block.label] = {}
for k, v in proc_param_block.params.dict().items():
for k, v in proc_param_block.params.model_dump().items():
if v is not None:
data_collection_parameters[proc_param_block.label][k] = v
if controllers.get(session_id):
controllers[session_id].data_collection_parameters.update(
data_collection_parameters[proc_param_block.label]
)
for k, v in proc_param_block.params.dict().items():
for k, v in proc_param_block.params.model_dump().items():
if v is not None and hasattr(controllers[session_id]._environment, k):
setattr(controllers[session_id]._environment, k, v)
return {"success": True}
Expand Down
8 changes: 3 additions & 5 deletions src/murfey/server/api/clem.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from backports.entry_points_selectable import entry_points
from fastapi import APIRouter
from pydantic import BaseModel, validator
from pydantic import BaseModel, field_validator
from sqlalchemy.exc import NoResultFound
from sqlmodel import Session, select

Expand Down Expand Up @@ -820,10 +820,8 @@ class AlignAndMergeParams(BaseModel):
flatten: Literal["mean", "min", "max", ""] = ""
align_across: Literal["enabled", ""] = ""

@validator(
"images",
pre=True,
)
@field_validator("images", mode="before")
@classmethod
def parse_stringified_list(cls, value):
if isinstance(value, str):
try:
Expand Down
2 changes: 1 addition & 1 deletion src/murfey/server/api/prometheus.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def increment_rsync_file_count(
logger.error(
f"Failed to find rsync instance for visit {sanitise(visit_name)} "
"with the following properties: \n"
f"{rsyncer_info.dict()}",
f"{rsyncer_info.model_dump()}",
exc_info=True,
)
return None
Expand Down
2 changes: 1 addition & 1 deletion src/murfey/server/api/session_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def get_current_visits(instrument_name: str, db=ispyb_db):


class SessionInfo(BaseModel):
session_id: Optional[int]
session_id: Optional[int] = None
session_name: str = ""
rescale: bool = True

Expand Down
20 changes: 10 additions & 10 deletions src/murfey/server/api/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class DCGroupParameters(BaseModel):
tag: str
atlas: str = ""
sample: Optional[int] = None
atlas_pixel_size: int = 0
atlas_pixel_size: float = 0


@router.post("/visits/{visit_name}/{session_id}/register_data_collection_group")
Expand Down Expand Up @@ -346,13 +346,13 @@ class SPAProcessFile(BaseModel):
tag: str
path: str
description: str
processing_job: Optional[int]
data_collection_id: Optional[int]
processing_job: Optional[int] = None
data_collection_id: Optional[int] = None
image_number: int
autoproc_program_id: Optional[int]
foil_hole_id: Optional[int]
pixel_size: Optional[float]
dose_per_frame: Optional[float]
autoproc_program_id: Optional[int] = None
foil_hole_id: Optional[int] = None
pixel_size: Optional[float] = None
dose_per_frame: Optional[float] = None
mc_binning: Optional[int] = 1
gain_ref: Optional[str] = None
extract_downscale: bool = True
Expand Down Expand Up @@ -608,9 +608,9 @@ class TomoProcessFile(BaseModel):
tag: str
image_number: int
pixel_size: float
dose_per_frame: Optional[float]
dose_per_frame: Optional[float] = None
frame_count: int
tilt_axis: Optional[float]
tilt_axis: Optional[float] = None
mc_uuid: Optional[int] = None
voltage: float = 300
mc_binning: int = 1
Expand Down Expand Up @@ -894,7 +894,7 @@ class Sample(BaseModel):
sample_group_id: int
sample_id: int
subsample_id: int
image_path: Optional[Path]
image_path: Optional[Path] = None


@correlative_router.get("/visit/{visit_name}/samples")
Expand Down
32 changes: 19 additions & 13 deletions src/murfey/server/demo_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,38 @@
from fastapi.responses import FileResponse, HTMLResponse
from ispyb.sqlalchemy import BLSession
from PIL import Image
from pydantic import BaseModel, BaseSettings
from pydantic import BaseModel
from pydantic_settings import BaseSettings

Check warning on line 17 in src/murfey/server/demo_api.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/server/demo_api.py#L16-L17

Added lines #L16 - L17 were not covered by tests
from sqlalchemy import func
from sqlmodel import select
from werkzeug.utils import secure_filename

import murfey.server.api.bootstrap
import murfey.server.prometheus as prom
from murfey.server import (
_flush_grid_square_records,
_murfey_id,
get_hostname,
get_microscope,
sanitise,
sanitise_path,
)
from murfey.server import shutdown as _shutdown
from murfey.server import templates
from murfey.server.api.auth import MurfeySessionID, validate_token
from murfey.server.api import templates
from murfey.server.api.auth import MurfeySessionIDFrontend as MurfeySessionID
from murfey.server.api.auth import validate_token

Check warning on line 26 in src/murfey/server/demo_api.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/server/demo_api.py#L24-L26

Added lines #L24 - L26 were not covered by tests
from murfey.server.api.session_info import Visit
from murfey.server.api.workflow import (
DCGroupParameters,
DCParameters,
ProcessingJobParameters,
)
from murfey.server.feedback import (

Check warning on line 33 in src/murfey/server/demo_api.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/server/demo_api.py#L33

Added line #L33 was not covered by tests
_flush_grid_square_records,
_murfey_id,
get_microscope,
sanitise,
)
from murfey.server.murfey_db import murfey_db
from murfey.util.config import MachineConfig, from_file, security_from_file
from murfey.server.run import shutdown as _shutdown
from murfey.util import sanitise_path
from murfey.util.config import (

Check warning on line 42 in src/murfey/server/demo_api.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/server/demo_api.py#L40-L42

Added lines #L40 - L42 were not covered by tests
MachineConfig,
from_file,
get_hostname,
security_from_file,
)
from murfey.util.db import (
AutoProcProgram,
ClientEnvironment,
Expand Down
2 changes: 1 addition & 1 deletion src/murfey/server/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from prometheus_client import make_asgi_app
from pydantic import BaseSettings
from pydantic_settings import BaseSettings

import murfey.server
import murfey.server.api.auth
Expand Down
73 changes: 54 additions & 19 deletions src/murfey/util/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,21 @@
import socket
from functools import lru_cache
from pathlib import Path
from typing import Literal, Optional, Union
from typing import Any, Literal, Optional

import yaml
from backports.entry_points_selectable import entry_points
from pydantic import BaseModel, BaseSettings, Extra, validator
from pydantic import BaseModel, ConfigDict, RootModel, ValidationInfo, field_validator
from pydantic_settings import BaseSettings


class MagnificationTable(RootModel[dict[int, float]]):
pass


CALIBRATIONS_VALIDATION_SCHEMAS = {
"magnification": MagnificationTable,
}


class MachineConfig(BaseModel): # type: ignore
Expand All @@ -26,7 +36,7 @@
# Hardware and software -----------------------------------------------------------
camera: str = "FALCON"
superres: bool = False
calibrations: dict[str, dict[str, Union[dict, float]]]
calibrations: dict[str, Any]
acquisition_software: list[str]
software_versions: dict[str, str] = {}
software_settings_output_directories: dict[str, list[str]] = {}
Expand Down Expand Up @@ -72,7 +82,7 @@

# Particle picking setup
default_model: Path
model_search_directory: str = "processing"
picking_model_search_directory: str = "processing"
initial_model_search_directory: str = "processing/initial_model"

# Data analysis plugins
Expand All @@ -93,15 +103,43 @@
node_creator_queue: str = "node_creator"
notifications_queue: str = "pato_notification"

class Config:
"""
Inner class that defines this model's parsing and serialising behaviour
"""
# Pydantic BaseModel settings
model_config = ConfigDict(extra="allow")

@field_validator("calibrations", mode="before")
@classmethod
def validate_calibration_data(
cls, v: dict[str, dict[Any, Any]]
) -> dict[str, dict[Any, Any]]:
# Pass the calibration dictionaries through their matching Pydantic models, if any are set
if isinstance(v, dict):
validated = {}
for (
key,
value,
) in v.items():
model_cls = CALIBRATIONS_VALIDATION_SCHEMAS.get(key)

Check warning on line 121 in src/murfey/util/config.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/util/config.py#L121

Added line #L121 was not covered by tests
if model_cls:
try:

Check warning on line 123 in src/murfey/util/config.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/util/config.py#L123

Added line #L123 was not covered by tests
# Validate and store as a dict object with the corrected types
validated[key] = model_cls.model_validate(value).root
except Exception as e:
raise ValueError(f"Validation failed for key '{key}': {e}")

Check warning on line 127 in src/murfey/util/config.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/util/config.py#L125-L127

Added lines #L125 - L127 were not covered by tests
else:
validated[key] = value

Check warning on line 129 in src/murfey/util/config.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/util/config.py#L129

Added line #L129 was not covered by tests
return validated
# Let it validate and fail as-is
return v

Check warning on line 132 in src/murfey/util/config.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/util/config.py#L132

Added line #L132 was not covered by tests

extra = Extra.allow
json_encoders = {
Path: str,
}
@field_validator("software_versions", mode="before")
@classmethod
def validate_software_versions(cls, v: dict[str, Any]) -> dict[str, str]:
# Software versions should be numerical strings, even if they appear int- or float-like
if isinstance(v, dict):
validated = {key: str(value) for key, value in v.items()}
return validated

Check warning on line 140 in src/murfey/util/config.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/util/config.py#L139-L140

Added lines #L139 - L140 were not covered by tests
# Let it validate and fail as-is
return v

Check warning on line 142 in src/murfey/util/config.py

View check run for this annotation

Codecov / codecov/patch

src/murfey/util/config.py#L142

Added line #L142 was not covered by tests


def from_file(config_file_path: Path, instrument: str = "") -> dict[str, MachineConfig]:
Expand Down Expand Up @@ -144,16 +182,13 @@
graylog_host: str = ""
graylog_port: Optional[int] = None

class Config:
json_encoders = {
Path: str,
}
model_config = ConfigDict()

@validator("graylog_port")
@field_validator("graylog_port")
def check_port_present_if_host_is(
cls, v: Optional[int], values: dict, **kwargs
cls, v: Optional[int], info: ValidationInfo, **kwargs
) -> Optional[int]:
if values["graylog_host"] and v is None:
if info.data.get("graylog_host") and v is None:
raise ValueError("The Graylog port must be set if the Graylog host is")
return v

Expand Down
Loading