From 6394d5c73f7d7a96be278d011745ff93792afb05 Mon Sep 17 00:00:00 2001 From: Jeffrey Thiessen Date: Mon, 13 Jun 2022 17:25:31 -0500 Subject: [PATCH 1/6] add uniform file count validation --- docs/developers/objects.md | 2 + iridauploader/core/parsing_handler.py | 8 +- .../core/uniform_file_count_validator.py | 36 ++++++++ iridauploader/model/sequencing_run.py | 8 ++ .../core/test_uniform_file_count_validator.py | 84 +++++++++++++++++++ 5 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 iridauploader/core/uniform_file_count_validator.py create mode 100644 iridauploader/tests/core/test_uniform_file_count_validator.py diff --git a/docs/developers/objects.md b/docs/developers/objects.md index 0d0c2d81..99359eb1 100644 --- a/docs/developers/objects.md +++ b/docs/developers/objects.md @@ -14,6 +14,8 @@ It contains a `project_list` which relate to the IRIDA projects that samples wil The `metadata` dict is mostly unused, but must include `layoutType` as either `PAIRED_END` or `SINGLE_END`, this determines if the samples within the sequencing run are paired end or single end reads. +There is also a helper method `is_paired_end` that checks against the `metadata` dict and returns a boolean. + ### Project `model/project.py` The `Project` object relates to a project on IRIDA. diff --git a/iridauploader/core/parsing_handler.py b/iridauploader/core/parsing_handler.py index 242bbc53..a2439bb8 100644 --- a/iridauploader/core/parsing_handler.py +++ b/iridauploader/core/parsing_handler.py @@ -5,7 +5,7 @@ import iridauploader.config as config import iridauploader.parsers as parsers -from iridauploader.core import model_validator, file_size_validator +from iridauploader.core import model_validator, file_size_validator, uniform_file_count_validator def get_parser_from_config(): @@ -46,6 +46,12 @@ def parse_and_validate(directory): logging.info("parsing_handler:Exception while validating Sequencing Run") raise parsers.exceptions.ValidationError("Sequencing Run is not valid", validation_result) + logging.info("Validating file counts match sequencing run") + validation_result = uniform_file_count_validator.validate_uniform_file_count(sequencing_run) + if not validation_result.is_valid(): + logging.info("parsing_handler:Exception while validating Sequencing Run") + raise parsers.exceptions.ValidationError("Sequencing Run is not valid", validation_result) + logging.info("Validating files contain data") validation_result = file_size_validator.validate_file_size_minimum(sequencing_run) if not validation_result.is_valid(): diff --git a/iridauploader/core/uniform_file_count_validator.py b/iridauploader/core/uniform_file_count_validator.py new file mode 100644 index 00000000..9368baae --- /dev/null +++ b/iridauploader/core/uniform_file_count_validator.py @@ -0,0 +1,36 @@ +import iridauploader.model as model +from iridauploader.parsers.exceptions.sequence_file_error import SequenceFileError + + +def validate_uniform_file_count(sequencing_run): + """ + Validate the files in a SequencingRun object are all either in pairs, or single files + + :param sequencing_run: SequencingRun object to validate + :return: ValidationResult object with list of errors if any + """ + paired = sequencing_run.is_paired_end() + expected_file_count = 2 if paired else 1 + + validation_result = model.ValidationResult() + + for p in sequencing_run.project_list: + for s in p.sample_list: + # do validation of files + if not _matching_find_count(s, paired): + error_msg = "File count for sample `{}` does not match the expected file count `{}`. " \ + "Please verify your data.".format(s.sample_name, expected_file_count) + validation_result.add_error(SequenceFileError(error_msg)) + + return validation_result + + +def _matching_find_count(sample, paired): + """ + checks paired end / single end file matching on sample + + :param sample: Sample object + :param paired: boolean + :return: boolean + """ + return sample.sequence_file.is_paired_end() == paired diff --git a/iridauploader/model/sequencing_run.py b/iridauploader/model/sequencing_run.py index b736d842..e23f2369 100644 --- a/iridauploader/model/sequencing_run.py +++ b/iridauploader/model/sequencing_run.py @@ -71,3 +71,11 @@ def upload_route_string(self, sequencing_run_type): def get_dict(self): return self.__dict__ + + def is_paired_end(self): + """ + Checks the metadata field to see if run is paired end or single end + :return: boolean + """ + layout_type = self.metadata['layoutType'] + return True if layout_type == "PAIRED_END" else False diff --git a/iridauploader/tests/core/test_uniform_file_count_validator.py b/iridauploader/tests/core/test_uniform_file_count_validator.py new file mode 100644 index 00000000..c5770803 --- /dev/null +++ b/iridauploader/tests/core/test_uniform_file_count_validator.py @@ -0,0 +1,84 @@ +import unittest +from unittest.mock import patch +from os import path + + +from iridauploader.core import file_size_validator +from iridauploader import model + +path_to_module = path.abspath(path.dirname(__file__)) +if len(path_to_module) == 0: + path_to_module = '.' + + +class TestValidateFileSizeMinimum(unittest.TestCase): + """ + Testing the validate_file_size_minimum function + """ + + def setUp(self): + print("\nStarting " + self.__module__ + ": " + self._testMethodName) + + @staticmethod + def _make_seq_run(): + """ + Make a sequencing run pointed at real data for the tests + :return: SequencingRun object + """ + files_1 = model.SequenceFile([ + path.join(path_to_module, + "fake_ngs_data", "Data", "Intensities", "BaseCalls", "01-1111_S1_L001_R1_001.fastq.gz"), + path.join(path_to_module, + "fake_ngs_data", "Data", "Intensities", "BaseCalls", "01-1111_S1_L001_R2_001.fastq.gz"), + ]) + files_2 = model.SequenceFile([ + path.join(path_to_module, + "fake_ngs_data", "Data", "Intensities", "BaseCalls", "02-2222_S1_L001_R1_001.fastq.gz"), + path.join(path_to_module, + "fake_ngs_data", "Data", "Intensities", "BaseCalls", "02-2222_S1_L001_R2_001.fastq.gz"), + ]) + files_3 = model.SequenceFile([ + path.join(path_to_module, + "fake_ngs_data", "Data", "Intensities", "BaseCalls", "03-3333_S1_L001_R1_001.fastq.gz"), + path.join(path_to_module, + "fake_ngs_data", "Data", "Intensities", "BaseCalls", "03-3333_S1_L001_R2_001.fastq.gz"), + ]) + sample_1 = model.Sample("test_sample", "description", 1) + sample_1.sequence_file = files_1 + sample_2 = model.Sample("test_sample", "description", 1) + sample_2.sequence_file = files_2 + sample_3 = model.Sample("test_sample", "description", 1) + sample_3.sequence_file = files_3 + project = model.Project("test_project", [sample_1, sample_2, sample_3], "description") + sequencing_run = model.SequencingRun({"layoutType": "PAIRED_END"}, [project], "miseq") + return sequencing_run + + @patch("iridauploader.core.file_size_validator.config.read_config_option") + def test_files_too_small(self, mock_read_file_size_config_option): + # force the function to grab 100 (min file size) when it tries to call config + mock_read_file_size_config_option.side_effect = [100] + + # make a sequencing run to work with + sequencing_run = self._make_seq_run() + + # Run code to test + res = file_size_validator.validate_file_size_minimum(sequencing_run) + + # validate result + self.assertFalse(res.is_valid(), "File size is not too small") + print(res) + + @patch("iridauploader.core.file_size_validator.config.read_config_option") + def test_files_not_too_small(self, mock_read_file_size_config_option): + # force the function to grab 0 (min file size) when it tries to call config + mock_read_file_size_config_option.side_effect = [0] + + # make a sequencing run to work with + sequencing_run = self._make_seq_run() + + # Run code to test + res = file_size_validator.validate_file_size_minimum(sequencing_run) + + # validate result + self.assertTrue(res.is_valid(), "File size is too small") + print(res) From 7245a2912ef876213df066a090400c1ed44b431f Mon Sep 17 00:00:00 2001 From: Jeffrey Thiessen Date: Mon, 13 Jun 2022 17:27:46 -0500 Subject: [PATCH 2/6] fix broken test --- iridauploader/tests/core/test_parsing_handler.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/iridauploader/tests/core/test_parsing_handler.py b/iridauploader/tests/core/test_parsing_handler.py index 22d957eb..edf55540 100644 --- a/iridauploader/tests/core/test_parsing_handler.py +++ b/iridauploader/tests/core/test_parsing_handler.py @@ -48,7 +48,12 @@ def setUp(self): @patch("iridauploader.core.file_size_validator.validate_file_size_minimum") @patch("iridauploader.core.parsing_handler.model_validator.validate_sequencing_run") @patch("iridauploader.core.parsing_handler.get_parser_from_config") - def test_all_functions_called(self, mock_get_parser, mock_validate_model, mock_validate_file_size): + def test_all_functions_called( + self, + mock_get_parser, + mock_validate_model, + mock_validate_file_size, + mock_validate_uniform_file_count): """ Makes sure that all relevant functions are called so that it will parse and validate fully :return: @@ -65,6 +70,7 @@ def test_all_functions_called(self, mock_get_parser, mock_validate_model, mock_v mock_validate_model.side_effect = [mock_validation_result] mock_validate_file_size.side_effect = [mock_validation_result] + mock_validate_uniform_file_count.side_effect = [mock_validation_result] res = parsing_handler.parse_and_validate("mock_directory") From 16364d9e38307aeac3f9905c9b70109c9af036f8 Mon Sep 17 00:00:00 2001 From: Jeffrey Thiessen Date: Mon, 13 Jun 2022 17:49:57 -0500 Subject: [PATCH 3/6] bump version --- iridauploader/__init__.py | 2 +- setup.py | 2 +- windows-installer.cfg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iridauploader/__init__.py b/iridauploader/__init__.py index 2b30f9e9..b0c98798 100644 --- a/iridauploader/__init__.py +++ b/iridauploader/__init__.py @@ -1 +1 @@ -VERSION_NUMBER = "0.8.1" +VERSION_NUMBER = "0.8.2" diff --git a/setup.py b/setup.py index 9c244b95..d485422f 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setuptools.setup( name='iridauploader', - version='0.8.1', + version='0.8.2', description='IRIDA uploader: upload NGS data to IRIDA system', url='https://github.com/phac-nml/irida-uploader', author='Jeffrey Thiessen', diff --git a/windows-installer.cfg b/windows-installer.cfg index a676e43b..a8958e2f 100644 --- a/windows-installer.cfg +++ b/windows-installer.cfg @@ -1,6 +1,6 @@ [Application] name=IRIDA Uploader GUI -version=0.8.1 +version=0.8.2 entry_point=iridauploader.gui.gui:main icon=iridauploader/gui/images/icon.ico # Uncomment this to have a console show alongside the application From c9bd2032bfddde20b6cb1ab238faf1649237298a Mon Sep 17 00:00:00 2001 From: Jeffrey Thiessen Date: Mon, 13 Jun 2022 17:53:15 -0500 Subject: [PATCH 4/6] update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f99dd0d..6797eebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ Changes ======= +Beta 0.8.2 +---------- +Bug Fixes: +* Catch mixed paired end and single end files in a sequencing run at the validation step and show user which samples are incorrect. + Beta 0.8.1 --------- Bug Fixes: From 6ec7959c4ed8e0bcd11cfb833333a04f2c9f2202 Mon Sep 17 00:00:00 2001 From: Jeffrey Thiessen Date: Tue, 14 Jun 2022 13:03:04 -0500 Subject: [PATCH 5/6] fix existing test --- iridauploader/tests/core/test_parsing_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/iridauploader/tests/core/test_parsing_handler.py b/iridauploader/tests/core/test_parsing_handler.py index edf55540..2eec547a 100644 --- a/iridauploader/tests/core/test_parsing_handler.py +++ b/iridauploader/tests/core/test_parsing_handler.py @@ -45,6 +45,7 @@ class TestParseAndValidate(unittest.TestCase): def setUp(self): print("\nStarting " + self.__module__ + ": " + self._testMethodName) + @patch("iridauploader.core.uniform_file_count_validator.validate_uniform_file_count") @patch("iridauploader.core.file_size_validator.validate_file_size_minimum") @patch("iridauploader.core.parsing_handler.model_validator.validate_sequencing_run") @patch("iridauploader.core.parsing_handler.get_parser_from_config") @@ -66,7 +67,7 @@ def test_all_functions_called( mock_get_parser.side_effect = [mock_parser_instance] mock_validation_result = unittest.mock.MagicMock() - mock_validation_result.is_valid.side_effect = [True, True] + mock_validation_result.is_valid.side_effect = [True, True, True] mock_validate_model.side_effect = [mock_validation_result] mock_validate_file_size.side_effect = [mock_validation_result] From 0c76db3cd164e0cfd4927d2514a1027bf01bda65 Mon Sep 17 00:00:00 2001 From: Jeffrey Thiessen Date: Tue, 14 Jun 2022 14:15:56 -0500 Subject: [PATCH 6/6] add new tests --- .../BaseCalls/01-1111_S1_L001_R1_001.fastq.gz | Bin 0 -> 864 bytes .../BaseCalls/01-1111_S1_L001_R2_001.fastq.gz | Bin 0 -> 864 bytes .../BaseCalls/02-2222_S1_L001_R1_001.fastq.gz | Bin 0 -> 864 bytes .../02-2222x_S1_L001_R2_001.fastq.gz | Bin 0 -> 864 bytes .../BaseCalls/03-3333_S1_L001_R1_001.fastq.gz | Bin 0 -> 864 bytes .../BaseCalls/03-3333_S1_L001_R2_001.fastq.gz | Bin 0 -> 864 bytes .../fake_ngs_data_name_error/SampleSheet.csv | 24 +++ .../core/test_uniform_file_count_validator.py | 169 +++++++++++++++--- 8 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 iridauploader/tests/core/fake_ngs_data_name_error/Data/Intensities/BaseCalls/01-1111_S1_L001_R1_001.fastq.gz create mode 100644 iridauploader/tests/core/fake_ngs_data_name_error/Data/Intensities/BaseCalls/01-1111_S1_L001_R2_001.fastq.gz create mode 100644 iridauploader/tests/core/fake_ngs_data_name_error/Data/Intensities/BaseCalls/02-2222_S1_L001_R1_001.fastq.gz create mode 100644 iridauploader/tests/core/fake_ngs_data_name_error/Data/Intensities/BaseCalls/02-2222x_S1_L001_R2_001.fastq.gz create mode 100644 iridauploader/tests/core/fake_ngs_data_name_error/Data/Intensities/BaseCalls/03-3333_S1_L001_R1_001.fastq.gz create mode 100644 iridauploader/tests/core/fake_ngs_data_name_error/Data/Intensities/BaseCalls/03-3333_S1_L001_R2_001.fastq.gz create mode 100644 iridauploader/tests/core/fake_ngs_data_name_error/SampleSheet.csv diff --git a/iridauploader/tests/core/fake_ngs_data_name_error/Data/Intensities/BaseCalls/01-1111_S1_L001_R1_001.fastq.gz b/iridauploader/tests/core/fake_ngs_data_name_error/Data/Intensities/BaseCalls/01-1111_S1_L001_R1_001.fastq.gz new file mode 100644 index 0000000000000000000000000000000000000000..e117e5331094503cfbc3338546845055c8624100 GIT binary patch literal 864 zcmV-m1E2gKiwFpaxj0e)128r%GB`CgUsEw(OfWDpUs5q&FfcJLW?^%5aR9BA&vF_u z494#~MQ1p60`@LT6Iim5|42L2DJQ@Gvs9nt4e9id9O8hpmL=JsAD`a-gm^wZU&fb9 z*v1nLzn{Y;USbS7orlxu5~nczh?o0C?YrDsZbZ~FWywvbN!Fzh*+L&l*R2Uw4bD1r zUwsMA@X!c#&N|#BXIK!X!N{r)FzFd)79^48nUK6$AX#qp6C6dBSd*D8`^lL+PzZ-* zi`KOEK%y>q7d=|XYC}$3WYuNS1+iMO!}@PkZ#b;Bwm&s^ zAZAZeEsK6`S&HzMSETY-B`ta!b6(ff&#Qu@Jm`n#wI;#NI%purej(je(~WZ|C18HitOgGx1c~Gh9@Z=^ zZre5+29eSrU&JLDEEA)oR@HNz(n*YuX_^=hx>->)?6Wb$u&v3sNvT$>yrk-n%~7d{ zRcemM#d4pd@VE@h0uQHUNpcz!S3Mqil8ky2sg>qqD&sivFr?vlJObwVcwFWf=Q(Po zwA^$m9OEs<$Z9>r#F>?xIq>1|^mIKa4TtZ*W%?B{k1r2(A^*}*g>3Ag9Qc)W9U-el zb}0G}*=0rWgZAol;J}6R=QNp~h{+Ry9CDE{?ErDW!$M4fq{{_jw&VP)Y+^=TifH4* z5rnEh3GEi9X!@Wu&=+u_kzP@d#KlNp?(#y}r9q_vY%r_5n5Kv-!e9icdX$m1z>pE? z>Qnh41+Z#n)0B;*TC3CN@GaTa*st!#l{PBguob8^)hbus#OgVO5zkW zMYt8bW+=<$TX2cL&N@Foa1pDY7p*rID@5-E9R$-Jub6iWl-d=dx3cDa$ywR9yu9cT zy{$Eu5nrPHxj1zi(Q$f5B5#0bI9nFGRP(1yAEaR_y~-vITea5hNpdfp)lq7d=|XYC}$3WYuNS1+iMO!}@PkZ#b;Bwm&s^ zAZAZeEsK6`S&HzMSETY-B`ta!b6(ff&#Qu@Jm`n#wI;#NI%purej(je(~WZ|C18HitOgGx1c~Gh9@Z=^ zZre5+29eSrU&JLDEEA)oR@HNz(n*YuX_^=hx>->)?6Wb$u&v3sNvT$>yrk-n%~7d{ zRcemM#d4pd@VE@h0uQHUNpcz!S3Mqil8ky2sg>qqD&sivFr?vlJObwVcwFWf=Q(Po zwA^$m9OEs<$Z9>r#F>?xIq>1|^mIKa4TtZ*W%?B{k1r2(A^*}*g>3Ag9Qc)W9U-el zb}0G}*=0rWgZAol;J}6R=QNp~h{+Ry9CDE{?ErDW!$M4fq{{_jw&VP)Y+^=TifH4* z5rnEh3GEi9X!@Wu&=+u_kzP@d#KlNp?(#y}r9q_vY%r_5n5Kv-!e9icdX$m1z>pE? z>Qnh41+Z#n)0B;*TC3CN@GaTa*st!#l{PBguob8^)hbus#OgVO5zkW zMYt8bW+=<$TX2cL&N@Foa1pDY7p*rID@5-E9R$-Jub6iWl-d=dx3cDa$ywR9yu9cT zy{$Eu5nrPHxj1zi(Q$f5B5#0bI9nFGRP(1yAEaR_y~-vITea5hNpdfp)lq7d=|XYC}$3WYuNS1+iMO!}@PkZ#b;Bwm&s^ zAZAZeEsK6`S&HzMSETY-B`ta!b6(ff&#Qu@Jm`n#wI;#NI%purej(je(~WZ|C18HitOgGx1c~Gh9@Z=^ zZre5+29eSrU&JLDEEA)oR@HNz(n*YuX_^=hx>->)?6Wb$u&v3sNvT$>yrk-n%~7d{ zRcemM#d4pd@VE@h0uQHUNpcz!S3Mqil8ky2sg>qqD&sivFr?vlJObwVcwFWf=Q(Po zwA^$m9OEs<$Z9>r#F>?xIq>1|^mIKa4TtZ*W%?B{k1r2(A^*}*g>3Ag9Qc)W9U-el zb}0G}*=0rWgZAol;J}6R=QNp~h{+Ry9CDE{?ErDW!$M4fq{{_jw&VP)Y+^=TifH4* z5rnEh3GEi9X!@Wu&=+u_kzP@d#KlNp?(#y}r9q_vY%r_5n5Kv-!e9icdX$m1z>pE? z>Qnh41+Z#n)0B;*TC3CN@GaTa*st!#l{PBguob8^)hbus#OgVO5zkW zMYt8bW+=<$TX2cL&N@Foa1pDY7p*rID@5-E9R$-Jub6iWl-d=dx3cDa$ywR9yu9cT zy{$Eu5nrPHxj1zi(Q$f5B5#0bI9nFGRP(1yAEaR_y~-vITea5hNpdfp)lq7d=|XYC}$3WYuNS1+iMO!}@PkZ#b;Bwm&s^ zAZAZeEsK6`S&HzMSETY-B`ta!b6(ff&#Qu@Jm`n#wI;#NI%purej(je(~WZ|C18HitOgGx1c~Gh9@Z=^ zZre5+29eSrU&JLDEEA)oR@HNz(n*YuX_^=hx>->)?6Wb$u&v3sNvT$>yrk-n%~7d{ zRcemM#d4pd@VE@h0uQHUNpcz!S3Mqil8ky2sg>qqD&sivFr?vlJObwVcwFWf=Q(Po zwA^$m9OEs<$Z9>r#F>?xIq>1|^mIKa4TtZ*W%?B{k1r2(A^*}*g>3Ag9Qc)W9U-el zb}0G}*=0rWgZAol;J}6R=QNp~h{+Ry9CDE{?ErDW!$M4fq{{_jw&VP)Y+^=TifH4* z5rnEh3GEi9X!@Wu&=+u_kzP@d#KlNp?(#y}r9q_vY%r_5n5Kv-!e9icdX$m1z>pE? z>Qnh41+Z#n)0B;*TC3CN@GaTa*st!#l{PBguob8^)hbus#OgVO5zkW zMYt8bW+=<$TX2cL&N@Foa1pDY7p*rID@5-E9R$-Jub6iWl-d=dx3cDa$ywR9yu9cT zy{$Eu5nrPHxj1zi(Q$f5B5#0bI9nFGRP(1yAEaR_y~-vITea5hNpdfp)lq7d=|XYC}$3WYuNS1+iMO!}@PkZ#b;Bwm&s^ zAZAZeEsK6`S&HzMSETY-B`ta!b6(ff&#Qu@Jm`n#wI;#NI%purej(je(~WZ|C18HitOgGx1c~Gh9@Z=^ zZre5+29eSrU&JLDEEA)oR@HNz(n*YuX_^=hx>->)?6Wb$u&v3sNvT$>yrk-n%~7d{ zRcemM#d4pd@VE@h0uQHUNpcz!S3Mqil8ky2sg>qqD&sivFr?vlJObwVcwFWf=Q(Po zwA^$m9OEs<$Z9>r#F>?xIq>1|^mIKa4TtZ*W%?B{k1r2(A^*}*g>3Ag9Qc)W9U-el zb}0G}*=0rWgZAol;J}6R=QNp~h{+Ry9CDE{?ErDW!$M4fq{{_jw&VP)Y+^=TifH4* z5rnEh3GEi9X!@Wu&=+u_kzP@d#KlNp?(#y}r9q_vY%r_5n5Kv-!e9icdX$m1z>pE? z>Qnh41+Z#n)0B;*TC3CN@GaTa*st!#l{PBguob8^)hbus#OgVO5zkW zMYt8bW+=<$TX2cL&N@Foa1pDY7p*rID@5-E9R$-Jub6iWl-d=dx3cDa$ywR9yu9cT zy{$Eu5nrPHxj1zi(Q$f5B5#0bI9nFGRP(1yAEaR_y~-vITea5hNpdfp)lq7d=|XYC}$3WYuNS1+iMO!}@PkZ#b;Bwm&s^ zAZAZeEsK6`S&HzMSETY-B`ta!b6(ff&#Qu@Jm`n#wI;#NI%purej(je(~WZ|C18HitOgGx1c~Gh9@Z=^ zZre5+29eSrU&JLDEEA)oR@HNz(n*YuX_^=hx>->)?6Wb$u&v3sNvT$>yrk-n%~7d{ zRcemM#d4pd@VE@h0uQHUNpcz!S3Mqil8ky2sg>qqD&sivFr?vlJObwVcwFWf=Q(Po zwA^$m9OEs<$Z9>r#F>?xIq>1|^mIKa4TtZ*W%?B{k1r2(A^*}*g>3Ag9Qc)W9U-el zb}0G}*=0rWgZAol;J}6R=QNp~h{+Ry9CDE{?ErDW!$M4fq{{_jw&VP)Y+^=TifH4* z5rnEh3GEi9X!@Wu&=+u_kzP@d#KlNp?(#y}r9q_vY%r_5n5Kv-!e9icdX$m1z>pE? z>Qnh41+Z#n)0B;*TC3CN@GaTa*st!#l{PBguob8^)hbus#OgVO5zkW zMYt8bW+=<$TX2cL&N@Foa1pDY7p*rID@5-E9R$-Jub6iWl-d=dx3cDa$ywR9yu9cT zy{$Eu5nrPHxj1zi(Q$f5B5#0bI9nFGRP(1yAEaR_y~-vITea5hNpdfp)l