Added Postgres support.

This commit is contained in:
lwark
2024-07-25 08:41:44 -05:00
parent 54e1e55804
commit 4bc5e08ac6
32 changed files with 579 additions and 1030 deletions

View File

@@ -1,7 +1,12 @@
## 202407.04
- Added support for postgresql databases (auto backup not functional).
## 202407.02
- HTML template for 'About'.
- More flexible custom parsers/writers due to custom info items.
- Vastly increased portability of the reporting functions.
## 202407.01

View File

@@ -1,7 +1,8 @@
## Startup:
1. Open the app using the shortcut in the Submissions folder. For example: L:\\Robotics Laboratory Support\\Submissions\\submissions_v122b.exe - Shortcut.lnk (Version may have changed).
1. Open the app using the shortcut in the Submissions folder: L:\Robotics Laboratory Support\Submissions\Submissions App.lnk.
1. Ignore the large black window of fast scrolling text, it is there for debugging purposes.
2. The 'Submissions' tab should be open by default.
3. Default settings (config.yml) will be copied to C:\Users\{YOUR USERNAME}\AppData\Local\submissions\config
## Logging in New Run:
*should fit 90% of usage cases*
@@ -109,3 +110,16 @@ This is meant to import .xslx files created from the Design & Analysis Software
1. Click "Monthly" -> "Link PCR Logs".
2. Chose the .csv file taken from the PCR table runlogs folder.
## SETUP:
## Download:
1. Clone or download from github.
2. Enter the downloaded folder.
## Database:
1. Copy 'alembic_default.ini' to 'alembic.ini' in the same folder.
2. Open 'alembic.ini' and edit 'sqlalchemy.url' to the desired path of the database.
1. The path by default is sqlite based. Postgresql support is available.
2. Postgres path

View File

@@ -2,7 +2,7 @@
- The hardest part of this is going to be the sample parsing. I'm onto using the cell formulas in the plate map to suss out the location in the lookup table, but it could get a little recursive up in here.
- [ ] Create a default info return function.
- [x] Parse comment from excel sheet.
- [ ] Make reporting better.
- [x] Make reporting better.
- [x] Build master query method?
- Obviously there will need to be extensions, but I feel the attr method I have in Submissions could work.
- [x] Fix Artic RSLNamer
@@ -23,7 +23,7 @@
- [x] Merge BasicSubmission.find_subclasses and BasicSubmission.find_polymorphic_subclass
- [x] Fix updating of Extraction Kit in submission form widget.
- [x] Fix cropping of gel image.
- [ ] Create Tips ... *sigh*.
- [x] Create Tips ... *sigh*.
- [x] Create platemap image from html for export to pdf.
- [x] Move plate map maker to submission.
- [x] Finish Equipment Parser (add in regex to id asset_number)

View File

@@ -1,8 +1,8 @@
"""First Commit
"""rebuild 20240723
Revision ID: e3f6770ef515
Revision ID: 0746f7e2c10e
Revises:
Create Date: 2024-01-22 14:01:02.958292
Create Date: 2024-07-23 14:08:37.436400
"""
from alembic import op
@@ -10,7 +10,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e3f6770ef515'
revision = '0746f7e2c10e'
down_revision = None
branch_labels = None
depends_on = None
@@ -25,6 +25,12 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('submitter_id')
)
op.create_table('_configitem',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('key', sa.String(length=32), nullable=True),
sa.Column('value', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_contact',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
@@ -66,9 +72,10 @@ def upgrade() -> None:
op.create_table('_process',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint('id')
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('_reagenttype',
op.create_table('_reagentrole',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('eol_ext', sa.Interval(), nullable=True),
@@ -78,10 +85,17 @@ def upgrade() -> None:
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('info_map', sa.JSON(), nullable=True),
sa.Column('template_file', sa.BLOB(), nullable=True),
sa.Column('defaults', sa.JSON(), nullable=True),
sa.Column('template_file', sa.LargeBinary(), nullable=True),
sa.Column('sample_map', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('_tiprole',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_bacterialculturesample',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('organism', sa.String(length=64), nullable=True),
@@ -117,17 +131,17 @@ def upgrade() -> None:
sa.ForeignKeyConstraint(['equipmentrole_id'], ['_equipmentrole.id'], ),
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], )
)
op.create_table('_kittypereagenttypeassociation',
sa.Column('reagent_types_id', sa.INTEGER(), nullable=False),
op.create_table('_kittypereagentroleassociation',
sa.Column('reagent_roles_id', sa.INTEGER(), nullable=False),
sa.Column('kits_id', sa.INTEGER(), nullable=False),
sa.Column('submission_type_id', sa.INTEGER(), nullable=False),
sa.Column('uses', sa.JSON(), nullable=True),
sa.Column('required', sa.INTEGER(), nullable=True),
sa.Column('last_used', sa.String(length=32), nullable=True),
sa.ForeignKeyConstraint(['kits_id'], ['_kittype.id'], ),
sa.ForeignKeyConstraint(['reagent_types_id'], ['_reagenttype.id'], ),
sa.ForeignKeyConstraint(['reagent_roles_id'], ['_reagentrole.id'], ),
sa.ForeignKeyConstraint(['submission_type_id'], ['_submissiontype.id'], ),
sa.PrimaryKeyConstraint('reagent_types_id', 'kits_id', 'submission_type_id')
sa.PrimaryKeyConstraint('reagent_roles_id', 'kits_id', 'submission_type_id')
)
op.create_table('_kittypes_processes',
sa.Column('process_id', sa.INTEGER(), nullable=True),
@@ -141,13 +155,19 @@ def upgrade() -> None:
sa.ForeignKeyConstraint(['contact_id'], ['_contact.id'], ),
sa.ForeignKeyConstraint(['org_id'], ['_organization.id'], )
)
op.create_table('_process_tiprole',
sa.Column('process_id', sa.INTEGER(), nullable=True),
sa.Column('tiprole_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], ),
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], )
)
op.create_table('_reagent',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('type_id', sa.INTEGER(), nullable=True),
sa.Column('role_id', sa.INTEGER(), nullable=True),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('lot', sa.String(length=64), nullable=True),
sa.Column('expiry', sa.TIMESTAMP(), nullable=True),
sa.ForeignKeyConstraint(['type_id'], ['_reagenttype.id'], name='fk_reagent_type_id', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['role_id'], ['_reagentrole.id'], name='fk_reagent_role_id', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_submissiontypeequipmentroleassociation',
@@ -175,6 +195,23 @@ def upgrade() -> None:
sa.ForeignKeyConstraint(['equipmentroles_id'], ['_submissiontype.id'], ),
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], )
)
op.create_table('_submissiontypetiproleassociation',
sa.Column('tiprole_id', sa.INTEGER(), nullable=False),
sa.Column('submissiontype_id', sa.INTEGER(), nullable=False),
sa.Column('uses', sa.JSON(), nullable=True),
sa.Column('static', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['submissiontype_id'], ['_submissiontype.id'], ),
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], ),
sa.PrimaryKeyConstraint('tiprole_id', 'submissiontype_id')
)
op.create_table('_tips',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('role_id', sa.INTEGER(), nullable=True),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('lot', sa.String(length=64), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['_tiprole.id'], name='fk_tip_role_id', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_wastewatersample',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('ww_processing_num', sa.String(length=64), nullable=True),
@@ -197,13 +234,15 @@ def upgrade() -> None:
sa.Column('extraction_kit_id', sa.INTEGER(), nullable=True),
sa.Column('submission_type_name', sa.String(), nullable=True),
sa.Column('technician', sa.String(length=64), nullable=True),
sa.Column('reagents_id', sa.String(), nullable=True),
sa.Column('reagents_id', sa.INTEGER(), nullable=True),
sa.Column('extraction_info', sa.JSON(), nullable=True),
sa.Column('pcr_info', sa.JSON(), nullable=True),
sa.Column('run_cost', sa.FLOAT(precision=2), nullable=True),
sa.Column('uploaded_by', sa.String(length=32), nullable=True),
sa.Column('signed_by', sa.String(length=32), nullable=True),
sa.Column('comment', sa.JSON(), nullable=True),
sa.Column('submission_category', sa.String(length=64), nullable=True),
sa.Column('cost_centre', sa.String(length=64), nullable=True),
sa.Column('contact_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['contact_id'], ['_contact.id'], name='fk_BS_contact_id', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['extraction_kit_id'], ['_kittype.id'], name='fk_BS_extkit_id', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['reagents_id'], ['_reagent.id'], name='fk_BS_reagents_id', ondelete='SET NULL'),
sa.ForeignKeyConstraint(['submission_type_name'], ['_submissiontype.name'], name='fk_BS_subtype_name', ondelete='SET NULL'),
@@ -212,11 +251,23 @@ def upgrade() -> None:
sa.UniqueConstraint('rsl_plate_num'),
sa.UniqueConstraint('submitter_plate_num')
)
op.create_table('_reagenttypes_reagents',
op.create_table('_equipment_tips',
sa.Column('equipment_id', sa.INTEGER(), nullable=True),
sa.Column('tips_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
)
op.create_table('_reagentroles_reagents',
sa.Column('reagent_id', sa.INTEGER(), nullable=True),
sa.Column('reagenttype_id', sa.INTEGER(), nullable=True),
sa.Column('reagentrole_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['reagent_id'], ['_reagent.id'], ),
sa.ForeignKeyConstraint(['reagenttype_id'], ['_reagenttype.id'], )
sa.ForeignKeyConstraint(['reagentrole_id'], ['_reagentrole.id'], )
)
op.create_table('_tiproles_tips',
sa.Column('tiprole_id', sa.INTEGER(), nullable=True),
sa.Column('tips_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], ),
sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
)
op.create_table('_bacterialculture',
sa.Column('id', sa.INTEGER(), nullable=False),
@@ -225,7 +276,7 @@ def upgrade() -> None:
)
op.create_table('_control',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('parent_id', sa.String(), nullable=True),
sa.Column('parent_id', sa.INTEGER(), nullable=True),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('submitted_date', sa.TIMESTAMP(), nullable=True),
sa.Column('contains', sa.JSON(), nullable=True),
@@ -264,19 +315,31 @@ def upgrade() -> None:
sa.PrimaryKeyConstraint('reagent_id', 'submission_id')
)
op.create_table('_submissionsampleassociation',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('sample_id', sa.INTEGER(), nullable=False),
sa.Column('submission_id', sa.INTEGER(), nullable=False),
sa.Column('row', sa.INTEGER(), nullable=False),
sa.Column('column', sa.INTEGER(), nullable=False),
sa.Column('submission_rank', sa.INTEGER(), nullable=False),
sa.Column('base_sub_type', sa.String(), nullable=True),
sa.ForeignKeyConstraint(['sample_id'], ['_basicsample.id'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
sa.PrimaryKeyConstraint('submission_id', 'row', 'column')
sa.PrimaryKeyConstraint('submission_id', 'row', 'column'),
sa.UniqueConstraint('id')
)
op.create_table('_submissiontipsassociation',
sa.Column('tip_id', sa.INTEGER(), nullable=False),
sa.Column('submission_id', sa.INTEGER(), nullable=False),
sa.Column('role_name', sa.String(length=32), nullable=False),
sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
sa.ForeignKeyConstraint(['tip_id'], ['_tips.id'], ),
sa.PrimaryKeyConstraint('tip_id', 'submission_id', 'role_name')
)
op.create_table('_wastewater',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('ext_technician', sa.String(length=64), nullable=True),
sa.Column('pcr_technician', sa.String(length=64), nullable=True),
sa.Column('pcr_info', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['id'], ['_basicsubmission.id'], ),
sa.PrimaryKeyConstraint('id')
)
@@ -284,20 +347,36 @@ def upgrade() -> None:
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('artic_technician', sa.String(length=64), nullable=True),
sa.Column('dna_core_submission_number', sa.String(length=64), nullable=True),
sa.Column('pcr_info', sa.JSON(), nullable=True),
sa.Column('gel_image', sa.String(length=64), nullable=True),
sa.Column('gel_info', sa.JSON(), nullable=True),
sa.Column('gel_controls', sa.JSON(), nullable=True),
sa.Column('source_plates', sa.JSON(), nullable=True),
sa.Column('artic_date', sa.TIMESTAMP(), nullable=True),
sa.Column('ngs_date', sa.TIMESTAMP(), nullable=True),
sa.Column('gel_date', sa.TIMESTAMP(), nullable=True),
sa.Column('gel_barcode', sa.String(length=16), nullable=True),
sa.ForeignKeyConstraint(['id'], ['_basicsubmission.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_wastewaterarticassociation',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('source_plate', sa.String(length=16), nullable=True),
sa.Column('source_plate_number', sa.INTEGER(), nullable=True),
sa.Column('source_well', sa.String(length=8), nullable=True),
sa.Column('ct', sa.String(length=8), nullable=True),
sa.ForeignKeyConstraint(['id'], ['_submissionsampleassociation.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_wastewaterassociation',
sa.Column('sample_id', sa.INTEGER(), nullable=False),
sa.Column('submission_id', sa.INTEGER(), nullable=False),
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('ct_n1', sa.FLOAT(precision=2), nullable=True),
sa.Column('ct_n2', sa.FLOAT(precision=2), nullable=True),
sa.Column('n1_status', sa.String(length=32), nullable=True),
sa.Column('n2_status', sa.String(length=32), nullable=True),
sa.Column('pcr_results', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['sample_id'], ['_submissionsampleassociation.sample_id'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_submissionsampleassociation.submission_id'], ),
sa.PrimaryKeyConstraint('sample_id', 'submission_id')
sa.ForeignKeyConstraint(['id'], ['_submissionsampleassociation.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
@@ -305,30 +384,38 @@ def upgrade() -> None:
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_wastewaterassociation')
op.drop_table('_wastewaterarticassociation')
op.drop_table('_wastewaterartic')
op.drop_table('_wastewater')
op.drop_table('_submissiontipsassociation')
op.drop_table('_submissionsampleassociation')
op.drop_table('_submissionreagentassociation')
op.drop_table('_submissionequipmentassociation')
op.drop_table('_control')
op.drop_table('_bacterialculture')
op.drop_table('_reagenttypes_reagents')
op.drop_table('_tiproles_tips')
op.drop_table('_reagentroles_reagents')
op.drop_table('_equipment_tips')
op.drop_table('_basicsubmission')
op.drop_table('_wastewatersample')
op.drop_table('_tips')
op.drop_table('_submissiontypetiproleassociation')
op.drop_table('_submissiontypes_processes')
op.drop_table('_submissiontypekittypeassociation')
op.drop_table('_submissiontypeequipmentroleassociation')
op.drop_table('_reagent')
op.drop_table('_process_tiprole')
op.drop_table('_orgs_contacts')
op.drop_table('_kittypes_processes')
op.drop_table('_kittypereagenttypeassociation')
op.drop_table('_kittypereagentroleassociation')
op.drop_table('_equipmentroles_processes')
op.drop_table('_equipmentroles_equipment')
op.drop_table('_equipment_processes')
op.drop_table('_discount')
op.drop_table('_bacterialculturesample')
op.drop_table('_tiprole')
op.drop_table('_submissiontype')
op.drop_table('_reagenttype')
op.drop_table('_reagentrole')
op.drop_table('_process')
op.drop_table('_organization')
op.drop_table('_kittype')
@@ -336,5 +423,6 @@ def downgrade() -> None:
op.drop_table('_equipment')
op.drop_table('_controltype')
op.drop_table('_contact')
op.drop_table('_configitem')
op.drop_table('_basicsample')
# ### end Alembic commands ###

View File

@@ -1,54 +0,0 @@
"""Made WastewaterArticAssociation
Revision ID: 16b20d4368e6
Revises: d2b094cfa308
Create Date: 2024-06-13 12:16:48.385516
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '16b20d4368e6'
down_revision = 'd2b094cfa308'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_wastewaterarticassociation',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('source_plate', sa.String(length=16), nullable=True),
sa.Column('source_plate_number', sa.INTEGER(), nullable=True),
sa.Column('source_well', sa.String(length=8), nullable=True),
sa.Column('ct', sa.String(length=8), nullable=True),
sa.ForeignKeyConstraint(['id'], ['_submissionsampleassociation.id'], ),
sa.PrimaryKeyConstraint('id')
)
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint(None, ['id'])
#
# with op.batch_alter_table('_submissiontipsassociation', schema=None) as batch_op:
# batch_op.alter_column('role_name',
# existing_type=sa.INTEGER(),
# type_=sa.String(length=32),
# existing_nullable=True)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissiontipsassociation', schema=None) as batch_op:
batch_op.alter_column('role_name',
existing_type=sa.String(length=32),
type_=sa.INTEGER(),
existing_nullable=True)
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
op.drop_table('_wastewaterarticassociation')
# ### end Alembic commands ###

