From 20c693abf39da76422419fbe2b8db391a1cbcf0b Mon Sep 17 00:00:00 2001
From: jgoldste <0uTfr1p@n>
Date: Fri, 31 Jul 2020 10:56:01 -0600
Subject: [PATCH] SSA-6324: rearranged scode_project_updater package structure;
 started creating unit tests that don't depend on a particular state of the
 dev database

---
 apps/__init__.py                              |   0
 apps/cli/__init__.py                          |   0
 apps/cli/utilities/__init__.py                |   0
 .../s_code_project_updater/__init__.py        |   0
 .../utilities/s_code_project_updater/setup.py |   1 +
 .../src/s_code_project_updater/__init__.py    |  10 +
 .../s_code_project_updater/test_projects.py   | 205 -------------
 .../s_code_project_updater/updater_tests.py   | 272 ------------------
 .../s_code_project_updater/test/__init__.py   |   0
 .../test/test_projects.py                     | 157 ++++++++++
 .../test/test_updater.py                      | 225 +++++++++++++++
 apps/cli/utilities/test_data/__init__.py      |   0
 .../insert_one_bogus_project_into_archive.py  |  25 --
 13 files changed, 393 insertions(+), 502 deletions(-)
 create mode 100644 apps/__init__.py
 create mode 100644 apps/cli/__init__.py
 create mode 100644 apps/cli/utilities/__init__.py
 create mode 100644 apps/cli/utilities/s_code_project_updater/__init__.py
 delete mode 100644 apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/test_projects.py
 delete mode 100644 apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/updater_tests.py
 create mode 100644 apps/cli/utilities/s_code_project_updater/test/__init__.py
 create mode 100644 apps/cli/utilities/s_code_project_updater/test/test_projects.py
 create mode 100644 apps/cli/utilities/s_code_project_updater/test/test_updater.py
 create mode 100644 apps/cli/utilities/test_data/__init__.py
 delete mode 100644 apps/cli/utilities/test_data/insert_one_bogus_project_into_archive.py

