Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/actions/setup-python/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ runs:
- name: Install Python project
shell: bash
run: |
pip install --no-cache-dir --editable .[dev] && \
pip install --no-cache-dir --editable .[all,dev] && \
for pkg in python/loris_*; do \
pip install --no-cache-dir --no-deps --editable "$pkg"; \
done
25 changes: 18 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ dependencies = [
]

[project.optional-dependencies]
all = [
"loris-ephys-visualizer-module @ {root:uri}/python/loris_ephys_visualizer_module",
"loris-meegqc-module @ {root:uri}/python/loris_meegqc_module",
"loris-server @ {root:uri}/python/loris_server",
]
dev = [
"pyright",
"pytest",
Expand All @@ -55,11 +60,14 @@ packages = [
members = ["python/loris_*"]

[tool.uv.sources]
loris_bids_importer = { workspace = true }
loris_bids_utils = { workspace = true }
loris_dicom_importer = { workspace = true }
loris_ephys_chunker = { workspace = true }
loris_utils = { workspace = true }
loris_bids_importer = { workspace = true }
loris_bids_utils = { workspace = true }
loris_dicom_importer = { workspace = true }
loris_ephys_chunker = { workspace = true }
loris_ephys_visualizer_module = { workspace = true }
loris_meegqc_module = { workspace = true }
loris_server = { workspace = true }
loris_utils = { workspace = true }

[tool.ruff]
src = ["python"]
Expand All @@ -68,15 +76,18 @@ line-length = 120
preview = true

[tool.ruff.lint]
ignore = ["E202", "E203", "E221", "E241", "E251", "E272"]
select = ["E", "EXE", "F", "I", "N", "RUF", "UP", "W"]
ignore = ["E202", "E203", "E221", "E241", "E251", "E272", "FAST003"]
select = ["E", "EXE", "F", "FAST", "I", "N", "RUF", "UP", "W"]

[tool.ruff.lint.pycodestyle]
max-doc-length = 100

[tool.ruff.lint.per-file-ignores]
# ORM models often have very long lines.
"python/lib/db/models/*.py" = ["E501"]
# ORM queries can use booleans and `None` in comparisons.
"python/lib/db/queries/*.py" = ["E711", "E712"]
"python/loris_meegqc_module/src/loris_meegqc_module/database/models/*.py" = ["E501"]

# The strict type checking configuration is used to type check only the modern (typed) modules. An
# additional basic type checking configuration to type check legacy modules can be found in the
Expand Down
8 changes: 8 additions & 0 deletions python/lib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
from lib.logging import log_error_exit


def get_jwt_secret_key_config(env: Env) -> str:
"""
Get the LORIS JWT secret key from the in-database configuration.
"""

return _get_config_value(env, 'JWTKey')


def get_patient_id_dicom_header_config(env: Env) -> Literal['PatientID', 'PatientName']:
"""
Get the DICOM header in which to look for the patient ID from the in-database configuration, or
Expand Down
1 change: 1 addition & 0 deletions python/lib/db/decorators/y_n_bool.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class YNBool(TypeDecorator[bool]):
"""

impl = Enum('Y', 'N')
cache_ok = True

def process_bind_param(self, value: bool | None, dialect: Dialect) -> Literal['Y', 'N'] | None:
match value:
Expand Down
12 changes: 12 additions & 0 deletions python/lib/db/misc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from datetime import datetime

from sqlalchemy import func, select
from sqlalchemy.orm import Session as Database


def get_database_time(db: Database) -> datetime:
"""
Get the current time from the database server.
"""

return db.execute(select(func.now())).scalar_one()
35 changes: 35 additions & 0 deletions python/lib/db/models/bids_dataset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from datetime import datetime
from pathlib import Path

from sqlalchemy.orm import Mapped, mapped_column

from lib.db.base import Base
from lib.db.decorators.string_path import StringPath


class DbBidsDataset(Base):
"""
A LORIS BIDS dataset.
"""

__tablename__ = 'bids_dataset'

id: Mapped[int] = mapped_column('ID', primary_key=True, autoincrement=True)
"""
The ID of this BIDS dataset.
"""

path: Mapped[Path] = mapped_column('Path', StringPath, unique=True)
"""
The path of this BIDS dataset, relative to the LORIS data directory.
"""

insert_time: Mapped[datetime] = mapped_column('InsertTime', default=datetime.now)
"""
The time at which this BIDS dataset was created in LORIS.
"""

update_time: Mapped[datetime] = mapped_column('UpdateTime', default=datetime.now)
"""
The last time at which this BIDS dataset was updated in LORIS.
"""
61 changes: 61 additions & 0 deletions python/lib/db/models/bids_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from datetime import datetime
from pathlib import Path

from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship

import lib.db.models.bids_dataset as db_bids_dataset
from lib.db.base import Base
from lib.db.decorators.int_bool import IntBool
from lib.db.decorators.string_path import StringPath


class DbBidsFile(Base):
"""
A file within a LORIS BIDS dataset.
"""

__tablename__ = 'bids_file'
__table_args__ = (
UniqueConstraint('DatasetID', 'Path', name='bids_file_dataset_id_path_unique'),
)

id: Mapped[int] = mapped_column('ID', primary_key=True, autoincrement=True)
"""
The ID of this BIDS file.
"""

dataset_id: Mapped[int] = mapped_column('DatasetID', ForeignKey('bids_dataset.ID', ondelete='CASCADE'))
"""
The ID of the BIDS dataset to which this file belongs.
"""

path: Mapped[Path] = mapped_column('Path', StringPath)
"""
The path of this file relative to its LORIS BIDS dataset.
"""

source_path: Mapped[Path | None] = mapped_column('SourcePath', StringPath)
"""
The source path of this file relative to the BIDS dataset from which it was imported.
"""

insert_time: Mapped[datetime] = mapped_column('InsertTime', default=datetime.now)
"""
The time at which this BIDS dataset was created in LORIS.
"""

blake2b_hash: Mapped[str] = mapped_column('Blake2bHash')
"""
The BLAKE2b hash of this file.
"""

derivative: Mapped[bool] = mapped_column('Derivative', IntBool)
"""
Whether this file is a BIDS derivative.
"""

dataset: Mapped['db_bids_dataset.DbBidsDataset'] = relationship('DbBidsDataset')
"""
The BIDS dataset to which this file belongs.
"""
11 changes: 11 additions & 0 deletions python/lib/db/models/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

import lib.db.models.bids_file as db_bids_file
import lib.db.models.dicom_archive as db_dicom_archive
import lib.db.models.file_parameter as db_file_parameter
import lib.db.models.session as db_session
Expand Down Expand Up @@ -40,6 +41,16 @@ class DbFile(Base):
acquisition_order_per_modality : Mapped[int | None] = mapped_column('AcqOrderPerModality')
acquisition_date : Mapped[date | None] = mapped_column('AcquisitionDate')

bids_info_id: Mapped[int | None] = mapped_column('BidsInfoID', ForeignKey('bids_file.ID', ondelete='SET NULL'))
"""
The ID of the BIDS information of this file, if any.
"""

bids_info: Mapped['db_bids_file.DbBidsFile | None'] = relationship('DbBidsFile')
"""
The BIDS information of this file, if any.
"""

session: Mapped['db_session.DbSession'] = relationship('DbSession', back_populates='files')
"""
The session to which this file belongs.
Expand Down
18 changes: 18 additions & 0 deletions python/lib/db/models/meg_ctf_head_shape_file.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from datetime import datetime
from pathlib import Path

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

import lib.db.models.bids_file as db_bids_file
import lib.db.models.meg_ctf_head_shape_point as db_meg_ctf_head_shape_point
from lib.db.base import Base
from lib.db.decorators.string_path import StringPath
Expand All @@ -20,11 +23,21 @@ class DbMegCtfHeadShapeFile(Base):
ID of the head shape file.
"""

bids_info_id: Mapped[int | None] = mapped_column('BidsInfoID', ForeignKey('bids_file.ID', ondelete='SET NULL'))
"""
The ID of the BIDS information of this head shape file, if any.
"""

path: Mapped[Path] = mapped_column('Path', StringPath)
"""
Path of the head shape file relative to the LORIS data directory.
"""

insert_time: Mapped[datetime] = mapped_column('InsertTime', default=datetime.now)
"""
The time at which this head shape file was created in LORIS.
"""

blake2b_hash: Mapped[str] = mapped_column('Blake2bHash')
"""
Blake2B hash of the head shape file, which may be used to check that the on-disk file data
Expand All @@ -35,3 +48,8 @@ class DbMegCtfHeadShapeFile(Base):
"""
3D points present in the head shape file.
"""

bids_info: Mapped['db_bids_file.DbBidsFile | None'] = relationship('DbBidsFile')
"""
The BIDS information of this head shape file, if any.
"""
15 changes: 15 additions & 0 deletions python/lib/db/models/module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlalchemy.orm import Mapped, mapped_column, relationship

import lib.db.models.permission as db_permission
from lib.db.base import Base
from lib.db.decorators.y_n_bool import YNBool


class DbModule(Base):
__tablename__ = 'modules'

id : Mapped[int] = mapped_column('ID', primary_key=True)
name : Mapped[str] = mapped_column('Name', unique=True)
active : Mapped[bool] = mapped_column('Active', YNBool)

permissions: Mapped[list['db_permission.DbPermission']] = relationship('DbPermission', back_populates='module')
19 changes: 19 additions & 0 deletions python/lib/db/models/permission.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

import lib.db.models.module as db_module
import lib.db.models.permission_category as db_permission_category
from lib.db.base import Base


class DbPermission(Base):
__tablename__ = 'permissions'

id : Mapped[int] = mapped_column('permID', primary_key=True)
code : Mapped[str] = mapped_column('code', default='', unique=True)
description : Mapped[str] = mapped_column('description', default='')
module_id : Mapped[int | None] = mapped_column('moduleID', ForeignKey('modules.ID'))
category_id : Mapped[int] = mapped_column('categoryID', ForeignKey('permissions_category.ID'), default=2)

module : Mapped['db_module.DbModule | None'] = relationship('DbModule', back_populates='permissions')
category : Mapped['db_permission_category.DbPermissionCategory'] = relationship('DbPermissionCategory')
10 changes: 10 additions & 0 deletions python/lib/db/models/permission_category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from sqlalchemy.orm import Mapped, mapped_column

from lib.db.base import Base


class DbPermissionCategory(Base):
__tablename__ = 'permissions_category'

id : Mapped[int] = mapped_column('ID', primary_key=True)
description : Mapped[str] = mapped_column('Description')
11 changes: 11 additions & 0 deletions python/lib/db/models/physio_event_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

import lib.db.models.bids_file as db_bids_file
import lib.db.models.imaging_file_type as db_imaging_file_type
import lib.db.models.physio_event_parameter as db_physio_event_parameter
import lib.db.models.physio_file as db_physio_file
Expand All @@ -24,8 +25,18 @@ class DbPhysioEventFile(Base):
last_update : Mapped[datetime] = mapped_column('LastUpdate', default=datetime.now)
last_written : Mapped[datetime] = mapped_column('LastWritten', default=datetime.now)

bids_info_id: Mapped[int | None] = mapped_column('BidsInfoID', ForeignKey('bids_file.ID', ondelete='SET NULL'))
"""
The ID of the BIDS information of this event file, if any.
"""

physio_file : Mapped['db_physio_file.DbPhysioFile | None'] = relationship('DbPhysioFile')
project : Mapped['db_project.DbProject | None'] = relationship('DbProject')
imaging_file_type : Mapped['db_imaging_file_type.DbImagingFileType | None'] = relationship('DbImagingFileType')
task_events : Mapped[list['db_physio_task_event.DbPhysioTaskEvent']] = relationship('DbPhysioTaskEvent', back_populates='event_file')
event_parameters : Mapped[list['db_physio_event_parameter.DbPhysioEventParameter']] = relationship('DbPhysioEventParameter', back_populates='event_file')

bids_info: Mapped['db_bids_file.DbBidsFile | None'] = relationship('DbBidsFile')
"""
The BIDS information of this event file, if any.
"""
13 changes: 12 additions & 1 deletion python/lib/db/models/physio_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

import lib.db.models.bids_file as db_bids_file
import lib.db.models.meg_ctf_head_shape_file as db_meg_ctf_head_shape_file
import lib.db.models.physio_channel as db_physio_channel
import lib.db.models.physio_event_archive as db_physio_event_archive
Expand Down Expand Up @@ -38,7 +39,12 @@ class DbPhysioFile(Base):
be a directory for MEG CTF data.
"""

head_shape_file_id: Mapped[int | None] = mapped_column('HeadShapeFileID', ForeignKey('meg_ctf_head_shape_file.ID'))
bids_info_id: Mapped[int | None] = mapped_column('BidsInfoID', ForeignKey('bids_file.ID', ondelete='SET NULL'))
"""
The ID of the BIDS information of this file, if any.
"""

head_shape_file_id: Mapped[int | None] = mapped_column('HeadShapeFileID', ForeignKey('meg_ctf_head_shape_file.ID', ondelete='SET NULL'))
"""
ID of the head shape file associated to this file, which is only present for MEG CTF files.
"""
Expand All @@ -53,6 +59,11 @@ class DbPhysioFile(Base):
event_files : Mapped[list['db_physio_event_file.DbPhysioEventFile']] = relationship('DbPhysioEventFile', back_populates='physio_file')
task_events : Mapped[list['db_physio_task_event.DbPhysioTaskEvent']] = relationship('DbPhysioTaskEvent', back_populates='physio_file')

bids_info: Mapped['db_bids_file.DbBidsFile | None'] = relationship('DbBidsFile')
"""
The BIDS information of this file, if any.
"""

head_shape_file: Mapped['db_meg_ctf_head_shape_file.DbMegCtfHeadShapeFile | None'] = relationship('DbMegCtfHeadShapeFile')
"""
The head shape file associated to this file, which is only present for MEG CTF files.
Expand Down
7 changes: 7 additions & 0 deletions python/lib/db/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from sqlalchemy.orm import Mapped, mapped_column, relationship

import lib.db.models.permission as db_permission
import lib.db.models.project as db_project
import lib.db.models.site as db_site
import lib.db.models.user_permission # type: ignore # ruff:ignore[unused-import]
import lib.db.models.user_project # type: ignore # ruff:ignore[unused-import]
import lib.db.models.user_site # type: ignore # ruff:ignore[unused-import]
from lib.db.base import Base
Expand Down Expand Up @@ -55,3 +57,8 @@ class DbUser(Base):
"""
The sites to which this user belongs to.
"""

permissions: Mapped[list['db_permission.DbPermission']] = relationship('DbPermission', secondary='user_perm_rel')
"""
The permissions granted to this user.
"""
Loading
Loading