View File

@@ -1,51 +0,0 @@
"""adding cost centre storage to basicsubmission
Revision ID: 6d2a357860ef
Revises: e6647bd661d9
Create Date: 2024-04-24 13:01:14.923814
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6d2a357860ef'
down_revision = 'e6647bd661d9'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# op.drop_table('_alembic_tmp__submissionsampleassociation')
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
batch_op.add_column(sa.Column('cost_centre', sa.String(length=64), nullable=True))
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint(None, ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint(None, type_='unique')
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
batch_op.drop_column('used_cost_centre')
op.create_table('_alembic_tmp__submissionsampleassociation',
sa.Column('sample_id', sa.INTEGER(), nullable=False),
sa.Column('submission_id', sa.INTEGER(), nullable=False),
sa.Column('row', sa.INTEGER(), nullable=False),
sa.Column('column', sa.INTEGER(), nullable=False),
sa.Column('base_sub_type', sa.VARCHAR(), nullable=True),
sa.Column('id', sa.INTEGER(), server_default=sa.text('1'), nullable=False),
sa.ForeignKeyConstraint(['sample_id'], ['_basicsample.id'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
sa.PrimaryKeyConstraint('submission_id', 'row', 'column'),
sa.UniqueConstraint('id', name='ssa_id')
)
# ### end Alembic commands ###

View File

@@ -1,34 +0,0 @@
"""adding gel image, info. Again
Revision ID: 70426df72f80
Revises: c4201b0ea9fe
Create Date: 2024-01-30 08:47:22.809841
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '70426df72f80'
down_revision = 'c4201b0ea9fe'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.add_column(sa.Column('gel_image', sa.String(length=64), nullable=True))
batch_op.add_column(sa.Column('gel_info', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.drop_column('gel_info')
batch_op.drop_column('gel_image')
# ### end Alembic commands ###

View File

@@ -1,38 +0,0 @@
"""tweaking submission sample association
Revision ID: 70d5a751f579
Revises: 97392dda5436
Create Date: 2024-01-25 13:39:34.163501
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '70d5a751f579'
down_revision = '97392dda5436'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=False)
batch_op.create_unique_constraint("ssa_id_unique", ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
batch_op.drop_constraint("ssa_id_unique", type_='unique')
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###

View File

@@ -1,72 +0,0 @@
"""Adding fields to Artic
Revision ID: 861b52a2004e
Revises: b744e8a452fd
Create Date: 2024-06-05 13:35:19.012337
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import sqlite
# revision identifiers, used by Alembic.
revision = '861b52a2004e'
down_revision = 'b744e8a452fd'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('_alembic_tmp__basicsubmission')
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint(None, ['id'])
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.add_column(sa.Column('artic_date', sa.TIMESTAMP(), nullable=True))
batch_op.add_column(sa.Column('ngs_date', sa.TIMESTAMP(), nullable=True))
batch_op.add_column(sa.Column('gel_date', sa.TIMESTAMP(), nullable=True))
batch_op.add_column(sa.Column('gel_barcode', sa.String(length=16), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.drop_column('gel_barcode')
batch_op.drop_column('gel_date')
batch_op.drop_column('ngs_date')
batch_op.drop_column('artic_date')
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint(None, type_='unique')
op.create_table('_alembic_tmp__basicsubmission',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('rsl_plate_num', sa.VARCHAR(length=32), nullable=False),
sa.Column('submitter_plate_num', sa.VARCHAR(length=127), nullable=True),
sa.Column('submitted_date', sa.TIMESTAMP(), nullable=True),
sa.Column('submitting_lab_id', sa.INTEGER(), nullable=True),
sa.Column('sample_count', sa.INTEGER(), nullable=True),
sa.Column('extraction_kit_id', sa.INTEGER(), nullable=True),
sa.Column('submission_type_name', sa.VARCHAR(), nullable=True),
sa.Column('technician', sa.VARCHAR(length=64), nullable=True),
sa.Column('reagents_id', sa.VARCHAR(), nullable=True),
sa.Column('extraction_info', sqlite.JSON(), nullable=True),
sa.Column('run_cost', sa.FLOAT(), nullable=True),
sa.Column('signed_by', sa.VARCHAR(length=32), nullable=True),
sa.Column('comment', sqlite.JSON(), nullable=True),
sa.Column('submission_category', sa.VARCHAR(length=64), nullable=True),
sa.Column('cost_centre', sa.VARCHAR(length=64), nullable=True),
sa.Column('contact_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['contact_id'], ['_contact.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['extraction_kit_id'], ['_kittype.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['reagents_id'], ['_reagent.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['submission_type_name'], ['_submissiontype.name'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['submitting_lab_id'], ['_organization.id'], ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('rsl_plate_num'),
sa.UniqueConstraint('submitter_plate_num')
)
# ### end Alembic commands ###

View File

@@ -1,34 +0,0 @@
"""Adding ranking to SubmissionSampleAssociation
Revision ID: 874af342c82c
Revises: a04c25fd9138
Create Date: 2024-05-03 15:08:20.194275
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '874af342c82c'
down_revision = 'a04c25fd9138'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
batch_op.add_column(sa.Column('submission_rank', sa.INTEGER(), nullable=False, default=1))
# batch_op.create_unique_constraint(None, ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint(None, type_='unique')
batch_op.drop_column('submission_rank')
# ### end Alembic commands ###

View File

@@ -1,50 +0,0 @@
"""Update to submissionsampleassociation
Revision ID: 97392dda5436
Revises: e3f6770ef515
Create Date: 2024-01-25 09:10:04.384194
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '97392dda5436'
down_revision = 'e3f6770ef515'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
batch_op.add_column(sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=True))
batch_op.create_unique_constraint("submissionsampleassociation_id", ['id'])
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.add_column(sa.Column('id', sa.INTEGER(), nullable=False))
# batch_op.drop_constraint("sample_id", type_='foreignkey')
# batch_op.drop_constraint("submission_id", type_='foreignkey')
batch_op.create_foreign_key("fk_subsampassoc_id", '_submissionsampleassociation', ['id'], ['id'])
# batch_op.drop_column('sample_id')
# batch_op.drop_column('submission_id')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.add_column(sa.Column('submission_id', sa.INTEGER(), nullable=False))
batch_op.add_column(sa.Column('sample_id', sa.INTEGER(), nullable=False))
batch_op.drop_constraint(None, type_='foreignkey')
batch_op.create_foreign_key(None, '_submissionsampleassociation', ['submission_id'], ['submission_id'])
batch_op.create_foreign_key(None, '_submissionsampleassociation', ['sample_id'], ['sample_id'])
batch_op.drop_column('id')
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
batch_op.drop_constraint(None, type_='unique')
batch_op.drop_column('id')
# ### end Alembic commands ###

View File

@@ -1,38 +0,0 @@
"""splitting off sample info in SubmissionType
Revision ID: a04c25fd9138
Revises: 6d2a357860ef
Create Date: 2024-05-01 09:11:44.957532
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a04c25fd9138'
down_revision = '6d2a357860ef'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint(None, ['id'])
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
batch_op.add_column(sa.Column('sample_map', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
batch_op.drop_column('sample_map')
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint(None, type_='unique')
# ### end Alembic commands ###

View File

@@ -1,40 +0,0 @@
"""Attaching contact to submission
Revision ID: b744e8a452fd
Revises: f829a8ab292f
Create Date: 2024-06-04 14:21:38.163431
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'b744e8a452fd'
down_revision = 'f829a8ab292f'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
batch_op.add_column(sa.Column('contact_id', sa.INTEGER(), nullable=True))
batch_op.create_foreign_key('fk_BS_contact_id', '_contact', ['contact_id'], ['id'], ondelete='SET NULL')
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint(None, ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint(None, type_='unique')
with op.batch_alter_table('_basicsubmission', schema=None) as batch_op:
batch_op.drop_constraint('fk_BS_contact_id', type_='foreignkey')
batch_op.drop_column('contact_id')
# ### end Alembic commands ###

View File

@@ -1,42 +0,0 @@
"""adding gel image, info
Revision ID: c4201b0ea9fe
Revises: 70d5a751f579
Create Date: 2024-01-30 08:42:03.928933
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c4201b0ea9fe'
down_revision = '70d5a751f579'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
batch_op.create_unique_constraint("unique_ssa_id", ['id'])
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=True)
with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
batch_op.drop_constraint("unique_ssa_id", type_='unique')
# ### end Alembic commands ###

View File

@@ -1,95 +0,0 @@
"""Adding tips
Revision ID: d2b094cfa308
Revises: 861b52a2004e
Create Date: 2024-06-11 13:16:57.319769
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd2b094cfa308'
down_revision = '861b52a2004e'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_tiprole',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_process_tiprole',
sa.Column('process_id', sa.INTEGER(), nullable=True),
sa.Column('tiprole_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['process_id'], ['_process.id'], ),
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], )
)
op.create_table('_submissiontypetiproleassociation',
sa.Column('tiprole_id', sa.INTEGER(), nullable=False),
sa.Column('submissiontype_id', sa.INTEGER(), nullable=False),
sa.Column('uses', sa.JSON(), nullable=True),
sa.Column('static', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['submissiontype_id'], ['_submissiontype.id'], ),
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], ),
sa.PrimaryKeyConstraint('tiprole_id', 'submissiontype_id')
)
op.create_table('_tips',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('role_id', sa.INTEGER(), nullable=True),
sa.Column('name', sa.String(length=64), nullable=True),
sa.Column('lot', sa.String(length=64), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['_tiprole.id'], name='fk_tip_role_id', ondelete='SET NULL'),
sa.PrimaryKeyConstraint('id')
)
op.create_table('_equipment_tips',
sa.Column('equipment_id', sa.INTEGER(), nullable=True),
sa.Column('tips_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['equipment_id'], ['_equipment.id'], ),
sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
)
op.create_table('_submissiontipsassociation',
sa.Column('tip_id', sa.INTEGER(), nullable=False),
sa.Column('submission_id', sa.INTEGER(), nullable=False),
sa.Column('role_name', sa.String(), nullable=False),
# sa.ForeignKeyConstraint(['role_name'], ['_tiprole.name'], ),
sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
sa.ForeignKeyConstraint(['tip_id'], ['_tips.id'], ),
sa.PrimaryKeyConstraint('tip_id', 'submission_id', 'role_name')
)
op.create_table('_tiproles_tips',
sa.Column('tiprole_id', sa.INTEGER(), nullable=True),
sa.Column('tips_id', sa.INTEGER(), nullable=True),
sa.ForeignKeyConstraint(['tiprole_id'], ['_tiprole.id'], ),
sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
)
# op.create_table('_submissions_tips',
# sa.Column('submission_id', sa.INTEGER(), nullable=True),
# sa.Column('tips_id', sa.INTEGER(), nullable=True),
# sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
# sa.ForeignKeyConstraint(['tips_id'], ['_tips.id'], )
# )
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint("unique_ssa_id", ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint(None, type_='unique')
op.drop_table('_submissions_tips')
op.drop_table('_tiproles_tips')
op.drop_table('_submissiontipassociation')
op.drop_table('_equipment_tips')
op.drop_table('_tips')
op.drop_table('_submissiontypetiproleassociation')
op.drop_table('_process_tiprole')
op.drop_table('_tiprole')
# ### end Alembic commands ###

View File

@@ -1,33 +0,0 @@
"""adding default info to submissiontype
Revision ID: e6647bd661d9
Revises: f18487b41f45
Create Date: 2024-04-22 12:02:21.512781
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e6647bd661d9'
down_revision = 'f18487b41f45'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
batch_op.add_column(sa.Column('defaults', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_submissiontype', schema=None) as batch_op:
batch_op.drop_column('defaults')
# ### end Alembic commands ###

View File

@@ -1,51 +0,0 @@
"""adding source plates to Artic submission...again
Revision ID: f18487b41f45
Revises: fabf697c721d
Create Date: 2024-04-17 10:42:30.508213
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f18487b41f45'
down_revision = 'fabf697c721d'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# op.drop_table('_alembic_tmp__submissionsampleassociation')
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint("ssa_id", ['id'])
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.add_column(sa.Column('source_plates', sa.JSON(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.drop_column('source_plates')
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint(None, type_='unique')
# op.create_table('_alembic_tmp__submissionsampleassociation',
# sa.Column('sample_id', sa.INTEGER(), nullable=False),
# sa.Column('submission_id', sa.INTEGER(), nullable=False),
# sa.Column('row', sa.INTEGER(), nullable=False),
# sa.Column('column', sa.INTEGER(), nullable=False),
# sa.Column('base_sub_type', sa.VARCHAR(), nullable=True),
# sa.Column('id', sa.INTEGER(), server_default=sa.text('1'), nullable=False),
# sa.ForeignKeyConstraint(['sample_id'], ['_basicsample.id'], ),
# sa.ForeignKeyConstraint(['submission_id'], ['_basicsubmission.id'], ),
# sa.PrimaryKeyConstraint('submission_id', 'row', 'column'),
# sa.UniqueConstraint('id', name='ssa_unique')
# )
# ### end Alembic commands ###

View File

@@ -1,39 +0,0 @@
"""Adding configitems to db
Revision ID: f829a8ab292f
Revises: 874af342c82c
Create Date: 2024-05-15 14:03:11.767480
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f829a8ab292f'
down_revision = '874af342c82c'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('_configitem',
sa.Column('id', sa.INTEGER(), nullable=False),
sa.Column('key', sa.String(), nullable=True),
sa.Column('value', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint(None, ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint(None, type_='unique')
op.drop_table('_configitem')
# ### end Alembic commands ###

View File

@@ -1,48 +0,0 @@
"""adding source plates to Artic submission
Revision ID: fabf697c721d
Revises: 70426df72f80
Create Date: 2024-03-06 11:01:34.794411
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'fabf697c721d'
down_revision = '70426df72f80'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.create_unique_constraint("ssa_unique", ['id'])
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.add_column(sa.Column('source_plates', sa.JSON(), nullable=True))
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('_wastewaterassociation', schema=None) as batch_op:
batch_op.alter_column('id',
existing_type=sa.INTEGER(),
nullable=True)
with op.batch_alter_table('_wastewaterartic', schema=None) as batch_op:
batch_op.drop_column('source_plates')
# with op.batch_alter_table('_submissionsampleassociation', schema=None) as batch_op:
# batch_op.drop_constraint("ssa_unique", type_='unique')
# ### end Alembic commands ###

104
alembic_default.ini Normal file
View File

@@ -0,0 +1,104 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
script_location = ./alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python-dateutil library that can be
# installed by adding `alembic[tz]` to the pip requirements
# string value is passed to dateutil.tz.gettz()
# leave blank for localtime
# timezone =
# max length of characters to apply to the
# "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to ./alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:./alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = sqlite:///L:\Robotics Laboratory Support\Submissions\submissions.db
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@@ -17,7 +17,7 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
connection_record (_type_): _description_
"""
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
# cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()

View File

@@ -3,12 +3,13 @@ Contains all models for sqlalchemy
"""
from __future__ import annotations
import sys, logging
from sqlalchemy import Column, INTEGER, String, JSON
from sqlalchemy import Column, INTEGER, String, JSON, inspect
from sqlalchemy.orm import DeclarativeMeta, declarative_base, Query, Session
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.exc import ArgumentError
from typing import Any, List
from pathlib import Path
from tools import report_result
# Load testing environment
if 'pytest' in sys.modules:
@@ -90,7 +91,7 @@ class BaseClass(Base):
Returns:
dict | list | str: Output of key:value dict or single (list, str) desired variable
"""
"""
dicto = dict(singles=['id'])
output = {}
for k, v in dicto.items():
@@ -110,7 +111,7 @@ class BaseClass(Base):
Returns:
Any | List[Any]: Result of query execution.
"""
"""
return cls.execute_query(**kwargs)
@classmethod
@@ -152,38 +153,43 @@ class BaseClass(Base):
case _:
return query.limit(limit).all()
@report_result
def save(self):
"""
Add the object to the database and commit
"""
# logger.debug(f"Saving object: {pformat(self.__dict__)}")
report = Report()
try:
self.__database_session__.add(self)
self.__database_session__.commit()
# self.__database_session__.merge(self)
except Exception as e:
logger.critical(f"Problem saving object: {e}")
self.__database_session__.rollback()
report.add_result(Result(msg=f"Problem saving object {e}", status="Critical"))
return report
class ConfigItem(BaseClass):
"""
Key:JSON objects to store config settings in database.
"""
"""
id = Column(INTEGER, primary_key=True)
key = Column(String(32)) #: Name of the configuration item.
value = Column(JSON) #: Value associated with the config item.
key = Column(String(32)) #: Name of the configuration item.
value = Column(JSON) #: Value associated with the config item.
def __repr__(self):
return f"ConfigItem({self.key} : {self.value})"
@classmethod
def get_config_items(cls, *args) -> ConfigItem|List[ConfigItem]:
def get_config_items(cls, *args) -> ConfigItem | List[ConfigItem]:
"""
Get desired config items from database
Returns:
ConfigItem|List[ConfigItem]: Config item(s)
"""
"""
config_items = cls.__database_session__.query(cls).all()
config_items = [item for item in config_items if item.key in args]
if len(args) == 1:
@@ -196,4 +202,5 @@ from .controls import *
from .organizations import *
from .kits import *
from .submissions import *
BasicSubmission.reagents.creator = lambda reg: SubmissionReagentAssociation(reagent=reg)

View File

@@ -1532,7 +1532,7 @@ class Process(BaseClass):
query = cls.__database_session__.query(cls)
match name:
case str():
logger.debug(f"Lookup Process with name str {name}")
# logger.debug(f"Lookup Process with name str {name}")
query = query.filter(cls.name == name)
limit = 1
case _:

View File

@@ -23,7 +23,7 @@ import pandas as pd
from openpyxl import Workbook
from openpyxl.worksheet.worksheet import Worksheet
from openpyxl.drawing.image import Image as OpenpyxlImage
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr
from tools import row_map, setup_lookup, jinja_template_loading, rreplace, row_keys, check_key_or_attr, Result, Report
from datetime import datetime, date
from typing import List, Any, Tuple, Literal
from dateutil.parser import parse
@@ -691,7 +691,8 @@ class BasicSubmission(BaseClass):
Args:
input_dict (dict): Input sample dictionary
xl (pd.ExcelFile): original xl workbook, used for child classes mostly
xl (Workbook): original xl workbook, used for child classes mostly
custom_fields: Dictionary of locations, ranges, etc to be used by this function
Returns:
dict: Updated sample dictionary
@@ -739,6 +740,7 @@ class BasicSubmission(BaseClass):
input_excel (Workbook): initial workbook.
info (dict | None, optional): dictionary of additional info. Defaults to None.
backup (bool, optional): Whether this is part of a backup operation. Defaults to False.
custom_fields: Dictionary of locations, ranges, etc to be used by this function
Returns:
Workbook: Updated workbook
@@ -1046,14 +1048,16 @@ class BasicSubmission(BaseClass):
"""
code = 0
msg = ""
report = Report()
disallowed = ["id"]
if kwargs == {}:
raise ValueError("Need to narrow down query or the first available instance will be returned.")
for key in kwargs.keys():
if key in disallowed:
raise ValueError(
f"{key} is not allowed as a query argument as it could lead to creation of duplicate objects. Use .query() instead.")
instance = cls.query(submission_type=submission_type, limit=1, **kwargs)
sanitized_kwargs = {k: v for k, v in kwargs.items() if k not in disallowed}
# for key in kwargs.keys():
# if key in disallowed:
# raise ValueError(
# f"{key} is not allowed as a query argument as it could lead to creation of duplicate objects. Use .query() instead.")
instance = cls.query(submission_type=submission_type, limit=1, **sanitized_kwargs)
# logger.debug(f"Retrieved instance: {instance}")
if instance is None:
used_class = cls.find_polymorphic_subclass(attrs=kwargs, polymorphic_identity=submission_type)
@@ -1070,7 +1074,8 @@ class BasicSubmission(BaseClass):
else:
code = 1
msg = "This submission already exists.\nWould you like to overwrite?"
return instance, code, msg
report.add_result(Result(msg=msg, code=code))
return instance, report
# Custom context events for the ui
@@ -1135,7 +1140,7 @@ class BasicSubmission(BaseClass):
# logger.debug(widg)
widg.setParent(None)
pyd = self.to_pydantic(backup=True)
form = pyd.to_form(parent=obj)
form = pyd.to_form(parent=obj, disable=['rsl_plate_num'])
obj.app.table_widget.formwidget.layout().addWidget(form)
def add_comment(self, obj):
@@ -1352,13 +1357,29 @@ class Wastewater(BasicSubmission):
Args:
input_dict (dict): Input sample dictionary
xl (Workbook): xl (Workbook): original xl workbook, used for child classes mostly.
custom_fields: Dictionary of locations, ranges, etc to be used by this function
Returns:
dict: Updated sample dictionary
"""
input_dict = super().custom_info_parser(input_dict)
logger.debug(f"Input dict: {pformat(input_dict)}")
if xl is not None:
input_dict['csv'] = xl["Copy to import file"]
try:
input_dict['csv'] = xl["Copy to import file"]
except KeyError as e:
logger.error(e)
try:
match input_dict['rsl_plate_num']:
case dict():
input_dict['csv'] = xl[input_dict['rsl_plate_num']['value']]
case str():
input_dict['csv'] = xl[input_dict['rsl_plate_num']]
case _:
pass
except Exception as e:
logger.error(f"Error handling couldn't get csv due to: {e}")
return input_dict
@classmethod
@@ -1604,11 +1625,12 @@ class WastewaterArtic(BasicSubmission):
Args:
input_dict (dict): Input sample dictionary
xl (pd.ExcelFile): original xl workbook, used for child classes mostly
custom_fields: Dictionary of locations, ranges, etc to be used by this function
Returns:
dict: Updated sample dictionary
"""
# TODO: Clean up and move range start/stops to db somehow.
from backend.validators import RSLNamer
input_dict = super().custom_info_parser(input_dict)
egel_section = custom_fields['egel_results']
ws = xl[egel_section['sheet']]
@@ -1621,12 +1643,11 @@ class WastewaterArtic(BasicSubmission):
source_plates_section = custom_fields['source_plates']
ws = xl[source_plates_section['sheet']]
data = [dict(plate=ws.cell(row=ii, column=source_plates_section['plate_column']).value, starting_sample=ws.cell(row=ii, column=source_plates_section['starting_sample_column']).value) for ii in
range(source_plates_section['start_row'], source_plates_section['end_row'])]
range(source_plates_section['start_row'], source_plates_section['end_row']+1)]
for datum in data:
if datum['plate'] in ["None", None, ""]:
continue
else:
from backend.validators import RSLNamer
datum['plate'] = RSLNamer(filename=datum['plate'], sub_type="Wastewater").parsed_name
input_dict['source_plates'] = data
return input_dict
@@ -1820,6 +1841,7 @@ class WastewaterArtic(BasicSubmission):
input_excel (Workbook): initial workbook.
info (dict | None, optional): dictionary of additional info. Defaults to None.
backup (bool, optional): Whether this is part of a backup operation. Defaults to False.
custom_fields: Dictionary of locations, ranges, etc to be used by this function
Returns:
Workbook: Updated workbook
@@ -2798,7 +2820,7 @@ class WastewaterArticAssociation(SubmissionSampleAssociation):
DOC: https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html
"""
id = Column(INTEGER, ForeignKey("_submissionsampleassociation.id"), primary_key=True)
source_plate = Column(String(16))
source_plate = Column(String(32))
source_plate_number = Column(INTEGER)
source_well = Column(String(8))
ct = Column(String(8)) #: AKA ct for N1

View File

@@ -108,7 +108,7 @@ class PydReagent(BaseModel):
Returns:
dict: Information dictionary
"""
"""
try:
extras = list(self.model_extra.keys())
except AttributeError:
@@ -161,7 +161,7 @@ class PydReagent(BaseModel):
# reagent.reagent_submission_associations.append(assoc)
else:
assoc = None
report.add_result(Result(owner = __name__, code=0, msg="New reagent created.", status="Information"))
report.add_result(Result(owner=__name__, code=0, msg="New reagent created.", status="Information"))
else:
if submission is not None and reagent not in submission.reagents:
assoc = SubmissionReagentAssociation(reagent=reagent, submission=submission)
@@ -217,7 +217,7 @@ class PydSample(BaseModel, extra='allow'):
Returns:
dict: Information dictionary
"""
"""
fields = list(self.model_fields.keys()) + list(self.model_extra.keys())
return {k: getattr(self, k) for k in fields}
@@ -254,7 +254,8 @@ class PydSample(BaseModel, extra='allow'):
submission=submission,
sample=instance,
row=row, column=column, id=aid,
submission_rank=submission_rank, **self.model_extra)
submission_rank=submission_rank,
**self.model_extra)
# logger.debug(f"Using submission_sample_association: {association}")
try:
# instance.sample_submission_associations.append(association)
@@ -270,7 +271,7 @@ class PydSample(BaseModel, extra='allow'):
Returns:
dict: Information dictionary
"""
"""
try:
extras = list(self.model_extra.keys())
except AttributeError:
@@ -281,10 +282,10 @@ class PydSample(BaseModel, extra='allow'):
class PydTips(BaseModel):
name: str
lot: str|None = Field(default=None)
lot: str | None = Field(default=None)
role: str
def to_sql(self, submission:BasicSubmission) -> SubmissionTipsAssociation:
def to_sql(self, submission: BasicSubmission) -> SubmissionTipsAssociation:
"""
Con
@@ -293,7 +294,7 @@ class PydTips(BaseModel):
Returns:
SubmissionTipsAssociation: Association between queried tips and submission
"""
"""
tips = Tips.query(name=self.name, lot=self.lot, limit=1)
assoc = SubmissionTipsAssociation(submission=submission, tips=tips, role_name=self.role)
return assoc
@@ -305,7 +306,7 @@ class PydEquipment(BaseModel, extra='ignore'):
nickname: str | None
processes: List[str] | None
role: str | None
tips: List[PydTips]|None = Field(default=None)
tips: List[PydTips] | None = Field(default=None)
@field_validator('processes', mode='before')
@classmethod
@@ -338,23 +339,19 @@ class PydEquipment(BaseModel, extra='ignore'):
if equipment is None:
return
if submission is not None:
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
process = Process.query(name=self.processes[0])
if process is None:
logger.error(f"Found unknown process: {process}.")
# from frontend.widgets.pop_ups import QuestionAsker
# dlg = QuestionAsker(title="Add Process?",
# message=f"Unable to find {self.processes[0]} in the database.\nWould you like to add it?")
# if dlg.exec():
# kit = submission.extraction_kit
# submission_type = submission.submission_type
# process = Process(name=self.processes[0])
# process.kit_types.append(kit)
# process.submission_types.append(submission_type)
# process.equipment.append(equipment)
# process.save()
assoc.process = process
assoc.role = self.role
# NOTE: Need to make sure the same association is not added to the submission
assoc = SubmissionEquipmentAssociation.query(equipment_id=equipment.id, submission_id=submission.id,
role=self.role, limit=1)
if assoc is None:
assoc = SubmissionEquipmentAssociation(submission=submission, equipment=equipment)
process = Process.query(name=self.processes[0])
if process is None:
logger.error(f"Found unknown process: {process}.")
assoc.process = process
assoc.role = self.role
else:
assoc = None
else:
assoc = None
return equipment, assoc
@@ -365,7 +362,7 @@ class PydEquipment(BaseModel, extra='ignore'):
Returns:
dict: Information dictionary
"""
"""
try:
extras = list(self.model_extra.keys())
except AttributeError:
@@ -441,7 +438,7 @@ class PydSubmission(BaseModel, extra='allow'):
return value.date()
case int():
return dict(value=datetime.fromordinal(datetime(1900, 1, 1).toordinal() + value['value'] - 2).date(),
missing=True)
missing=True)
case str():
string = re.sub(r"(_|-)\d$", "", value['value'])
try:
@@ -508,7 +505,7 @@ class PydSubmission(BaseModel, extra='allow'):
output = "RSL-BS-Test001"
else:
output = RSLNamer(filename=values.data['filepath'].__str__(), sub_type=sub_type,
data=values.data).parsed_name
data=values.data).parsed_name
return dict(value=output, missing=True)
@field_validator("technician", mode="before")
@@ -637,14 +634,14 @@ class PydSubmission(BaseModel, extra='allow'):
self.submission_object = BasicSubmission.find_polymorphic_subclass(
polymorphic_identity=self.submission_type['value'])
def set_attribute(self, key:str, value):
def set_attribute(self, key: str, value):
"""
Better handling of attribute setting.
Args:
key (str): Name of field to set
value (_type_): Value to set field to.
"""
"""
self.__setattr__(name=key, value=value)
def handle_duplicate_samples(self):
@@ -710,7 +707,7 @@ class PydSubmission(BaseModel, extra='allow'):
missing_reagents = [reagent for reagent in self.reagents if reagent.missing]
return missing_info, missing_reagents
def to_sql(self) -> Tuple[BasicSubmission, Result]:
def to_sql(self) -> Tuple[BasicSubmission, Report]:
"""
Converts this instance into a backend.db.models.submissions.BasicSubmission instance
@@ -718,13 +715,13 @@ class PydSubmission(BaseModel, extra='allow'):
Tuple[BasicSubmission, Result]: BasicSubmission instance, result object
"""
# self.__dict__.update(self.model_extra)
report = Report()
dicto = self.improved_dict()
instance, code, msg = BasicSubmission.query_or_create(submission_type=self.submission_type['value'],
rsl_plate_num=self.rsl_plate_num['value'])
result = Result(msg=msg, code=code)
instance, result = BasicSubmission.query_or_create(submission_type=self.submission_type['value'],
rsl_plate_num=self.rsl_plate_num['value'])
report.add_result(result)
self.handle_duplicate_samples()
# logger.debug(f"Here's our list of duplicate removed samples: {self.samples}")
# for key, value in self.__dict__.items():
for key, value in dicto.items():
if isinstance(value, dict):
value = value['value']
@@ -733,13 +730,13 @@ class PydSubmission(BaseModel, extra='allow'):
# logger.debug(f"Setting {key} to {value}")
match key:
case "reagents":
if code == 1:
if report.results[0].code == 1:
instance.submission_reagent_associations = []
# logger.debug(f"Looking through {self.reagents}")
for reagent in self.reagents:
reagent, assoc, _ = reagent.toSQL(submission=instance)
# logger.debug(f"Association: {assoc}")
if assoc is not None:# and assoc not in instance.submission_reagent_associations:
if assoc is not None: # and assoc not in instance.submission_reagent_associations:
instance.submission_reagent_associations.append(assoc)
# instance.reagents.append(reagent)
case "samples":
@@ -755,10 +752,7 @@ class PydSubmission(BaseModel, extra='allow'):
if equip is None:
continue
equip, association = equip.toSQL(submission=instance)
if association is not None and association not in instance.submission_equipment_associations:
# association.save()
# logger.debug(
# f"Equipment association SQL object to be added to submission: {association.__dict__}")
if association is not None:
instance.submission_equipment_associations.append(association)
case "tips":
for tips in self.tips:
@@ -817,9 +811,9 @@ class PydSubmission(BaseModel, extra='allow'):
# except AttributeError as e:
# logger.debug(f"Something went wrong constructing instance {self.rsl_plate_num}: {e}")
# logger.debug(f"Constructed submissions message: {msg}")
return instance, result
return instance, report
def to_form(self, parent: QWidget):
def to_form(self, parent: QWidget, disable:list|None=None):
"""
Converts this instance into a frontend.widgets.submission_widget.SubmissionFormWidget
@@ -830,7 +824,8 @@ class PydSubmission(BaseModel, extra='allow'):
SubmissionFormWidget: Submission form widget
"""
from frontend.widgets.submission_widget import SubmissionFormWidget
return SubmissionFormWidget(parent=parent, submission=self)
logger.debug(f"Disbable: {disable}")
return SubmissionFormWidget(parent=parent, submission=self, disable=disable)
def to_writer(self) -> "SheetWriter":
"""
@@ -838,7 +833,7 @@ class PydSubmission(BaseModel, extra='allow'):
Returns:
SheetWriter: Sheetwriter object that will perform writing.
"""
"""
from backend.excel.writer import SheetWriter
return SheetWriter(self)
@@ -896,8 +891,8 @@ class PydSubmission(BaseModel, extra='allow'):
status="Warning")
report.add_result(result)
return output_reagents, report
def export_csv(self, filename:Path|str):
def export_csv(self, filename: Path | str):
try:
worksheet = self.csv
except AttributeError:
@@ -1024,4 +1019,3 @@ class PydEquipmentRole(BaseModel):
"""
from frontend.widgets.equipment_usage import RoleComboBox
return RoleComboBox(parent=parent, role=self, used=used)

View File

@@ -1,6 +1,6 @@
'''
"""
Constructs main application.
'''
"""
from PyQt6.QtWidgets import (
QTabWidget, QWidget, QVBoxLayout,
QHBoxLayout, QScrollArea, QMainWindow,
@@ -13,7 +13,7 @@ from markdown import markdown
from tools import check_if_app, Settings, Report, jinja_template_loading
from datetime import date
from .pop_ups import AlertPop, HTMLPop
from .pop_ups import HTMLPop
from .misc import LogParser
import logging, webbrowser, sys, shutil
from .submission_table import SubmissionsSheet
@@ -36,7 +36,7 @@ class App(QMainWindow):
self.report = Report()
# NOTE: indicate version and connected database in title bar
try:
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_path}"
self.title = f"Submissions App (v{ctx.package.__version__}) - {ctx.database_session.get_bind().url}"
except (AttributeError, KeyError):
self.title = f"Submissions App"
# NOTE: set initial app position and size
@@ -164,27 +164,6 @@ class App(QMainWindow):
instr = HTMLPop(html=html, title="Instructions")
instr.exec()
def result_reporter(self):
"""
Report any anomolous results - if any - to the user
Args:
result (dict | None, optional): The result from a function. Defaults to None.
"""
# logger.debug(f"Running results reporter for: {self.report.results}")
if len(self.report.results) > 0:
# logger.debug(f"We've got some results!")
for result in self.report.results:
# logger.debug(f"Showing result: {result}")
if result is not None:
alert = result.report()
if alert.exec():
pass
self.report = Report()
else:
self.statusBar().showMessage("Action completed sucessfully.", 5000)
def runSearch(self):
dlg = LogParser(self)
dlg.exec()
@@ -201,12 +180,19 @@ class App(QMainWindow):
Copies the database into the backup directory the first time it is opened every month.
"""
month = date.today().strftime("%Y-%m")
current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve()
# logger.debug(f"Here is the db directory: {self.ctx.database_path}")
# logger.debug(f"Here is the backup directory: {self.ctx.backup_path}")
current_month_bak = Path(self.ctx.backup_path).joinpath(f"submissions_backup-{month}").resolve().with_suffix(".db")
if not current_month_bak.exists() and "demo" not in self.ctx.database_path.__str__():
logger.info("No backup found for this month, backing up database.")
shutil.copyfile(self.ctx.database_path, current_month_bak)
match self.ctx.database_schema:
case "sqlite":
current_month_bak = current_month_bak.with_suffix(".db")
if not current_month_bak.exists() and "demo" not in self.ctx.database_path.__str__():
logger.info("No backup found for this month, backing up database.")
shutil.copyfile(self.ctx.database_path, current_month_bak)
case "postgresql+psycopg2":
logger.warning(f"Backup function not yet implemented for psql")
current_month_bak = current_month_bak.with_suffix(".psql")
class AddSubForm(QWidget):

View File

@@ -15,7 +15,7 @@ logger = logging.getLogger(f"submissions.{__name__}")
class EquipmentUsage(QDialog):
def __init__(self, parent, submission: BasicSubmission) -> QDialog:
def __init__(self, parent, submission: BasicSubmission):
super().__init__(parent)
self.submission = submission
self.setWindowTitle(f"Equipment Checklist - {submission.rsl_plate_num}")
@@ -139,7 +139,7 @@ class RoleComboBox(QWidget):
Changes what tips are available when process is changed
"""
process = self.process.currentText().strip()
logger.debug(f"Checking process: {process} for equipment {self.role.name}")
# logger.debug(f"Checking process: {process} for equipment {self.role.name}")
process = Process.query(name=process)
if process.tip_roles:
for iii, tip_role in enumerate(process.tip_roles):

View File

@@ -21,10 +21,10 @@ logger = logging.getLogger(f"submissions.{__name__}")
# Main window class
class GelBox(QDialog):
def __init__(self, parent, img_path:str|Path, submission:WastewaterArtic):
def __init__(self, parent, img_path: str | Path, submission: WastewaterArtic):
super().__init__(parent)
# NOTE: setting title
self.setWindowTitle("PyQtGraph")
self.setWindowTitle(f"Gel - {img_path}")
self.img_path = img_path
self.submission = submission
# NOTE: setting geometry
@@ -41,7 +41,7 @@ class GelBox(QDialog):
def UiComponents(self):
"""
Create widgets in ui
"""
"""
# NOTE: setting configuration options
pg.setConfigOptions(antialias=True)
# NOTE: creating image view object
@@ -49,41 +49,42 @@ class GelBox(QDialog):
# NOTE: Create image.
# NOTE: For some reason, ImageView wants to flip the image, so we have to rotate and flip the array first.
# NOTE: Using the Image.rotate function results in cropped image, so using np.
img = np.flip(np.rot90(np.array(Image.open(self.img_path)),1),0)
img = np.flip(np.rot90(np.array(Image.open(self.img_path)), 1), 0)
self.imv.setImage(img)
layout = QGridLayout()
layout.addWidget(QLabel("DNA Core Submission Number"),0,1)
layout.addWidget(QLabel("DNA Core Submission Number"), 21, 1)
self.core_number = QLineEdit()
self.core_number.setText(self.submission.dna_core_submission_number)
layout.addWidget(self.core_number, 0,2)
layout.addWidget(QLabel("Gel Barcode"),0,3)
layout.addWidget(self.core_number, 21, 2)
layout.addWidget(QLabel("Gel Barcode"), 21, 3)
self.gel_barcode = QLineEdit()
self.gel_barcode.setText(self.submission.gel_barcode)
layout.addWidget(self.gel_barcode, 0, 4)
layout.addWidget(self.gel_barcode, 21, 4)
# NOTE: setting this layout to the widget
# NOTE: plot window goes on right side, spanning 3 rows
layout.addWidget(self.imv, 1, 1,20,20)
layout.addWidget(self.imv, 0, 1, 20, 20)
# NOTE: setting this widget as central widget of the main window
try:
control_info = sorted(self.submission.gel_controls, key=lambda d: d['location'])
except KeyError:
control_info = None
self.form = ControlsForm(parent=self, control_info=control_info)
layout.addWidget(self.form,22,1,1,4)
layout.addWidget(self.form, 22, 1, 1, 4)
QBtn = QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout.addWidget(self.buttonBox, 23, 1, 1, 1)#, alignment=Qt.AlignmentFlag.AlignTop)
layout.addWidget(self.buttonBox, 23, 1, 1, 1) #, alignment=Qt.AlignmentFlag.AlignTop)
self.setLayout(layout)
def parse_form(self) -> Tuple[str, str|Path, list]:
def parse_form(self) -> Tuple[str, str | Path, list]:
"""
Get relevant values from self/form
Returns:
Tuple[str, str|Path, list]: output values
"""
"""
dna_core_submission_number = self.core_number.text()
gel_barcode = self.gel_barcode.text()
values, comment = self.form.parse_form()
@@ -92,7 +93,7 @@ class GelBox(QDialog):
class ControlsForm(QWidget):
def __init__(self, parent, control_info:List=None) -> None:
def __init__(self, parent, control_info: List = None) -> None:
super().__init__(parent)
self.layout = QGridLayout()
columns = []
@@ -101,9 +102,10 @@ class ControlsForm(QWidget):
tt_text = "\n".join([f"{item['sample_id']} - CELL {item['location']}" for item in control_info])
except TypeError:
tt_text = None
for iii, item in enumerate(["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
for iii, item in enumerate(
["Negative Control Key", "Description", "Results - 65 C", "Results - 63 C", "Results - Spike"]):
label = QLabel(item)
self.layout.addWidget(label, 0, iii,1,1)
self.layout.addWidget(label, 0, iii, 1, 1)
if iii > 1:
columns.append(item)
elif iii == 0:
@@ -114,7 +116,8 @@ class ControlsForm(QWidget):
label = QLabel(item)
self.layout.addWidget(label, iii, 0, 1, 1)
rows.append(item)
for iii, item in enumerate(["Processing Negative (PBS)", "Extraction Negative (Extraction buffers ONLY)", "Artic no-template control (mastermix ONLY)"], start=1):
for iii, item in enumerate(["Processing Negative (PBS)", "Extraction Negative (Extraction buffers ONLY)",
"Artic no-template control (mastermix ONLY)"], start=1):
label = QLabel(item)
self.layout.addWidget(label, iii, 1, 1, 1)
for iii in range(3):
@@ -125,11 +128,11 @@ class ControlsForm(QWidget):
widge.setCurrentIndex(0)
widge.setEditable(True)
widge.setObjectName(f"{rows[iii]} : {columns[jjj]}")
self.layout.addWidget(widge, iii+1, jjj+2, 1, 1)
self.layout.addWidget(QLabel("Comments:"), 0,5,1,1)
self.layout.addWidget(widge, iii + 1, jjj + 2, 1, 1)
self.layout.addWidget(QLabel("Comments:"), 0, 5, 1, 1)
self.comment_field = QTextEdit(self)
self.comment_field.setFixedHeight(50)
self.layout.addWidget(self.comment_field, 1,5,4,1)
self.layout.addWidget(self.comment_field, 1, 5, 4, 1)
self.setLayout(self.layout)
def parse_form(self) -> List[dict]:
@@ -138,12 +141,12 @@ class ControlsForm(QWidget):
Returns:
List[dict]: output of values
"""
"""
output = []
for le in self.findChildren(QComboBox):
label = [item.strip() for item in le.objectName().split(" : ")]
try:
dicto = [item for item in output if item['name']==label[0]][0]
dicto = [item for item in output if item['name'] == label[0]][0]
except IndexError:
dicto = dict(name=label[0], values=[])
dicto['values'].append(dict(name=label[1], value=le.currentText()))

View File

@@ -8,7 +8,7 @@ from PyQt6.QtWidgets import (
QDialogButtonBox, QDateEdit, QPushButton, QFormLayout
)
from PyQt6.QtCore import Qt, QDate
from tools import jinja_template_loading, Settings
from tools import jinja_template_loading
from backend.db.models import *
import logging
from .pop_ups import AlertPop
@@ -45,18 +45,19 @@ class AddReagentForm(QDialog):
self.exp_input.setObjectName('expiry')
# NOTE: if expiry is not passed in from gui, use today
if expiry is None:
self.exp_input.setDate(QDate.currentDate())
# self.exp_input.setDate(QDate.currentDate())
self.exp_input.setDate(QDate(1970, 1, 1))
else:
try:
self.exp_input.setDate(expiry)
except TypeError:
self.exp_input.setDate(QDate.currentDate())
self.exp_input.setDate(QDate(1970, 1, 1))
# NOTE: widget to get reagent type info
self.type_input = QComboBox()
self.type_input.setObjectName('type')
self.type_input.addItems([item.name for item in ReagentRole.query()])
# logger.debug(f"Trying to find index of {reagent_type}")
# NOTE: convert input to user friendly string?
# NOTE: convert input to user-friendly string?
try:
reagent_role = reagent_role.replace("_", " ").title()
except AttributeError:

View File

@@ -7,8 +7,8 @@ from PyQt6.QtWidgets import QTableView, QMenu
from PyQt6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel
from PyQt6.QtGui import QAction, QCursor
from backend.db.models import BasicSubmission
from backend.excel import make_report_html, make_report_xlsx, ReportMaker
from tools import Report, Result, row_map, get_first_blank_df_row, html_to_pdf
from backend.excel import ReportMaker
from tools import Report, Result, report_result
from .functions import select_save_file, select_open_file
from .misc import ReportDatePicker
import pandas as pd
@@ -129,14 +129,15 @@ class SubmissionsSheet(QTableView):
func = self.con_actions[action_name]
func(obj=self)
@report_result
def link_extractions(self):
"""
Pull extraction logs into the db
"""
self.link_extractions_function()
self.app.report.add_result(self.report)
"""
self.report = Report()
self.app.result_reporter()
self.link_extractions_function()
self.report.add_result(self.report)
return self.report
def link_extractions_function(self):
"""
@@ -179,6 +180,7 @@ class SubmissionsSheet(QTableView):
sub.save()
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
@report_result
def link_pcr(self):
"""
Pull pcr logs into the db
@@ -186,7 +188,7 @@ class SubmissionsSheet(QTableView):
self.link_pcr_function()
self.app.report.add_result(self.report)
self.report = Report()
self.app.result_reporter()
return self.report
def link_pcr_function(self):
"""
@@ -225,15 +227,15 @@ class SubmissionsSheet(QTableView):
# NOTE: check if pcr_info already exists
sub.save()
self.report.add_result(Result(msg=f"We added {count} logs to the database.", status='Information'))
@report_result
def generate_report(self):
"""
Make a report
"""
self.generate_report_function()
self.app.report.add_result(self.report)
"""
self.report = Report()
self.app.result_reporter()
self.generate_report_function()
return self.report
def generate_report_function(self):
"""
@@ -250,43 +252,7 @@ class SubmissionsSheet(QTableView):
dlg = ReportDatePicker()
if dlg.exec():
info = dlg.parse_form()
# logger.debug(f"Report info: {info}")
# NOTE: find submissions based on date range
subs = BasicSubmission.query(start_date=info['start_date'], end_date=info['end_date'])
# NOTE: convert each object to dict
records = [item.to_dict(report=True) for item in subs]
# logger.debug(f"Records: {pformat(records)}")
# NOTE: make dataframe from record dictionaries
detailed_df, summary_df = make_report_xlsx(records=records)
html = make_report_html(df=summary_df, start_date=info['start_date'], end_date=info['end_date'])
# NOTE: get save location of report
fname = select_save_file(obj=self, default_name=f"Submissions_Report_{info['start_date']}-{info['end_date']}.docx", extension="docx")
# html_to_pdf(html=html, output_file=fname)
# writer = pd.ExcelWriter(fname.with_suffix(".xlsx"), engine='openpyxl')
# summary_df.to_excel(writer, sheet_name="Report")
# detailed_df.to_excel(writer, sheet_name="Details", index=False)
# worksheet: Worksheet = writer.sheets['Report']
# for idx, col in enumerate(summary_df, start=1): # loop through all columns
# series = summary_df[col]
# max_len = max((
# series.astype(str).map(len).max(), # len of largest item
# len(str(series.name)) # len of column name/header
# )) + 20 # adding a little extra space
# try:
# # NOTE: Convert idx to letter
# col_letter = chr(ord('@') + idx)
# worksheet.column_dimensions[col_letter].width = max_len
# except ValueError:
# pass
# blank_row = get_first_blank_df_row(summary_df) + 1
# # logger.debug(f"Blank row index = {blank_row}")
# for col in range(3,6):
# col_letter = row_map[col]
# worksheet.cell(row=blank_row, column=col, value=f"=SUM({col_letter}2:{col_letter}{str(blank_row-1)})")
# for cell in worksheet['D']:
# if cell.row > 1:
# cell.style = 'Currency'
# writer.close()
rp = ReportMaker(start_date=info['start_date'], end_date=info['end_date'])
rp.write_report(filename=fname, obj=self)
self.report.add_result(report)

View File

@@ -11,7 +11,7 @@ from pathlib import Path
from . import select_open_file, select_save_file
import logging, difflib, inspect
from pathlib import Path
from tools import Report, Result, check_not_nan, workbook_2_csv, main_form_style
from tools import Report, Result, check_not_nan, workbook_2_csv, main_form_style, report_result
from backend.excel.parser import SheetParser
from backend.validators import PydSubmission, PydReagent
from backend.db import (
@@ -59,17 +59,16 @@ class SubmissionFormContainer(QWidget):
self.app.last_dir = fname.parent
self.import_drag.emit(fname)
@report_result
def importSubmission(self, fname: Path | None = None):
"""
import submission from excel sheet into form
"""
self.app.raise_()
self.app.activateWindow()
self.import_submission_function(fname)
# logger.debug(f"Result from result reporter: {self.report.results}")
self.app.report.add_result(self.report)
self.report = Report()
self.app.result_reporter()
self.import_submission_function(fname)
return self.report
def import_submission_function(self, fname: Path | None = None):
"""
@@ -115,8 +114,9 @@ class SubmissionFormContainer(QWidget):
# logger.debug(f"Outgoing report: {self.report.results}")
# logger.debug(f"All attributes of submission container:\n{pformat(self.__dict__)}")
@report_result
def add_reagent(self, reagent_lot: str | None = None, reagent_role: str | None = None, expiry: date | None = None,
name: str | None = None):
name: str | None = None) -> Tuple[PydReagent, Report]:
"""
Action to create new reagent in DB.
@@ -144,16 +144,18 @@ class SubmissionFormContainer(QWidget):
sqlobj, assoc, result = reagent.toSQL()
sqlobj.save()
report.add_result(result)
self.app.report.add_result(report)
self.app.result_reporter()
return reagent
# logger.debug(f"Reagent: {reagent}, Report: {report}")
return reagent, report
class SubmissionFormWidget(QWidget):
def __init__(self, parent: QWidget, submission: PydSubmission) -> None:
def __init__(self, parent: QWidget, submission: PydSubmission, disable: list | None = None) -> None:
super().__init__(parent)
# self.report = Report()
# logger.debug(f"Disable: {disable}")
if disable is None:
disable = []
self.app = parent.app
self.pyd = submission
self.missing_info = []
@@ -166,12 +168,19 @@ class SubmissionFormWidget(QWidget):
for k in list(self.pyd.model_fields.keys()) + list(self.pyd.model_extra.keys()):
if k in self.ignore:
continue
try:
# logger.debug(f"Key: {k}, Disable: {disable}")
check = k in disable
# logger.debug(f"Check: {check}")
except TypeError:
check = False
try:
value = self.pyd.__getattribute__(k)
except AttributeError:
logger.error(f"Couldn't get attribute from pyd: {k}")
value = dict(value=None, missing=True)
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'], sub_obj=st)
add_widget = self.create_widget(key=k, value=value, submission_type=self.pyd.submission_type['value'],
sub_obj=st, disable=check)
if add_widget is not None:
self.layout.addWidget(add_widget)
if k == "extraction_kit":
@@ -180,11 +189,13 @@ class SubmissionFormWidget(QWidget):
self.scrape_reagents(self.pyd.extraction_kit)
def create_widget(self, key: str, value: dict | PydReagent, submission_type: str | None = None,
extraction_kit: str | None = None, sub_obj:BasicSubmission|None=None) -> "self.InfoItem":
extraction_kit: str | None = None, sub_obj: BasicSubmission | None = None,
disable: bool = False) -> "self.InfoItem":
"""
Make an InfoItem widget to hold a field
Args:
disable ():
key (str): Name of the field
value (dict): Value of field
submission_type (str | None, optional): Submissiontype as str. Defaults to None.
@@ -192,18 +203,25 @@ class SubmissionFormWidget(QWidget):
Returns:
self.InfoItem: Form widget to hold name:value
"""
# logger.debug(f"Key: {key}, Disable: {disable}")
if key not in self.ignore:
match value:
case PydReagent():
if value.name.lower() != "not applicable":
widget = self.ReagentFormWidget(self, reagent=value, extraction_kit=extraction_kit)
else:
widget = None
case _:
widget = self.InfoItem(self, key=key, value=value, submission_type=submission_type, sub_obj=sub_obj)
# logger.debug(f"Setting widget enabled to: {not disable}")
if disable:
widget.input.setEnabled(False)
widget.input.setToolTip("Widget disabled to protect database integrity.")
return widget
return None
@report_result
def scrape_reagents(self, *args, **kwargs): #extraction_kit:str, caller:str|None=None):
"""
Extracted scrape reagents function that will run when
@@ -250,8 +268,7 @@ class SubmissionFormWidget(QWidget):
self.layout.addWidget(submit_btn)
submit_btn.clicked.connect(self.submit_new_sample_function)
self.setLayout(self.layout)
self.app.report.add_result(report)
self.app.result_reporter()
return report
def clear_form(self):
"""
@@ -275,7 +292,8 @@ class SubmissionFormWidget(QWidget):
query = [widget for widget in query if widget.objectName() == object_name]
return query
def submit_new_sample_function(self) -> QWidget:
@report_result
def submit_new_sample_function(self, *args) -> Report:
"""
Parse forms and add sample to the database.
@@ -294,37 +312,40 @@ class SubmissionFormWidget(QWidget):
_, result = self.pyd.check_kit_integrity()
report.add_result(result)
if len(result.results) > 0:
self.app.report.add_result(report)
self.app.result_reporter()
# self.app.report.add_result(report)
# self.app.report_result()
return
# logger.debug(f"PYD before transformation into SQL:\n\n{self.pyd}\n\n")
base_submission, result = self.pyd.to_sql()
# logger.debug(f"SQL object: {pformat(base_submission.__dict__)}")
# logger.debug(f"Base submission: {base_submission.to_dict()}")
# NOTE: check output message for issues
match result.code:
try:
code = report.results[-1].code
except IndexError:
code = 0
match code:
# NOTE: code 0: everything is fine.
case 0:
report.add_result(None)
pass
# NOTE: code 1: ask for overwrite
case 1:
dlg = QuestionAsker(title=f"Review {base_submission.rsl_plate_num}?", message=result.msg)
if dlg.exec():
# NOTE: Do not add duplicate reagents.
result = None
pass
else:
self.app.ctx.database_session.rollback()
report.add_result(Result(msg="Overwrite cancelled", status="Information"))
self.app.report.add_result(report)
self.app.result_reporter()
return
# self.app.report.add_result(report)
# self.app.report_result()
return report
# NOTE: code 2: No RSL plate number given
case 2:
report.add_result(result)
self.app.report.add_result(report)
self.app.result_reporter()
return
# self.app.report.add_result(report)
# self.app.report_result()
return report
case _:
pass
# NOTE: add reagents to submission object
@@ -338,8 +359,7 @@ class SubmissionFormWidget(QWidget):
# NOTE: reset form
self.setParent(None)
# logger.debug(f"All attributes of obj: {pformat(self.__dict__)}")
self.app.report.add_result(report)
self.app.result_reporter()
return report
def export_csv_function(self, fname: Path | None = None):
"""
@@ -352,7 +372,6 @@ class SubmissionFormWidget(QWidget):
fname = select_save_file(obj=self, default_name=self.pyd.construct_filename(), extension="csv")
try:
self.pyd.export_csv(fname)
# workbook_2_csv(worksheet=self.pyd.csv, filename=fname)
except PermissionError:
logger.warning(f"Could not get permissions to {fname}. Possibly the request was cancelled.")
except AttributeError:
@@ -398,11 +417,13 @@ class SubmissionFormWidget(QWidget):
class InfoItem(QWidget):
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None, sub_obj:BasicSubmission|None=None) -> None:
def __init__(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None,
sub_obj: BasicSubmission | None = None) -> None:
super().__init__(parent)
layout = QVBoxLayout()
self.label = self.ParsedQLabel(key=key, value=value)
self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type, sub_obj=sub_obj)
self.input: QWidget = self.set_widget(parent=self, key=key, value=value, submission_type=submission_type,
sub_obj=sub_obj)
self.setObjectName(key)
try:
self.missing: bool = value['missing']
@@ -439,7 +460,8 @@ class SubmissionFormWidget(QWidget):
return None, None
return self.input.objectName(), dict(value=value, missing=self.missing)
def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None, sub_obj:BasicSubmission|None=None) -> QWidget:
def set_widget(self, parent: QWidget, key: str, value: dict, submission_type: str | None = None,
sub_obj: BasicSubmission | None = None) -> QWidget:
"""
Creates form widget
@@ -472,6 +494,7 @@ class SubmissionFormWidget(QWidget):
pass
# set combobox values to lookedup values
add_widget.addItems(labs)
add_widget.setToolTip("Select submitting lab.")
case 'extraction_kit':
# if extraction kit not available, all other values fail
if not check_not_nan(value):
@@ -493,15 +516,7 @@ class SubmissionFormWidget(QWidget):
logger.error(f"Couldn't find {obj.prsr.sub['extraction_kit']}")
obj.ext_kit = uses[0]
add_widget.addItems(uses)
# case 'submitted_date':
# # NOTE: uses base calendar
# add_widget = QDateEdit(calendarPopup=True)
# # NOTE: sets submitted date based on date found in excel sheet
# try:
# add_widget.setDate(value)
# # NOTE: if not found, use today
# except:
# add_widget.setDate(date.today())
add_widget.setToolTip("Select extraction kit.")
case 'submission_category':
add_widget = QComboBox()
cats = ['Diagnostic', "Surveillance", "Research"]
@@ -511,6 +526,7 @@ class SubmissionFormWidget(QWidget):
except ValueError:
cats.insert(0, cats.pop(cats.index(submission_type)))
add_widget.addItems(cats)
add_widget.setToolTip("Enter submission category or select from list.")
case _:
if key in sub_obj.timestamps():
add_widget = QDateEdit(calendarPopup=True)
@@ -520,11 +536,13 @@ class SubmissionFormWidget(QWidget):
# NOTE: if not found, use today
except:
add_widget.setDate(date.today())
add_widget.setToolTip(f"Select date for {key}")
else:
# NOTE: anything else gets added in as a line edit
add_widget = QLineEdit()
# logger.debug(f"Setting widget text to {str(value).replace('_', ' ')}")
add_widget.setText(str(value).replace("_", " "))
add_widget.setToolTip(f"Enter value for {key}")
if add_widget is not None:
add_widget.setObjectName(key)
add_widget.setParent(parent)
@@ -594,13 +612,14 @@ class SubmissionFormWidget(QWidget):
# NOTE: If changed set self.missing to True and update self.label
self.lot.currentTextChanged.connect(self.updated)
def parse_form(self) -> Tuple[PydReagent, dict]:
def parse_form(self) -> Tuple[PydReagent | None, Report]:
"""
Pulls form info into PydReagent
Returns:
Tuple[PydReagent, dict]: PydReagent and Report(?)
"""
report = Report()
lot = self.lot.currentText()
# logger.debug(f"Using this lot for the reagent {self.reagent}: {lot}")
wanted_reagent = Reagent.query(lot_number=lot, reagent_role=self.reagent.role)
@@ -609,14 +628,16 @@ class SubmissionFormWidget(QWidget):
dlg = QuestionAsker(title=f"Add {lot}?",
message=f"Couldn't find reagent type {self.reagent.role}: {lot} in the database.\n\nWould you like to add it?")
if dlg.exec():
wanted_reagent = self.parent().parent().add_reagent(reagent_lot=lot, reagent_role=self.reagent.role,
expiry=self.reagent.expiry,
name=self.reagent.name)
return wanted_reagent, None
wanted_reagent, _ = self.parent().parent().add_reagent(reagent_lot=lot,
reagent_role=self.reagent.role,
expiry=self.reagent.expiry,
name=self.reagent.name)
return wanted_reagent, report
else:
# NOTE: In this case we will have an empty reagent and the submission will fail kit integrity check
# logger.debug("Will not add reagent.")
return None, Result(msg="Failed integrity check", status="Critical")
report.add_result(Result(msg="Failed integrity check", status="Critical"))
return None, report
else:
# NOTE: Since this now gets passed in directly from the parser -> pyd -> form and the parser gets the name
# from the db, it should no longer be necessary to query the db with reagent/kit, but with rt name directly.
@@ -624,7 +645,7 @@ class SubmissionFormWidget(QWidget):
if rt is None:
rt = ReagentRole.query(kit_type=self.extraction_kit, reagent=wanted_reagent)
return PydReagent(name=wanted_reagent.name, lot=wanted_reagent.lot, role=rt.name,
expiry=wanted_reagent.expiry, missing=False), None
expiry=wanted_reagent.expiry, missing=False), report
def updated(self):
"""
@@ -708,4 +729,5 @@ class SubmissionFormWidget(QWidget):
# logger.debug(f"New relevant reagents: {relevant_reagents}")
self.setObjectName(f"lot_{reagent.role}")
self.addItems(relevant_reagents)
self.setToolTip(f"Enter lot number for the reagent used for {reagent.role}")
# self.setStyleSheet(main_form_style)

View File

@@ -4,6 +4,8 @@ Contains miscellaenous functions used by both frontend and backend.
from __future__ import annotations
import json
from json import JSONDecodeError
import jinja2
import numpy as np
import logging, re, yaml, sys, os, stat, platform, getpass, inspect, csv
import pandas as pd
@@ -18,7 +20,6 @@ from typing import Any, Tuple, Literal, List
from PyQt6.QtGui import QPageSize
from PyQt6.QtWebEngineWidgets import QWebEngineView
from openpyxl.worksheet.worksheet import Worksheet
# from PyQt6 import QtPrintSupport, QtCore, QtWebEngineWidgets
from PyQt6.QtPrintSupport import QPrinter
logger = logging.getLogger(f"submissions.{__name__}")
@@ -74,7 +75,7 @@ def check_key_or_attr(key: str, interest: dict | object, check_none: bool = Fals
Returns:
bool: True if exists, else False
"""
"""
match interest:
case dict():
if key in interest.keys():
@@ -175,7 +176,7 @@ def is_missing(value: Any) -> Tuple[Any, bool]:
Returns:
Tuple[Any, bool]: Value, True if nan, else False
"""
"""
if check_not_nan(value):
return value, False
else:
@@ -222,7 +223,11 @@ class Settings(BaseSettings, extra="allow"):
FileNotFoundError: Error if database not found.
"""
database_schema: str
directory_path: Path
database_user: str | None = None
database_password: str | None = None
database_name: str
database_path: Path | str | None = None
backup_path: Path | str | None = None
# super_users: list|None = None
@@ -260,17 +265,26 @@ class Settings(BaseSettings, extra="allow"):
@field_validator('database_path', mode="before")
@classmethod
def ensure_database_exists(cls, value, values):
if value == ":memory:":
return value
match value:
case str():
value = Path(value)
case None:
value = values.data['directory_path'].joinpath("submissions.db")
if value.exists():
return value
else:
raise FileNotFoundError(f"Couldn't find database at {value}")
# if value == ":memory:":
# return value
match values.data['database_schema']:
case "sqlite":
value = f"/{Path(value).absolute().__str__()}/{values.data['database_name']}.db"
# db_name = f"{values.data['database_name']}.db"
case _:
value = f"@{value}/{values.data['database_name']}"
# db_name = values.data['database_name']
# match value:
# case str():
# value = Path(value)
# case None:
# value = values.data['directory_path'].joinpath("submissions.db")
# if value.exists():
# return value
# else:
# raise FileNotFoundError(f"Couldn't find database at {value}")
return value
@field_validator('database_session', mode="before")
@classmethod
@@ -278,27 +292,33 @@ class Settings(BaseSettings, extra="allow"):
if value is not None:
return value
else:
database_path = values.data['database_path']
if database_path is None:
# NOTE: check in user's .submissions directory for submissions.db
if Path.home().joinpath(".submissions", "submissions.db").exists():
database_path = Path.home().joinpath(".submissions", "submissions.db")
# NOTE: finally, look in the local dir
else:
database_path = package_dir.joinpath("submissions.db")
else:
if database_path == ":memory:":
pass
# NOTE: check if user defined path is directory
elif database_path.is_dir():
database_path = database_path.joinpath("submissions.db")
# NOTE: check if user defined path is a file
elif database_path.is_file():
database_path = database_path
else:
raise FileNotFoundError("No database file found. Exiting program.")
template = jinja_template_loading().from_string(
"{{ values['database_schema'] }}://{% if values['database_user'] %}{{ values['database_user'] }}{% if values['database_password'] %}:{{ values['database_password'] }}{% endif %}{% endif %}{{ values['database_path'] }}")
database_path = template.render(values=values.data)
# print(f"Using {database_path} for database path")
# database_path = values.data['database_path']
# if database_path is None:
# # NOTE: check in user's .submissions directory for submissions.db
# if Path.home().joinpath(".submissions", "submissions.db").exists():
# database_path = Path.home().joinpath(".submissions", "submissions.db")
# # NOTE: finally, look in the local dir
# else:
# database_path = package_dir.joinpath("submissions.db")
# else:
# if database_path == ":memory:":
# pass
# # NOTE: check if user defined path is directory
# elif database_path.is_dir():
# database_path = database_path.joinpath("submissions.db")
# # NOTE: check if user defined path is a file
# elif database_path.is_file():
# database_path = database_path
# else:
# raise FileNotFoundError("No database file found. Exiting program.")
logger.info(f"Using {database_path} for database file.")
engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
# engine = create_engine(f"sqlite:///{database_path}") #, echo=True, future=True)
# engine = create_engine("postgresql+psycopg2://postgres:RE,4321q@localhost:5432/submissions")
engine = create_engine(database_path)
session = Session(engine)
return session
@@ -316,13 +336,21 @@ class Settings(BaseSettings, extra="allow"):
def set_from_db(self, db_path: Path):
if 'pytest' in sys.modules:
config_items = dict(power_users=['lwark', 'styson', 'ruwang'])
output = dict(power_users=['lwark', 'styson', 'ruwang'])
else:
session = Session(create_engine(f"sqlite:///{db_path}"))
# session = Session(create_engine(f"sqlite:///{db_path}"))
session = self.database_session
config_items = session.execute(text("SELECT * FROM _configitem")).all()
session.close()
config_items = {item[1]: json.loads(item[2]) for item in config_items}
for k, v in config_items.items():
# print(config_items)
output = {}
for item in config_items:
try:
output[item[1]] = json.loads(item[2])
except (JSONDecodeError, TypeError):
output[item[1]] = item[2]
# config_items = {item[1]: json.loads(item[2]) for item in config_items}
for k, v in output.items():
if not hasattr(self, k):
self.__setattr__(k, v)
@@ -355,7 +383,6 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
CONFIGDIR.mkdir(parents=True)
except FileExistsError:
logger.warning(f"Config directory {CONFIGDIR} already exists.")
try:
LOGDIR.mkdir(parents=True)
except FileExistsError:
@@ -373,7 +400,7 @@ def get_config(settings_path: Path | str | None = None) -> Settings:
if check_if_app():
settings_path = Path(sys._MEIPASS).joinpath("files", "config.yml")
else:
settings_path = package_dir.joinpath('config.yml')
settings_path = package_dir.joinpath('src', 'config.yml')
with open(settings_path, "r") as dset:
default_settings = yaml.load(dset, Loader=yaml.Loader)
# NOTE: Tell program we need to copy the config.yml to the user directory
@@ -502,7 +529,7 @@ def setup_logger(verbosity: int = 3):
# NOTE: create console handler with a higher log level
# NOTE: create custom logger with STERR -> log
ch = logging.StreamHandler(stream=sys.stdout)
# NOTE: set looging level based on verbosity
# NOTE: set logging level based on verbosity
match verbosity:
case 3:
ch.setLevel(logging.DEBUG)
@@ -542,10 +569,10 @@ def copy_settings(settings_path: Path, settings: dict) -> dict:
dict: output dictionary for use in first run
"""
# NOTE: if the current user is not a superuser remove the superusers entry
if not getpass.getuser() in settings['super_users']:
del settings['super_users']
if not getpass.getuser() in settings['power_users']:
del settings['power_users']
# if not getpass.getuser() in settings['super_users']:
# del settings['super_users']
# if not getpass.getuser() in settings['power_users']:
# del settings['power_users']
if not settings_path.exists():
with open(settings_path, 'w') as f:
yaml.dump(settings, f)
@@ -651,7 +678,7 @@ class Report(BaseModel):
Args:
result (Result | Report | None): Results to be added.
"""
"""
match result:
case Result():
logger.info(f"Adding {result} to results.")
@@ -668,7 +695,7 @@ class Report(BaseModel):
logger.error(f"Unknown variable type: {type(result)} for <Result> entry into <Report>")
def rreplace(s:str, old:str, new:str) -> str:
def rreplace(s: str, old: str, new: str) -> str:
"""
Removes rightmost occurence of a substring
@@ -679,18 +706,18 @@ def rreplace(s:str, old:str, new:str) -> str:
Returns:
str: updated string
"""
"""
return (s[::-1].replace(old[::-1], new[::-1], 1))[::-1]
def html_to_pdf(html:str, output_file: Path | str):
def html_to_pdf(html: str, output_file: Path | str):
"""
Attempts to print an html string as a PDF. (currently not working)
Args:
html (str): Input html string.
output_file (Path | str): Output PDF file path.
"""
"""
if isinstance(output_file, str):
output_file = Path(output_file)
logger.debug(f"Printing PDF to {output_file}")
@@ -732,7 +759,7 @@ def workbook_2_csv(worksheet: Worksheet, filename: Path):
Args:
worksheet (Worksheet): Incoming worksheet
filename (Path): Output csv filepath.
"""
"""
with open(filename, 'w', newline="") as f:
c = csv.writer(f)
for r in worksheet.rows:
@@ -748,7 +775,7 @@ def is_power_user() -> bool:
Returns:
bool: True if yes, False if no.
"""
"""
try:
check = getpass.getuser() in ctx.power_users
except:
@@ -773,3 +800,32 @@ def check_authorization(func):
return dict(code=1, message="This user does not have permission for this function.", status="warning")
return wrapper
def report_result(func):
def wrapper(*args, **kwargs):
logger.debug(f"Arguments: {args}")
logger.debug(f"Keyword arguments: {kwargs}")
output = func(*args, **kwargs)
if isinstance(output, tuple):
report = [item for item in output if isinstance(item, Report)][0]
else:
report = None
logger.debug(f"Got report: {report}")
try:
results = report.results
except AttributeError:
logger.error("No results available")
results = []
for iii, result in enumerate(results):
logger.debug(f"Result {iii}: {result}")
try:
dlg = result.report()
dlg.exec()
except Exception as e:
logger.error(f"Problem reporting due to {e}")
logger.error(result.msg)
logger.debug(f"Returning: {output}")
return output
return wrapper