diff --git a/apps/__init__.py b/apps/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/cli/__init__.py b/apps/cli/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/cli/utilities/__init__.py b/apps/cli/utilities/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/cli/utilities/s_code_project_updater/__init__.py b/apps/cli/utilities/s_code_project_updater/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/cli/utilities/s_code_project_updater/setup.py b/apps/cli/utilities/s_code_project_updater/setup.py
index 36ee1407f..fc1c96408 100644
--- a/apps/cli/utilities/s_code_project_updater/setup.py
+++ b/apps/cli/utilities/s_code_project_updater/setup.py
@@ -17,6 +17,7 @@ setup(
     url='TBD',
     license="GPL",
     install_requires=['pycapo', 'pymygdala', 'schema', 'sqlalchemy', 'support'],
+    requires=['sqlalchemy', 'mysqldb'],
     keywords=[],
     packages=['s_code_project_updater'],
     package_dir={'':'src'},
diff --git a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/__init__.py b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/__init__.py
index e69de29bb..57122f286 100644
--- a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/__init__.py
+++ b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/__init__.py
@@ -0,0 +1,10 @@
+from enum import Enum
+
+
+class Telescope(Enum):
+    ''' all telescopes we expect to find in execution_blocks '''
+    ALMA = 'ALMA'
+    EVLA = 'EVLA'
+    VLA  = 'VLA'
+    VLBA = 'VLBA'
+    GBT  = 'GBT'
diff --git a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/test_projects.py b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/test_projects.py
deleted file mode 100644
index 27b8f2014..000000000
--- a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/test_projects.py
+++ /dev/null
@@ -1,205 +0,0 @@
-from abc import ABC
-from typing import List
-
-from .commands import ArchiveProject
-from schema import Author
-
-
-class AbstractTestProject(ABC, ArchiveProject):
-
-    def __init__(self, project_code: str, title: str, abstract: str, authors: List, is_alma: bool):
-        self.project_code = project_code
-        self.title = title
-        self.abstract = abstract
-        self.authors = authors
-        self.is_alma = is_alma
-        # self.author_pst_ids = self._get_author_pst_ids()
-
-    def _get_author_pst_ids(self):
-        # we want the PI's pst_person_id followed by the CoIs' pst_person_ids in numeric order
-        pi = self.authors[0]
-        coi_pst_ids = [int(coi.pst_person_id) for coi in self.authors[1:]]
-        coi_pst_ids = sorted(coi_pst_ids)
-
-        author_pst_ids = [int(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 as_sc_project(self):
-        author_pst_ids = [str(author.pst_person_id) for author in self.authors]
-        return ArchiveProject(self.project_code, self.title, self.abstract, author_pst_ids)
-
-class ScodeTestProject(AbstractTestProject):
-
-    def __init__(self):
-        self.project_code = 'SK0442'
-        self.title = 'Cool Sky Stuff'
-        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(author_id=65409,
-                   project=self.project_code,
-                   username='srandall',
-                   firstname='Scott',
-                   lastname='Randall',
-                   pst_person_id='4686',
-                   is_pi=True),
-
-            Author(author_id=65410,
-                   project=self.project_code,
-                   username='s.giacintucci',
-                   firstname='Simona',
-                   lastname='Giacintucci',
-                   pst_person_id='317',
-                   is_pi=False),
-
-            Author(author_id=65411,
-                   project=self.project_code,
-                   username='esch44',
-                   firstname='Emma',
-                   lastname='Schwartzman',
-                   pst_person_id='11991',
-                   is_pi=False),
-
-            Author(author_id=65412,
-                   project=self.project_code,
-                   username='tclarke',
-                   firstname='Tracy',
-                   lastname='Clarke',
-                   pst_person_id='341',
-                   is_pi=False),
-
-        ]
-
-        self.is_alma = False
-        self.author_pst_ids = self._get_author_pst_ids()
-
-        super(AbstractTestProject, self).__init__(self.project_code, self.title, self.abstract, self.author_pst_ids)
-
-class ScienceTestProject(AbstractTestProject):
-
-    def __init__(self):
-        self.project_code = '13B-014'
-        self.title = 'The Comprehensive VLA Survey for Black Holes in Globular Clusters'
-        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(author_id=8749,
-                   project=self.project_code,
-                   username='jstrader',
-                   firstname='Jay',
-                   lastname='Strader',
-                   pst_person_id='4064',
-                   is_pi=True),
-
-            Author(author_id=8743,
-                   project=self.project_code,
-                   username='jcamj',
-                   firstname='James',
-                   lastname='Miller-Jones',
-                   pst_person_id='490',
-                   is_pi=False),
-
-            Author(author_id=8744,
-                   project=self.project_code,
-                   username='chomiuk',
-                   firstname='Laura',
-                   lastname='Chomiuk',
-                   pst_person_id='701',
-                   is_pi=False),
-
-            Author(author_id=8745,
-                   project=self.project_code,
-                   username='gsivakoff',
-                   firstname='Gregory',
-                   lastname='Sivakoff',
-                   pst_person_id='834',
-                   is_pi=False),
-
-            Author(author_id=8746,
-                   project=self.project_code,
-                   username='tjmaccarone',
-                   firstname='Thomas',
-                   lastname='Maccarone',
-                   pst_person_id='887',
-                   is_pi=False),
-
-            Author(author_id=8747,
-                   project=self.project_code,
-                   username='anilseth',
-                   firstname='Anil',
-                   lastname='Setn',
-                   pst_person_id='1197',
-                   is_pi=False),
-
-            Author(author_id=8748,
-                   project=self.project_code,
-                   username='Craig Heinke',
-                   firstname='Craig',
-                   lastname='Heinke',
-                   pst_person_id='3729',
-                   is_pi=False),
-
-            Author(author_id=8750,
-                   project=self.project_code,
-                   username='evanoyola',
-                   firstname='Eva',
-                   lastname='Noyola',
-                   pst_person_id='5532',
-                   is_pi=False),
-
-        ]
-
-        self.is_alma = False
-        self.author_pst_ids = self._get_author_pst_ids()
-
-        super(AbstractTestProject, self).__init__(self.project_code, self.title, self.abstract, self.author_pst_ids)
-
-
-class AlmaTestProject(AbstractTestProject):
-
-    def __init__(self):
-        self.project_code = '2012.1.00060.S'
-        self.title = "Testing Schmidt's Conjecture in NGC 300: Bridging the Gap between Galactic and Extragalactic Star Formation"
-        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(author_id=37200,
-                   project=self.project_code,
-                   username='clada',
-                   firstname='Charles',
-                   lastname='Lada',
-                   pst_person_id=None,
-                   is_pi=True),
-
-            Author(author_id=37201,
-                   project=self.project_code,
-                   username='jforbrich',
-                   firstname='Jan',
-                   lastname='Forbrich',
-                   pst_person_id=None,
-                   is_pi=False),
-
-            Author(author_id=37202,
-                   project=self.project_code,
-                   username='cfaesi',
-                   firstname='Christopher',
-                   lastname='Faesi',
-                   pst_person_id=None,
-                   is_pi=False),
-
-        ]
-        self.is_alma = True
-        super(AbstractTestProject, self).__init__(self.project_code, self.title, self.abstract, None)
-
-
diff --git a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/updater_tests.py b/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/updater_tests.py
deleted file mode 100644
index 4bbe4cba3..000000000
--- a/apps/cli/utilities/s_code_project_updater/src/s_code_project_updater/updater_tests.py
+++ /dev/null
@@ -1,272 +0,0 @@
-import logging
-import os
-import unittest
-import warnings
-
-from support.logging import get_console_logger
-from .commands import ArchiveProject, ScodeProjectUpdater
-from .project_fetcher import ArchiveProjectFetcher
-from .test_projects import ScienceTestProject, ScodeTestProject, AlmaTestProject
-from schema import Author, ArchiveDBSession
-from sqlalchemy import exc as sa_exc
-
-_LOG = get_console_logger("scode_project_updater_tests", logging.DEBUG)
-
-class UpdaterTestCase(unittest.TestCase):
-
-    @classmethod
-    def setUp(self) -> None:
-        self.profile = os.environ['CAPO_PROFILE']
-
-    def restore_scode_project_to_original_state(self):
-        self.archive_context = ArchiveDBSession('SDM', profile=self.profile)
-        self.pst_context = ArchiveDBSession('PST', profile=self.profile)
-
-        scode_project = ScodeTestProject()
-        # start by restoring the title and abstract; authors will need special treatment
-        # args = scp.make_args(False)
-        args = ['-C', scode_project.project_code, '-P', self.profile, '-T', scode_project.title, '-A', scode_project.abstract]
-        project = ScodeProjectUpdater(args=args).update_project()
-
-        with warnings.catch_warnings(), self.archive_context, self.pst_context:
-            # Suppress SQLAlchemy warnings
-            warnings.simplefilter("ignore", category=sa_exc.SAWarning)
-
-            # clear existing investigators
-            investigators_list = self.archive_context.session.query(Author) \
-                .filter(Author.project_code == scode_project.project_code) \
-                .all()
-            for inv in investigators_list:
-                self.archive_context.session.delete(inv)
-            self.archive_context.session.commit()
-
-            # insert the canonical ones
-            canonical_authors = scode_project.authors
-            for author in canonical_authors:
-                author.project = project
-                self.archive_context.session.add(author)
-            self.archive_context.session.commit()
-
-        # confirm restoration
-        fetcher = ArchiveProjectFetcher(self.profile)
-        restored_project = fetcher.fetch_project(scode_project.project_code)
-        self.assertEqual(scode_project.title, restored_project.title)
-        self.assertEqual(scode_project.abstract, restored_project.abstract)
-
-        restored_authors = fetcher.detachable_author_list
-        self.assertEqual(len(scode_project.authors), len(restored_authors))
-
-        pi_found = False
-        for author in restored_authors:
-            if author.username == 'srandall':
-                self.assertTrue(author.is_pi, 'author is pi')
-                self.assertEqual(65409, author.author_id, 'expecting author_id 65409')
-                self.assertEqual('4686', author.pst_person_id, 'expecting pst_person_id 4686')
-                pi_found = True
-            else:
-                self.assertFalse(author.is_pi, 'author is pi')
-                self.assertNotEqual(65409, author.author_id, 'expecting author_id not 65409')
-                self.assertTrue(author.pst_person_id in ('317','341','11991'), "expecting pst_person_ids 317, 341, 11991")
-
-        self.assertTrue(pi_found)
-
-    def restore_science_project_to_original_state(self):
-        science_project = ScienceTestProject()
-        self.archive_context = ArchiveDBSession('SDM', profile=self.profile)
-        self.pst_context = ArchiveDBSession('PST', profile=self.profile)
-
-        # start by restoring the title and abstract; authors will need special treatment
-        # args = scp.make_args(False)
-        args = ['-C', science_project.project_code, '-P', self.profile, '-T', science_project.title, '-A', science_project.abstract]
-        project = ScodeProjectUpdater(args=args).update_project()
-
-        with warnings.catch_warnings(), self.archive_context, self.pst_context:
-            # Suppress SQLAlchemy warnings
-            warnings.simplefilter("ignore", category=sa_exc.SAWarning)
-            # clear existing investigators
-            investigators_list = self.archive_context.session.query(Author) \
-                .filter(Author.project_code == science_project.project_code) \
-                .all()
-            for inv in investigators_list:
-                self.archive_context.session.delete(inv)
-            self.archive_context.session.commit()
-
-            # insert the canonical ones
-            canonical_authors = science_project.authors
-            for author in canonical_authors:
-                author.project = project
-                self.archive_context.session.add(author)
-            self.archive_context.session.commit()
-
-        # confirm restoration
-        fetcher = ArchiveProjectFetcher(self.profile)
-        restored_project = fetcher.fetch_project(science_project.project_code)
-        self.assertEqual(science_project.title, restored_project.title)
-        self.assertEqual(science_project.abstract, restored_project.abstract)
-        restored_authors = fetcher.detachable_author_list
-        self.assertEqual(len(science_project.authors), len(restored_authors))
-
-        pi_found = False
-        for author in restored_authors:
-            if author.username == 'jstrader':
-                self.assertTrue(author.is_pi, 'author is pi')
-                self.assertEqual(8749, author.author_id, 'expecting author_id 8749')
-                self.assertEqual('4064', author.pst_person_id, 'expecting pst_person_id 4064')
-                pi_found = True
-            else:
-                self.assertFalse(author.is_pi, 'author is pi')
-                self.assertNotEqual(8749, author.pst_person_id, 'expecting author_id not 8749')
-                self.assertTrue(8742 < int(author.author_id) < 8751, 'expecting pst_person_id between 8743 and 8750')
-
-        self.assertTrue(pi_found)
-
-    def test_alma_project_has_not_changed(self):
-        alma_test_project = AlmaTestProject()
-        fetcher = ArchiveProjectFetcher(self.profile)
-        fetched = fetcher.fetch_project(alma_test_project.project_code)
-        self.assertEqual(alma_test_project.title, fetched.title)
-        self.assertEqual(alma_test_project.abstract, fetched.abstract)
-        authors_list = fetcher.detachable_author_list
-        self.assertEqual(len(alma_test_project.authors), len(authors_list))
-
-        pi_found = False
-        for author in authors_list:
-            self.assertIsNone(author.pst_person_id, 'expecting no pst_person_id')
-            if author.username == 'clada':
-                self.assertTrue(author.is_pi, 'author is pi')
-                self.assertEqual(37200, author.author_id, 'expecting author_id 37200')
-                pi_found = True
-            else:
-                self.assertFalse(author.is_pi, 'author is pi')
-                self.assertNotEqual(37200, author.pst_person_id, 'expecting author_id not 8749')
-
-        self.assertTrue(pi_found)
-
-    def test_restores_scode_project_correctly(self):
-        self.restore_scode_project_to_original_state()
-
-    def test_restores_science_project_correctly(self):
-        self.restore_science_project_to_original_state()
-
-    def test_can_fetch_from_project_code(self):
-        scode_project = ScodeTestProject()
-        args = ['-C', scode_project.project_code, '-P', self.profile]
-        fetched = ScodeProjectUpdater(args=args).update_project()
-        self.assertEqual(scode_project.title, fetched.title)
-        self.assertEqual(scode_project.abstract, fetched.abstract)
-
-    def test_can_fetch_non_scode_project(self):
-        args = ['-C', '13B-014', '-P', self.profile]
-        fetched = ScodeProjectUpdater(args=args).update_project()
-        self.assertIsNotNone(fetched)
-
-    def test_can_modify_non_scode_project(self):
-        to_modify = ScienceTestProject()
-        authors = to_modify.authors.copy()
-        pst_ids = [author.pst_person_id for author in authors]
-        scp = ArchiveProject(to_modify.project_code, 'foo', to_modify.abstract, pst_ids)
-        scp_args = scp.make_args(False)
-
-        updater = ScodeProjectUpdater(args=scp_args)
-        updated_project = updater.update_project()
-
-        fetcher = ArchiveProjectFetcher(self.profile)
-        retrieved_project = fetcher.fetch_project(updated_project.project_code)
-        # title should have changed
-        self.assertEqual('foo', retrieved_project.title)
-
-        self.restore_science_project_to_original_state()
-
-    def test_no_update_with_dry_run(self):
-
-        self.restore_scode_project_to_original_state()
-        scode_project = ScodeTestProject()
-        authors = scode_project.authors.copy()
-        pst_ids = [author.pst_person_id for author in authors]
-        pst_ids.append(5654)
-
-        scp = ArchiveProject(scode_project.project_code, scode_project.title, scode_project.abstract, pst_ids)
-        scp_args = scp.make_args(True)
-
-        updater = ScodeProjectUpdater(args=scp_args)
-        updated_project = updater.update_project()
-
-        fetcher = ArchiveProjectFetcher(self.profile)
-        retrieved_project = fetcher.fetch_project(updated_project.project_code)
-        authors = retrieved_project.authors
-
-        self.assertEqual(len(scode_project.authors), len(authors))
-        for author in authors:
-            if author.username == 'srandall':
-                assert author.is_pi
-            else:
-                assert not author.is_pi
-
-            self.assertEqual(scode_project.title, retrieved_project.title)
-            self.assertEqual(scode_project.abstract, retrieved_project.abstract)
-
-    def test_setting_investigators_preserves_title_and_abstract(self):
-        scode_project = ScodeTestProject()
-        authors = scode_project.authors.copy()
-        pst_ids = [author.pst_person_id for author in authors]
-        pst_ids.append(5654)
-
-        scp = ArchiveProject(scode_project.project_code, scode_project.title, scode_project.abstract, pst_ids)
-        scp_args = scp.make_args(False)
-
-        updater = ScodeProjectUpdater(args=scp_args)
-        updater.update_project()
-
-        fetcher = ArchiveProjectFetcher(self.profile)
-        retrieved_project = fetcher.fetch_project(scp.project_code)
-
-        authors = fetcher.detachable_author_list
-        self.assertEqual(len(scode_project.authors) + 1, len(authors))
-        for author in authors:
-            if author.username == 'srandall':
-                assert author.is_pi
-            else:
-                assert not author.is_pi
-
-        self.assertEqual(scode_project.title, retrieved_project.title)
-        self.assertEqual(scode_project.abstract, fetcher.abstract)
-        self.restore_scode_project_to_original_state()
-
-    def test_alma_project_not_updated(self):
-        alma_project = AlmaTestProject()
-        args = alma_project.make_args(False)
-        try:
-            updater = ScodeProjectUpdater(args=args)
-            updater.update_project()
-        except Exception as exc:
-            _LOG.info(f'attempt to update ALMA project failed, as expected: {exc}')
-        self.test_alma_project_has_not_changed()
-
-    def test_output_is_desired_format(self):
-        scode_project = ScodeTestProject()
-        authors = scode_project.authors.copy()
-        pi = authors[0]
-        coi_pst_ids = [int(coi.pst_person_id) for coi in authors[1:]]
-        coi_pst_ids = sorted(coi_pst_ids)
-
-        author_pst_ids = [int(pi.pst_person_id)]
-        [author_pst_ids.append(id) for id in coi_pst_ids]
-        authors_to_print = [str(id) for id in author_pst_ids]
-        id_list = ' '.join(authors_to_print)
-
-        scp = ArchiveProject(scode_project.project_code, scode_project.title, scode_project.abstract, author_pst_ids)
-        scp_args = scp.make_args(True)
-        ScodeProjectUpdater(args=scp_args).update_project()
-
-        fetcher = ArchiveProjectFetcher(self.profile)
-        fetcher.fetch_project(scp.project_code)
-        output = fetcher.build_project_info()
-        self.assertEqual(3, len(output))
-        authors_line = output[2]
-        self.assertEqual(f'Authors: {id_list}', authors_line )
-
-
-UpdaterTestCase()
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/apps/cli/utilities/s_code_project_updater/test/__init__.py b/apps/cli/utilities/s_code_project_updater/test/__init__.py
new file mode 100644
index 000000000..e69de29bb
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
new file mode 100644
index 000000000..2d65610e9
--- /dev/null
+++ b/apps/cli/utilities/s_code_project_updater/test/test_projects.py
@@ -0,0 +1,157 @@
+''' fake projects to use in testing scode_project_updater '''
+from shared.schema.src.schema import Author, Project
+
+
+class ScodeTestProject(Project):
+    ''' 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):
+    ''' 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):
+    ''' 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),
+        ]
+
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
new file mode 100644
index 000000000..1bb9c1770
--- /dev/null
+++ b/apps/cli/utilities/s_code_project_updater/test/test_updater.py
@@ -0,0 +1,225 @@
+''' unit/regression tests for s_code_project_updater '''
+import logging
+import os
+import unittest
+
+import pytest
+
+from astropy.time import Time
+
+from shared.schema.src.schema import create_session, Project
+from shared.support.src.support.logging import get_console_logger
+
+from .test_projects import AlmaTestProject, ScodeTestProject, ScienceTestProject
+
+_LOG = get_console_logger("scode_project_updater_tests", logging.DEBUG)
+
+class UpdaterTestCase(unittest.TestCase):
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        os.environ['CAPO_PROFILE'] = 'local'
+
+    # @pytest.mark.skip('test_create_scode_test_project')
+    def test_create_scode_test_project(self):
+        session = None
+        try:
+            session = create_session('SDM')
+            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)
+            except Exception as exc:
+                pytest.fail(f'insert failure: {exc}')
+
+        except Exception as exc:
+            pytest.fail(f'failed to create session: {exc}')
+        finally:
+            if session is not None:
+                # 1. delete fake project, then
+                session.close()
+
+
+    # @pytest.mark.skip('test_create_science_project')
+    def test_create_science_project(self):
+        session = None
+        try:
+            session = create_session('SDM')
+            project = ScienceTestProject()
+            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)
+            except Exception as exc:
+                pytest.fail(f'insert failure: {exc}')
+
+        except Exception as exc:
+            pytest.fail(f'failed to create session: {exc}')
+        finally:
+            if session is not None:
+                # 1. delete fake project, then
+                session.close()
+
+    def test_create_alma_project(self):
+        session = None
+        try:
+            session = create_session('SDM')
+            project = AlmaTestProject()
+            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)
+            except Exception as exc:
+                pytest.fail(f'insert failure: {exc}')
+
+        except Exception as exc:
+            pytest.fail(f'failed to create session: {exc}')
+        finally:
+            if session is not None:
+                # 1. delete fake project, then
+                session.close()
+
+    ### UTILITIES ###
+
+    @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)
+
+        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]
+
+    @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 template(self):
+        ''' just for copying & pasting '''
+        session = None
+        try:
+            session = create_session('SDM')
+            try:
+                # see if fake project already in DB
+                pass
+            except Exception as exc:
+                pytest.fail(f'query failure: {exc}')
+
+            # if it's there,
+            try:
+                # delete fake project
+                pass
+            except Exception as exc:
+                pytest.fail(f'delete failure: {exc}')
+
+            try:
+                # insert fake project
+                pass
+            except Exception as exc:
+                pytest.fail(f'insert failure: {exc}')
+
+            try:
+                # exercise feature
+                pass
+            except Exception as exc:
+                pytest.fail(f'TODO: {exc}')
+
+        except Exception as exc:
+            pytest.fail(f'failed to create session: {exc}')
+        finally:
+            if session is not None:
+                # 1. delete fake project, then
+                session.close()
+
+    def debug_multiple_fetch_create_session(self):
+        ''' for debugging connection issues;
+            to use, change "debug" to "test"
+        '''
+        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()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/apps/cli/utilities/test_data/__init__.py b/apps/cli/utilities/test_data/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/apps/cli/utilities/test_data/insert_one_bogus_project_into_archive.py b/apps/cli/utilities/test_data/insert_one_bogus_project_into_archive.py
deleted file mode 100644
index 8a76d559a..000000000
--- a/apps/cli/utilities/test_data/insert_one_bogus_project_into_archive.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import pytest
-from sqlalchemy import table
-
-from shared.schema.src.schema import create_session, Project
-
-session = create_session('SDM', profile='local')
-to_insert = Project(project_code='FAKE_TEST_PROJECT',
-                    title='Move along move along nothing to see here',
-                    starttime=56688.8103640046,
-                    endtime=-56810.4840497685,
-                    proprietary_duration=120)
-try:
-    table(Project).insert(to_insert)
-    session.commit()
-    session.flush()
-except Exception as exc:
-    pytest.fail(f'insert failed: {exc}')
-
-try:
-    retrieved = session.query(Project).filter(Project==to_insert).first()
-    if retrieved is None:
-        pytest.fail(f'{to_insert} not found')
-except Exception as exc:
-    pytest.fail(f'retrieval failed: {exc}')
-
-- 
GitLab