diff --git a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/commands.py b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/commands.py index f70b85c5c203dd6095863e9e3d36ce4bd1adfd9f..e726f4031e8c8225a112a33fea5aec19a43c5412 100644 --- a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/commands.py +++ b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/commands.py @@ -9,14 +9,14 @@ import warnings from typing import List from pymygdala import LogHandler, SendNRAOEvent +from s_code_project_updater import Telescope from schema.model import Author, Project from schema.pstmodel import Person, UserAuthentication from sqlalchemy import exc as sa_exc, asc, desc -### pytest can find these modules just fine from command line from support.capo import get_my_capo_config from support.logging import get_console_logger, LOG_MESSAGE_FORMATTER -from schema import ArchiveDBSession +from schema import ArchiveDBSession, ExecutionBlock from ._version import ___version___ as version from .project_fetcher import ArchiveProjectFetcher @@ -59,11 +59,10 @@ class ScodeProjectUpdater: self.set_minimum_properties_from_args(args_dict) return + self.project_code = args_dict['project'] self.stored_project = None _LOG.debug(f'{self.args}') - self.sc_project = self.scode_project_from_args(self.args) - self.capo_config = get_my_capo_config(profile=self.args.profile) try: self.archive_context = ArchiveDBSession('SDM', profile=self.capo_config.profile) @@ -269,9 +268,9 @@ class ScodeProjectUpdater: (including authors) according to the arguments passed in :return: ''' - fetcher = ArchiveProjectFetcher(self.profile) + fetcher = ArchiveProjectFetcher(self.args.profile) + self.project = fetcher.fetch_project(self.project_code) if self.is_fetch_only(): - self.project = fetcher.fetch_project(self.project_code) output = fetcher.build_project_info() try: [_LOG.info(line) for line in output] @@ -296,13 +295,12 @@ class ScodeProjectUpdater: if self.stored_project is None: self.exit_with_error('No project found for the project_code provided', 3) - if fetcher.is_alma: + if self.is_alma(): raise ValueError(f'{self.stored_project.project_code} ' f'is an ALMA project; update not permitted') if self.args.investigators: proposed_investigators = self.get_pst_users(self.args.investigators) - self.sc_project.investigators = proposed_investigators if len(proposed_investigators) == 0 or \ not len(self.args.investigators) == len(proposed_investigators): self.exit_with_error('One or more of the investigators you entered was not ' @@ -312,10 +310,8 @@ class ScodeProjectUpdater: if self.args.title: self.stored_project.title = self.args.title - self.sc_project.title = self.args.title if self.args.abstract: self.stored_project.abstract = self.args.abstract - self.sc_project.abstract = self.args.abstract if not self.args.dry: if not self.is_fetch_only(): @@ -327,13 +323,28 @@ class ScodeProjectUpdater: self.print_project() return self.stored_project + def is_alma(self): + ''' is this an alma project? ''' + with warnings.catch_warnings(): + # Suppress SQLAlchemy warnings + warnings.simplefilter("ignore", category=sa_exc.SAWarning) + + exec_block = self.archive_context.session.query(ExecutionBlock) \ + .filter(ExecutionBlock.project_code == self.project_code) \ + .filter(ExecutionBlock.telescope == Telescope.ALMA.value) \ + .first() + return exec_block is not None + + + def reindex_project(self): """ If we are not performing a dry run, and have made it this far without error, then we re-index the project so the updates will show up in the profile-mapped archive. :return: None """ - if not self.args.dry: + if not self.args.dry and not self.is_fetch_only() \ + and '_TEST_PROJECT' not in self.project_code: _LOG.info(f'Re-indexing project {self.args.project} to make changes available....') # Set up a LogHandler to record the fact we just made a change to this project. # We're adding it here, instead of earlier, because nothing we log earlier should be @@ -377,7 +388,6 @@ class ArchiveProject: self.title = title self.abstract = abstract self.investigators = author_pst_ids - self.is_alma = None options = [] options.append('-C') @@ -392,9 +402,6 @@ class ArchiveProject: options.append('--investigators') self.options = options - def set_alma(self, is_alma): - self.is_alma = is_alma - def make_args(self, is_dry): args = [] if is_dry: @@ -417,13 +424,6 @@ class ArchiveProject: return args - @staticmethod - def from_schema_project(project: Project, is_alma: bool): - to_return = ArchiveProject(project.project_code, project.title, - project.abstract, project.authors) - to_return.set_alma(is_alma) - return to_return - def is_arg(self, arg): return arg in self.options diff --git a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/project_fetcher.py b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/project_fetcher.py index 81fd02e0777247f88801d9303d80b7b1f73ff6c6..1c321e67d96333c698dc98471043749a5bc48ae7 100644 --- a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/project_fetcher.py +++ b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/project_fetcher.py @@ -4,7 +4,6 @@ import warnings from sqlalchemy import exc as sa_exc, asc, desc -## pytest can't find these from schema import ArchiveDBSession, create_session, ExecutionBlock from schema.model import Project, Author from support.capo import get_my_capo_config @@ -30,10 +29,13 @@ class ArchiveProjectFetcher: _LOG.error(f'An error occurred while creating a db context: {k_ex}') sys.exit(1) + def __exit__(self, exc_type, exc_val, exc_tb): + self.archive_context.close() + self.pst_context.close() + def fetch_project(self, project_code: str): - with warnings.catch_warnings(): # , self.archive_session, \ - # self.pst_session: + with warnings.catch_warnings(): # Suppress SQLAlchemy warnings warnings.simplefilter("ignore", category=sa_exc.SAWarning) """ @@ -48,7 +50,7 @@ class ArchiveProjectFetcher: raise AttributeError(f'project {project_code} not found') self.abstract = self.project.abstract - self.is_alma = self._is_alma() + # self.is_alma = self._is_alma() return self.project @@ -99,7 +101,7 @@ class ArchiveProjectFetcher: def _is_alma(self): ''' is this an alma project? ''' - with warnings.catch_warnings(): + with warnings.catch_warnings(), self.archive_context, self.pst_context: # Suppress SQLAlchemy warnings warnings.simplefilter("ignore", category=sa_exc.SAWarning) diff --git a/apps/cli/utilities/s_code_project_updater/test/test_projects.py b/apps/cli/utilities/s_code_project_updater/test/test_projects.py index 2d65610e997de39f72a4ff1cb2698f29d7c9a498..c6b8e9e60db5111f15cf8f4240431f83da636f3f 100644 --- a/apps/cli/utilities/s_code_project_updater/test/test_projects.py +++ b/apps/cli/utilities/s_code_project_updater/test/test_projects.py @@ -1,157 +1,205 @@ ''' fake projects to use in testing scode_project_updater ''' +import warnings + +from sqlalchemy import exc as sa_exc + from shared.schema.src.schema import Author, Project -class ScodeTestProject(Project): +class ScodeTestProject(): ''' fake s_code project ''' def __init__(self): - self.project_code = 'SCODE_TEST_PROJECT' - self.starttime = 58551.9380295139 - self.endtime = 58924.9589184028 - self.proprietary_duration = 365 - self.title = 'Copy of SK0442' - self.abstract = ' Lorem ipsum dolor sit amet, consectetur adipiscing ' \ - 'elit, sed do eiusmod tempor incididunt ut labore et ' \ - 'dolore magna aliqua. Ut enim ad minim veniam, quis ' \ - 'nostrud exercitation ullamco laboris nisi ut aliquip ' \ - 'ex ea commodo consequat. Duis aute irure dolor in ' \ - 'reprehenderit in voluptate velit esse cillum dolore ' \ - 'eu fugiat nulla pariatur. Excepteur sint occaecat ' \ - 'cupidatat non proident, sunt in culpa qui officia ' \ - 'deserunt mollit anim id est laborum.' - self.authors = [ - Author(project_code=self.project_code, - username='srandall', - firstname='Scott', - lastname='Randall', - pst_person_id='4686', - is_pi=True), - - Author(project_code=self.project_code, - username='s.giacintucci', - firstname='Simona', - lastname='Giacintucci', - pst_person_id='317', - is_pi=False), - - Author(project_code=self.project_code, - username='esch44', - firstname='Emma', - lastname='Schwartzman', - pst_person_id='11991', - is_pi=False), - - Author(project_code=self.project_code, - username='tclarke', - firstname='Tracy', - lastname='Clarke', - pst_person_id='341', - is_pi=False), - - ] - - -class ScienceTestProject(Project): + with warnings.catch_warnings(): + # Suppress SQLAlchemy warnings + warnings.simplefilter("ignore", category=sa_exc.SAWarning) + + + self.project_code = 'SCODE_TEST_PROJECT' + self.project = Project ( + project_code = self.project_code, + starttime = 58551.9380295139, + endtime = 58924.9589184028, + proprietary_duration = 365, + title = 'Copy of SK0442', + abstract = ' Lorem ipsum dolor sit amet, consectetur adipiscing ' \ + 'elit, sed do eiusmod tempor incididunt ut labore et ' \ + 'dolore magna aliqua. Ut enim ad minim veniam, quis ' \ + 'nostrud exercitation ullamco laboris nisi ut aliquip ' \ + 'ex ea commodo consequat. Duis aute irure dolor in ' \ + 'reprehenderit in voluptate velit esse cillum dolore ' \ + 'eu fugiat nulla pariatur. Excepteur sint occaecat ' \ + 'cupidatat non proident, sunt in culpa qui officia ' \ + 'deserunt mollit anim id est laborum.' + ) + self.project.authors = [ + Author(project_code=self.project_code, + username='srandall', + firstname='Scott', + lastname='Randall', + pst_person_id='4686', + is_pi=True), + + Author(project_code=self.project_code, + username='s.giacintucci', + firstname='Simona', + lastname='Giacintucci', + pst_person_id='317', + is_pi=False), + + Author(project_code=self.project_code, + username='esch44', + firstname='Emma', + lastname='Schwartzman', + pst_person_id='11991', + is_pi=False), + + Author(project_code=self.project_code, + username='tclarke', + firstname='Tracy', + lastname='Clarke', + pst_person_id='341', + is_pi=False), + + ] + + +class ScienceTestProject(): ''' fake VLA science project ''' def __init__(self): - self.project_code = 'VLA_TEST_PROJECT' - self.starttime = 56688.8103640046 - self.endtime = 56810.4840497685 - self.title = 'Copy of 13B-014 for testing' - self.abstract = 'Spurred by our surprising VLA discovery of the first ' \ - 'black holes in Milky Way ' \ - 'globular clusters, we propose an ambitious survey for both stellar-mass and ' \ - 'intermediate-mass black holes in globular clusters. ' \ - 'With well-defined selection criteria, our sample will allow the first statistical ' \ - 'determination of the presence of black holes in clusters. This survey will make an ' \ - 'immediate impact in a number of fields, including black hole demographics, ' \ - 'accretion physics, gravitational wave predictions, and globular cluster evolution.' - - self.authors = [ - Author(project_code=self.project_code, - username='jstrader', - firstname='Jay', - lastname='Strader', - pst_person_id='4064', - is_pi=True), - Author(project_code=self.project_code, - username='jcamj', - firstname='James', - lastname='Miller-Jones', - pst_person_id='490', - is_pi=False), - Author(project_code=self.project_code, - username='chomiuk', - firstname='Laura', - lastname='Chomiuk', - pst_person_id='701', - is_pi=False), - Author(project_code=self.project_code, - username='gsivakoff', - firstname='Gregory', - lastname='Sivakoff', - pst_person_id='834', - is_pi=False), - Author(project_code=self.project_code, - username='tjmaccarone', - firstname='Thomas', - lastname='Maccarone', - pst_person_id='887', - is_pi=False), - - Author(project_code=self.project_code, - username='anilseth', - firstname='Anil', - lastname='Setn', - pst_person_id='1197', - is_pi=False), - - Author(project_code=self.project_code, - username='Craig Heinke', - firstname='Craig', - lastname='Heinke', - pst_person_id='3729', - is_pi=False), - - Author(project_code=self.project_code, - username='evanoyola', - firstname='Eva', - lastname='Noyola', - pst_person_id='5532', - is_pi=False), - - ] - -class AlmaTestProject(Project): + with warnings.catch_warnings(): + # Suppress SQLAlchemy warnings + warnings.simplefilter("ignore", category=sa_exc.SAWarning) + + self.project_code = 'VLA_TEST_PROJECT' + self.project = Project( + project_code = self.project_code, + starttime=56688.8103640046, + endtime=56810.4840497685, + title='Copy of 13B-014 for testing', + abstract='Spurred by our surprising VLA discovery of the first ' \ + 'black holes in Milky Way ' \ + 'globular clusters, we propose an ambitious survey for both stellar-mass and ' \ + 'intermediate-mass black holes in globular clusters. ' \ + 'With well-defined selection criteria, our sample will allow the first statistical ' \ + 'determination of the presence of black holes in clusters. This survey will make an ' \ + 'immediate impact in a number of fields, including black hole demographics, ' \ + 'accretion physics, gravitational wave predictions, and globular cluster evolution.' + ) + self.project.authors = [ + Author(project_code=self.project_code, + username='jstrader', + firstname='Jay', + lastname='Strader', + pst_person_id='4064', + is_pi=True), + Author(project_code=self.project_code, + username='jcamj', + firstname='James', + lastname='Miller-Jones', + pst_person_id='490', + is_pi=False), + Author(project_code=self.project_code, + username='chomiuk', + firstname='Laura', + lastname='Chomiuk', + pst_person_id='701', + is_pi=False), + Author(project_code=self.project_code, + username='gsivakoff', + firstname='Gregory', + lastname='Sivakoff', + pst_person_id='834', + is_pi=False), + Author(project_code=self.project_code, + username='tjmaccarone', + firstname='Thomas', + lastname='Maccarone', + pst_person_id='887', + is_pi=False), + + Author(project_code=self.project_code, + username='anilseth', + firstname='Anil', + lastname='Setn', + pst_person_id='1197', + is_pi=False), + + Author(project_code=self.project_code, + username='Craig Heinke', + firstname='Craig', + lastname='Heinke', + pst_person_id='3729', + is_pi=False), + + Author(project_code=self.project_code, + username='evanoyola', + firstname='Eva', + lastname='Noyola', + pst_person_id='5532', + is_pi=False), + + ] + +class AlmaTestProject(): ''' fake ALMA project ''' def __init__(self): - self.project_code = 'ALMA_SSA_TEST_PROJECT' - self.starttime = 56799.3877155556 - self.endtime = 56799.4128683333 - self.proprietary_duration = 365 - self.title = 'Copy of 2012.1.00060.S for testing' - self.abstract = "Understanding the physical factors that control the " \ - "conversion of interstellar gas into stars " \ - "is of fundamental importance for both developing a predictive physical theory of star formation and understanding the evolution of galaxies from the earliest epochs of cosmic history to the present time. An important aspect of this question is the study of empirical relations that connect the star formation rate in a given region to local properties of the interstellar medium. An important example is the Schmidt-Kennicutt (KS) law for galaxies that relates the surface densities of the star formation rate and the surface densities of interstellar gas in a non-linear fashion. However, it is also known that there is a linear correlation between the total SFR in galaxies and the mass of dense molecular gas as traced by the high excitation HCN molecule. Contrary to the KS relation, this scaling relation suggests that the total SFR depends simply on the total amount of dense molecular gas in a star forming system. Recently, we have begun to test these scaling relations in the Galactic neighborhood where star formation rates can be much better constrained. We found that for local clouds the total SFR scales most directly, and linearly, with the total mass of high extinction (and dense) molecular gas. Furthermore, we found this linear scaling law between SFR and dense gas to extend and extrapolate directly and smoothly to external galaxies. Moreover, our observations also demonstrate that a KS type relation does not exist for molecular clouds in the Galactic neighborhood. This is a direct consequence of a well known scaling law between the mass and size of molecular clouds, Larson's third law. Overall, our results indicate that a linear scaling law, in which the total amount of dense gas controls the SFR, is the fundamental physical relation that connects star formation across the vast scales from individual GMCs to entire galaxies. Critical testing of these ideas require resolved observations of GMCs in external galaxies. Here we propose to use ALMA to evaluate star formation scaling laws in a nearby galaxy where we can obtain resolved observations of individual GMCs. This allows us to obtain observations of a larger sample of GMCs than is accessible in the Galactic neighborhood. An extensive APEX survey of HII regions in the nearby galaxy NGC 300 has provided us with a sample of 36 star-forming regions with CO(2-1) detections and 42 upper limits. We are currently working on obtaining star formation rates for these regions from multi-wavelength ancillary data including our Herschel observations. We propose to use ALMA's unequalled capabilities to obtain snapshot observations of 40 selected regions in CO(2-1) in order to make resolved measurements of cloud structure to obtain sizes and virial masses. As a pilot project, we also propose to observe the brightest subsample in HCN(1-0) as a dense-gas tracer. Our proposed ALMA CO observations will enable us to to test Larson's scaling laws in an external galaxy and to evaluate which formulation of the Schmidt law is the most meaningful and appropriate to apply to spiral galaxies, and in doing so refine Schmidt's original conjecture of a scaling relation between the rate of star formation and gas density." - self.authors = [ - Author(project_code=self.project_code, - username='clada', - firstname='Charles', - lastname='Lada', - pst_person_id=None, - is_pi=True), - - Author(project_code=self.project_code, - username='jforbrich', - firstname='Jan', - lastname='Forbrich', - pst_person_id=None, - is_pi=False), - Author(project_code=self.project_code, - username='cfaesi', - firstname='Christopher', - lastname='Faesi', - pst_person_id=None, - is_pi=False), - ] + with warnings.catch_warnings(): + # Suppress SQLAlchemy warnings + warnings.simplefilter("ignore", category=sa_exc.SAWarning) + + self.project_code = 'ALMA_TEST_PROJECT' + self.project = Project( + project_code=self.project_code, + starttime=56799.3877155556, + endtime=56799.4128683333, + proprietary_duration=365, + title='Copy of 2012.1.00060.S for testing', + abstract = "Understanding the physical factors that control the " \ + "conversion of interstellar gas into stars " \ + "is of fundamental importance for both developing a predictive physical theory of star formation and understanding the evolution of galaxies from the earliest epochs of cosmic history to the present time. An important aspect of this question is the study of empirical relations that connect the star formation rate in a given region to local properties of the interstellar medium. An important example is the Schmidt-Kennicutt (KS) law for galaxies that relates the surface densities of the star formation rate and the surface densities of interstellar gas in a non-linear fashion. However, it is also known that there is a linear correlation between the total SFR in galaxies and the mass of dense molecular gas as traced by the high excitation HCN molecule. Contrary to the KS relation, this scaling relation suggests that the total SFR depends simply on the total amount of dense molecular gas in a star forming system. Recently, we have begun to test these scaling relations in the Galactic neighborhood where star formation rates can be much better constrained. We found that for local clouds the total SFR scales most directly, and linearly, with the total mass of high extinction (and dense) molecular gas. Furthermore, we found this linear scaling law between SFR and dense gas to extend and extrapolate directly and smoothly to external galaxies. Moreover, our observations also demonstrate that a KS type relation does not exist for molecular clouds in the Galactic neighborhood. This is a direct consequence of a well known scaling law between the mass and size of molecular clouds, Larson's third law. Overall, our results indicate that a linear scaling law, in which the total amount of dense gas controls the SFR, is the fundamental physical relation that connects star formation across the vast scales from individual GMCs to entire galaxies. Critical testing of these ideas require resolved observations of GMCs in external galaxies. Here we propose to use ALMA to evaluate star formation scaling laws in a nearby galaxy where we can obtain resolved observations of individual GMCs. This allows us to obtain observations of a larger sample of GMCs than is accessible in the Galactic neighborhood. An extensive APEX survey of HII regions in the nearby galaxy NGC 300 has provided us with a sample of 36 star-forming regions with CO(2-1) detections and 42 upper limits. We are currently working on obtaining star formation rates for these regions from multi-wavelength ancillary data including our Herschel observations. We propose to use ALMA's unequalled capabilities to obtain snapshot observations of 40 selected regions in CO(2-1) in order to make resolved measurements of cloud structure to obtain sizes and virial masses. As a pilot project, we also propose to observe the brightest subsample in HCN(1-0) as a dense-gas tracer. Our proposed ALMA CO observations will enable us to to test Larson's scaling laws in an external galaxy and to evaluate which formulation of the Schmidt law is the most meaningful and appropriate to apply to spiral galaxies, and in doing so refine Schmidt's original conjecture of a scaling relation between the rate of star formation and gas density." + ) + self.project.authors = [ + Author(project_code=self.project_code, + username='clada', + firstname='Charles', + lastname='Lada', + pst_person_id=None, + is_pi=True), + + Author(project_code=self.project_code, + username='jforbrich', + firstname='Jan', + lastname='Forbrich', + pst_person_id=None, + is_pi=False), + Author(project_code=self.project_code, + username='cfaesi', + firstname='Christopher', + lastname='Faesi', + pst_person_id=None, + is_pi=False), + ] + +fake_projects = [ScodeTestProject().project, + ScienceTestProject().project, + AlmaTestProject().project] + +def get_test_project(project_code: str): + for project in fake_projects: + if project.project_code == project_code: + return project + return None + +def get_author_pst_ids(project: Project): + ''' build list of pst_person_ids for display; + we want the PI's pst_person_id followed by the CoIs' pst_person_ids + in numeric order + ''' + project_pi = project.authors[0] + coi_pst_ids = [int(coi.pst_person_id) for coi in project.authors[1:]] + coi_pst_ids = sorted(coi_pst_ids) + + author_pst_ids = [int(project_pi.pst_person_id)] + [author_pst_ids.append(id) for id in coi_pst_ids] + return [id for id in author_pst_ids] diff --git a/apps/cli/utilities/s_code_project_updater/test/test_updater.py b/apps/cli/utilities/s_code_project_updater/test/test_updater.py index 89cb5bb3cca603db1c74723f1e1c1b0da88f0ee3..e0137834b52f273ed0ab0b9df1cf4ccc3b4b6909 100644 --- a/apps/cli/utilities/s_code_project_updater/test/test_updater.py +++ b/apps/cli/utilities/s_code_project_updater/test/test_updater.py @@ -1,337 +1,313 @@ -''' unit/regression tests for s_code_project_updater ''' - import logging import os +import subprocess import unittest -import pytest - -from astropy.time import Time +import warnings -from s_code_project_updater.commands import \ - ScodeProjectUpdater -from schema import create_session, Project +import pytest +from s_code_project_updater.commands import UpdateException +from schema import create_session +from schema.model import Project from schema.pstmodel import Session +from sqlalchemy import exc as sa_exc from support.logging import get_console_logger -from .test_projects import AlmaTestProject, ScodeTestProject, ScienceTestProject +from .test_projects import get_author_pst_ids, ScodeTestProject, \ + ScienceTestProject, AlmaTestProject _LOG = get_console_logger("scode_project_updater_tests", logging.DEBUG) +_UPDATE_COMMAND = 'update_sproj' +PROFILE = 'local' + class UpdaterTestCase(unittest.TestCase): @classmethod def setUpClass(cls) -> None: - os.environ['CAPO_PROFILE'] = 'local' - - ### making sure fake-project creation works ### + os.environ['CAPO_PROFILE'] = PROFILE + cls.return_values = build_updater_return_values() - def test_create_scode_project(self): - session = None - project = None - try: - session = create_session('SDM') - except Exception as exc: - pytest.fail(f'failed to create session: {exc}') - - project = self.initialize_scode_test_project(session) - self.assertIsNotNone(project) - expected = ScodeTestProject() - self.assertEqual(expected.project_code, - project.project_code, - f'expected {expected.project_code}; got {project.project_code}') - num_authors_expected = len(expected.authors) - actual = len(project.authors) - self.assertEqual(num_authors_expected, actual, - f'expecting {num_authors_expected} authors but got {actual}') + def setUp(self) -> None: + self.initialize_test_data() - finally: - if project is not None: - try: - session.delete(project) - except Exception as exc: - _LOG.warning(f'delete failure: {exc}') - if session is not None: - session.close() - - def test_create_science_project(self): - session = None - project = None - try: - session = create_session('SDM') - except Exception as exc: - pytest.fail(f'failed to create session: {exc}') - - project = self.initialize_science_test_project(session) - self.assertIsNotNone(project) - expected = ScodeTestProject() - self.assertEqual(expected.project_code, - project.project_code, - f'expected {expected.project_code}; got {project.project_code}') - num_authors_expected = len(expected.authors) - actual = len(project.authors) - self.assertEqual(num_authors_expected, actual, - f'expecting {num_authors_expected} authors but got {actual}') - - finally: - if project is not None: - try: - session.delete(project) - except Exception as exc: - _LOG.warning(f'delete failure: {exc}') - if session is not None: - session.close() - - def test_create_alma_project(self): - session = None - project = None - try: - session = create_session('SDM') - except Exception as exc: - pytest.fail(f'failed to create session: {exc}') - - project = self.initialize_alma_test_project(session) - self.assertIsNotNone(project) - expected = ScodeTestProject() - self.assertEqual(expected.project_code, - project.project_code, - f'expected {expected.project_code}; got {project.project_code}') - num_authors_expected = len(expected.authors) - actual = len(project.authors) - self.assertEqual(num_authors_expected, actual, - f'expecting {num_authors_expected} authors but got {actual}') - - finally: - if project is not None: - try: - session.delete(project) - except Exception as exc: - _LOG.warning(f'delete failure: {exc}') - if session is not None: - session.close() - - ### THE MEAT ### def test_dry_run_does_not_update(self): - session = None - project = None + fake_project = ScodeTestProject().project + project_code = fake_project.project_code + session = create_session('SDM') + return_code = None try: - session = create_session('SDM') - except Exception as exc: - pytest.fail(f'failed to create session: {exc}') - - project = self.initialize_scode_test_project(session) + new_title = 'this is the new title' + self.assertNotEqual(fake_project.title, new_title) args = [ - '-C', project.project_code, - '-P', os.environ['profile'], - '-T', 'this is the new title', - '-dry' - ] - scode_updater = ScodeProjectUpdater(args) - updated = scode_updater.update_project() - self.assertIsNone(updated, 'project should not have been updated') - - after = self.initialize_scode_test_project(session) - self.assertEqual(after.project_code, - project.project_code, - f'expected {after.project_code}; got {project.project_code}') - num_authors_expected = len(after.authors) - actual = len(project.authors) - self.assertEqual(num_authors_expected, actual, - f'expecting {num_authors_expected} authors but got {actual}') + '-C', project_code, + '-P', PROFILE, + '-T', new_title, + '--dry' + ] + try: + return_code = CommandLineUpdaterLauncher(args).run() + except Exception as exc: + text = self.return_values[return_code] if return_code else '' + pytest.fail(f'{exc} {text}') - finally: - if project is not None: - try: - session.delete(project) - except Exception as exc: - _LOG.warning(f'delete failure: {exc}') - if session is not None: - session.close() + if not return_code: + updated = self.get_project_from_db(session, project_code) + # nothing should have been updated + self.assertEqual(fake_project.title, updated.title) + self.assertEqual(fake_project.abstract, updated.abstract) + self.assertEqual(len(fake_project.authors), + len(updated.authors)) - def test_alma_project_not_updated(self): - session = None - project = None - try: - session = create_session('SDM') except Exception as exc: - pytest.fail(f'failed to create session: {exc}') - - project = self.initialize_alma_project(session) - pst_ids = self.get_author_pst_ids(project) - args = [ - '-C', project.project_code, - '-P', os.environ['profile'], - '-I', pst_ids[1:], - '-T', 'this is the new title', - ] - scode_updater = ScodeProjectUpdater(args) - updated = scode_updater.update_project() - self.assertIsNone(updated, 'project should not have been updated') - - after = self.initialize_scode_test_project(session) - self.assertEqual(after.project_code, - project.project_code, - f'expected {after.project_code}; got {project.project_code}') - num_authors_expected = len(after.authors) - actual = len(project.authors) - self.assertEqual(num_authors_expected, actual, - f'expecting {num_authors_expected} authors but got {actual}') - + pytest.fail(f'{project_code}: {exc}') finally: - if project is not None: + session.close() + + def test_project_code_only_fetches(self): + fake_project = ScodeTestProject().project + project_code = fake_project.project_code + args = [ + '-C', project_code, + '-P', PROFILE, + ] + return_code = None + try: + return_code = CommandLineUpdaterLauncher(args).run() + if not return_code: + session = create_session('SDM') try: - session.delete(project) - except Exception as exc: - _LOG.warning(f'delete failure: {exc}') - if session is not None: + updated = self.get_project_from_db(session, project_code) + self.assertEqual(fake_project.title, updated.title) + self.assertEqual(fake_project.abstract, updated.abstract) + self.assertEqual(len(fake_project.authors), + len(updated.authors)) + count = 0 + for orig_author in fake_project.authors: + for author in updated.authors: + if author.username == orig_author.username: + count += 1 + break + self.assertEqual(len(fake_project.authors), count) + finally: session.close() + except Exception as exc: + text = self.return_values[return_code] if return_code else '' + pytest.fail(f'{exc} {text}') - - ### UTILITIES ### - - def initialize_scode_test_project(self, session: Session): + def test_updates_abstract_only(self): + fake_project = ScodeTestProject().project + project_code = fake_project.project_code + session = create_session('SDM') + new_abstract = "Well, here's another nice mess you've gotten us into, Ollie" + self.assertNotEqual(fake_project.abstract, new_abstract) + args = [ + '-C', project_code, + '-P', PROFILE, + '-A', new_abstract, + ] + return_code = None try: - project = ScodeTestProject() - result = None - - try: - # see if fake project already in DB - result = session.query(Project) \ - .filter(Project.project_code == - project.project_code) \ - .first() - except Exception as exc: - pytest.fail(f'query failure: {exc}') - - if result is not None: - # delete it so we can insert a virgin copy - try: - session.delete(result) - except Exception as exc: - pytest.fail(f'delete failure: {exc}') - - # insert fake project try: - session.add(project) - return project + return_code = CommandLineUpdaterLauncher(args).run() + except subprocess.TimeoutExpired as exp: + raise UpdateException(exp) except Exception as exc: - pytest.fail(f'insert failure: {exc}') + text = self.return_values[return_code] if return_code else '' + pytest.fail(f'{exc} {text}') + + if not return_code: + updated = self.get_project_from_db(session, project_code) + # only abstract should have been updated; + # all else should be same + self.assertEqual(fake_project.title, updated.title) + self.assertEqual(new_abstract, updated.abstract) + self.assertEqual(len(fake_project.authors), + len(updated.authors)) + else: + raise UpdateException() except Exception as exc: - pytest.fail(f'failed to create session: {exc}') + pytest.fail(f'{project_code}: {exc}') + finally: + session.close() - def initialize_science_test_project(self, session: Session): + def test_updates_abstract_and_title(self): + fake_project = ScodeTestProject().project + project_code = fake_project.project_code + session = create_session('SDM') + new_abstract = "I think you ought to know I'm feeling very depressed" + new_title = 'A Survey of the Mattresses of Sqornshellous Zeta' + self.assertNotEqual(fake_project.abstract, new_abstract) + self.assertNotEqual(fake_project.title, new_title) + args = [ + '-C', project_code, + '-P', PROFILE, + '-A', new_abstract, + '-T', new_title, + ] + return_code = None try: - project = ScienceTestProject() - result = None - try: - # see if fake project already in DB - result = session.query(Project) \ - .filter(Project.project_code == - project.project_code) \ - .first() + return_code = CommandLineUpdaterLauncher(args).run() + except subprocess.TimeoutExpired as exp: + raise UpdateException(exp) except Exception as exc: - pytest.fail(f'query failure: {exc}') - - if result is not None: - # delete it so we can insert a virgin copy - try: - session.delete(result) - except Exception as exc: - pytest.fail(f'delete failure: {exc}') + text = self.return_values[return_code] if return_code else '' + pytest.fail(f'{exc} {text}') + + if not return_code: + updated = self.get_project_from_db(session, project_code) + # abstract and title should have been updated; + # all else should be same + self.assertEqual(new_title, updated.title) + self.assertEqual(new_abstract, updated.abstract) + self.assertEqual(len(fake_project.authors), + len(updated.authors)) + else: + raise UpdateException() - # insert fake project + except Exception as exc: + pytest.fail(f'{project_code}: {exc}') + finally: + session.close() + + def test_adds_new_abstract_deletes_author(self): + fake_project = ScodeTestProject().project + new_project = fake_project + project_code = fake_project.project_code + new_abstract = "First there is a mountain, then there is no " \ + "mountain, then there is" + self.assertNotEqual(new_abstract, fake_project.abstract) + new_project.abstract = new_abstract + original_authors = fake_project.authors.copy() + new_authors = original_authors[:3] + self.assertEqual(len(original_authors) - 1, len(new_authors)) + new_project.authors = new_authors + args = [ + '-C', project_code, + '-P', PROFILE, + '-A', new_abstract, + '-I', + ] + for id in get_author_pst_ids(new_project): + args.append(str(id)) + + return_code = None + session = create_session('SDM') + try: try: - session.add(project) - return project + return_code = CommandLineUpdaterLauncher(args).run() + except subprocess.TimeoutExpired as exp: + raise UpdateException(exp) except Exception as exc: - pytest.fail(f'insert failure: {exc}') + text = self.return_values[return_code] if return_code else '' + pytest.fail(f'{exc} {text}') + + if not return_code: + updated = self.get_project_from_db(session, project_code) + # last author should have been removed and the abstract changed; + # title should remain same + self.assertEqual(new_abstract, updated.abstract) + self.assertEqual(fake_project.title, updated.title) + self.assertEqual(len(new_authors), len(updated.authors)) + self.assertFalse(original_authors[3] in updated.authors) + count = 0 + for orig_author in original_authors[:3]: + for new_author in updated.authors: + if new_author.username == orig_author.username: + count += 1 + break + self.assertEqual(len(new_authors), count) + else: + raise UpdateException() except Exception as exc: - pytest.fail(f'failed to create session: {exc}') + pytest.fail(f'{project_code}: {exc}') - def initialize_alma_project(self, session: Session): - try: - project = AlmaTestProject() - result = None + finally: + session.close() - try: - # see if fake project already in DB - result = session.query(Project) \ - .filter(Project.project_code == - project.project_code) \ - .first() - except Exception as exc: - pytest.fail(f'query failure: {exc}') - if result is not None: - # delete it so we can insert a virgin copy + ### UTILITIES ### + + def initialize_test_data(self): + session = create_session('SDM') + try: + with warnings.catch_warnings(): + # Suppress SQLAlchemy warnings + warnings.simplefilter("ignore", category=sa_exc.SAWarning) + + fake_projects = [ScodeTestProject().project, + ScienceTestProject().project, + AlmaTestProject().project] + num_commits = 0 try: - session.delete(result) + for fake_project in fake_projects: + project_code = fake_project.project_code + existing = session.query(Project) \ + .filter(Project.project_code == + project_code) \ + .first() + if existing is not None: + session.delete(existing) + session.commit() + session.add(fake_project) + session.commit() + num_commits += 1 + + self.assertEqual(len(fake_projects), num_commits) except Exception as exc: - pytest.fail(f'delete failure: {exc}') + pytest.fail(f'{exc}') + finally: + session.close() - # insert fake project - try: - session.add(project) - return project - except Exception as exc: - pytest.fail(f'insert failure: {exc}') + def get_project_from_db(self, session: Session, project_code: str): + with warnings.catch_warnings(): + # Suppress SQLAlchemy warnings + warnings.simplefilter("ignore", category=sa_exc.SAWarning) - except Exception as exc: - pytest.fail(f'failed to create session: {exc}') + return session.query(Project) \ + .filter(Project.project_code == project_code) \ + .first() - @staticmethod - def get_author_pst_ids(project: Project): - ''' build list of pst_person_ids for display; - we want the PI's pst_person_id followed by the CoIs' pst_person_ids in numeric order - ''' - project_pi = project.authors[0] - coi_pst_ids = [int(coi.pst_person_id) for coi in project.authors[1:]] - coi_pst_ids = sorted(coi_pst_ids) +class CommandLineUpdaterLauncher: - author_pst_ids = [int(project_pi.pst_person_id)] - [author_pst_ids.append(id) for id in coi_pst_ids] - return [str(id) for id in author_pst_ids] + def __init__(self, args: list): + self.args = [_UPDATE_COMMAND] + for arg in args: + self.args.append(str(arg)) + _LOG.info(f'{self.args}') - @staticmethod - def project_is_public(project_code: str): - ''' is this project accessible by all? ''' - session = create_session('SDM') - project = session.query(Project) \ - .filter(Project.project_code == project_code) \ - .first() - if project.proprietary_duration == 0: - return True - endtime = project.endtime - duration = project.proprietary_duration - now = Time.now().mjd - release_time = endtime + duration - elapsed_time = now - release_time - return elapsed_time > 0 - - - def debug_multiple_fetch_create_session(self): - ''' for debugging connection issues; - to use, change "debug" to "test" + def run(self): + ''' launch updater from command line + @:returns directory listing ''' - for _ in range(20): - session = None - try: - try: - session = create_session('SDM') - except Exception as exc: - pytest.fail(f'failed to create session: {exc}') - - try: - session.query("SELECT starttime FROM projects WHERE " - "project_code = '13B-014'") - except Exception as exc: - pytest.fail(f'query failure: {exc}') - finally: - if session is not None: - session.close() + args = self.args + try: + proc = subprocess.run(args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + timeout=60, + check=False, + bufsize=1, + universal_newlines=True) + return proc.returncode + except Exception as exc: + _LOG.error(f'{exc}') + if not isinstance(exc, subprocess.TimeoutExpired): + return exc.returncode + else: + raise + +def build_updater_return_values(): + return { + 1: 'error with capo configuration', + 2: 'error with input parameters', + 3: 'project not found', + 4: 'investigator not found', + 5: 'update failed', + } if __name__ == '__main__': unittest.main()