Repository: RockefellerArchiveCenter/DACSspace

Issue:
Allow package to be run from the command line
**Is your feature request related to a problem? Please describe.**
DACSspace is primarily designed to be run from the command line. We should make sure this is actually possible!

**Describe the solution you'd like**
DACSspace should support something like the following:
```
pip install dacsspace
dacsspace.py --published_only --invalid_only --schema-identifier rac
```


Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index 3890670..630d230 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,6 @@ log.txt
 *__pycache__
 .coverage
 .tox
+build
+*.egg-info
+.pytest_cache
diff --git a/dacsspace.py b/dacsspace/command_line.py
similarity index 64%
rename from dacsspace.py
rename to dacsspace/command_line.py
index d3110db..30c7472 100644
--- a/dacsspace.py
+++ b/dacsspace/command_line.py
@@ -1,10 +1,16 @@
 import argparse
 
-from dacsspace.dacsspace import DACSspace
+from .dacsspace import DACSspace
 
-if __name__ == '__main__':
+
+def main():
+    """Command line entrypoint for DACSspace. Parses arguments received from stdin."""
     parser = argparse.ArgumentParser(
-        description="Fetches data from AS, validates and reports results")
+        description="Fetches data from ArchivesSpace, validates and reports results")
+    parser.add_argument(
+        'csv_filepath',
+        help='Filepath for results report (CSV format)',
+        type=str)
     parser.add_argument(
         '--published_only',
         help='Fetches only published records from AS',
@@ -17,14 +23,16 @@ if __name__ == '__main__':
     group.add_argument(
         '--schema_identifier',
         help='Choose schema_identifier or schema_filepath. schema_identifier default is single_level_required.json',
-        type=str, default='single_level_required.json')
+        type=str,
+        default='single_level_required.json')
     group.add_argument(
         '--schema_filepath',
         help='Choose schema_identifier or schema_filepath. Schema_filepath default is None, only one of schema_identifier',
-        type=str, default=None)
+        type=str,
+        default=None)
     args = parser.parse_args()
 
-    DACSspace().run(
+    DACSspace(args.csv_filepath).run(
         args.published_only,
         args.invalid_only,
         args.schema_identifier,
diff --git a/dacsspace/dacsspace.py b/dacsspace/dacsspace.py
index 8e691e1..fd7454e 100644
--- a/dacsspace/dacsspace.py
+++ b/dacsspace/dacsspace.py
@@ -15,15 +15,13 @@ class DACSspace:
         if re.search(r'[*?:"<>|]', csv_filepath):
             raise ValueError(
                 'File name cannot contain the following characters: * ? : " < > | ')
+        self.csv_filepath = csv_filepath
 
     def run(self, published_only, invalid_only,
             schema_identifier, schema_filepath):
         client = ArchivesSpaceClient()
         validator = Validator(schema_identifier, schema_filepath)
-        reporter = CSVReporter()
+        reporter = CSVReporter(self.csv_filepath)
         data = client.get_resources(published_only)
-        results = []
-        for obj in data:
-            result = validator.validate(obj)
-            results.append(result)
+        results = [validator.validate_data(obj) for obj in data]
         reporter.write_report(results, invalid_only)
diff --git a/dacsspace/local_settings.example b/dacsspace/local_settings.example
index 1e1f498..b085bec 100644
--- a/dacsspace/local_settings.example
+++ b/dacsspace/local_settings.example
@@ -3,7 +3,3 @@ baseurl: https://sandbox.archivesspace.org/api/
 repository: 101
 user: admin
 password: admin
-
-[Destinations]
-directory: #Directory where you would like to save the csv file
-filename: dacs_singlelevel_report.csv
diff --git a/dacsspace/reporter.py b/dacsspace/reporter.py
index 8c522d5..67bec10 100644
--- a/dacsspace/reporter.py
+++ b/dacsspace/reporter.py
@@ -21,6 +21,7 @@ class CSVReporter:
             raise ValueError("Filemode must allow write options.")
         with open(self.filename, self.filemode) as f:
             fieldnames = [
+                "uri",
                 "valid",
                 "explanation"]
             writer = csv.DictWriter(
diff --git a/dacsspace/validator.py b/dacsspace/validator.py
index 3f4d8cb..7d47839 100644
--- a/dacsspace/validator.py
+++ b/dacsspace/validator.py
@@ -28,13 +28,16 @@ class Validator:
             data (dict): An ArchivesSpace object to be validated.
 
         Returns:
-           result (dict): The result of the validation. An dictionary with a boolean
-           indication of the validation result and, if necessary, an explanation
-           of any validation errors. { "valid": False, "explanation": "You are missing the following fields..." }
+           result (dict): The result of the validation. An dictionary with the
+           object's URI, a boolean indication of the validation result and, if
+           necessary, an explanation of any validation errors.
+
+           { "uri": "/repositories/2/resources/1234", "valid": False, "explanation": "You are missing the following fields..." }
         """
         validator = self.validator(self.schema)
         errors_found = [error.message for error in validator.iter_errors(data)]
         if len(errors_found):
-            return {"valid": False, "explanation": "\n".join(errors_found)}
+            return {"uri": data["uri"], "valid": False,
+                    "explanation": "\n".join(errors_found)}
         else:
-            return {"valid": True}
+            return {"uri": data["uri"], "valid": True}
diff --git a/setup.py b/setup.py
index b9753b0..8de4b02 100644
--- a/setup.py
+++ b/setup.py
@@ -14,6 +14,9 @@ setup(
     version="0.1.0",
     license='MIT',
     packages=find_packages(),
+    entry_points={
+        'console_scripts': ['dacsspace=dacsspace.command_line:main'],
+    },
     zip_safe=False,
     classifiers=[
         'Programming Language :: Python :: 3',
@@ -22,6 +25,7 @@ setup(
     ],
     python_requires=">=3.7",
     install_requires=[
+        "jsonschema",
         "requests",
     ],
 )
diff --git a/tox.ini b/tox.ini
index d891449..e3d0701 100644
--- a/tox.ini
+++ b/tox.ini
@@ -10,7 +10,7 @@ deps =
 skip_install = True
 commands =
 	coverage run -m pytest -s
-	coverage report -m
+	coverage report -m --omit=tests/*
 
 [testenv:linting]
 basepython = python3


Test patch:
diff --git a/tests/test_dacsspace.py b/tests/test_dacsspace.py
index d2335f8..4e52084 100644
--- a/tests/test_dacsspace.py
+++ b/tests/test_dacsspace.py
@@ -1,4 +1,5 @@
 from unittest import TestCase
+from unittest.mock import patch
 
 from dacsspace.dacsspace import DACSspace
 
@@ -20,3 +21,31 @@ class TestDACSspace(TestCase):
             DACSspace("myfile")
         self.assertEqual(str(err.exception),
                          "File must have .csv extension")
+
+    @patch('dacsspace.client.ArchivesSpaceClient.__init__')
+    @patch('dacsspace.client.ArchivesSpaceClient.get_resources')
+    @patch('dacsspace.validator.Validator.__init__')
+    @patch('dacsspace.reporter.CSVReporter.__init__')
+    @patch('dacsspace.reporter.CSVReporter.write_report')
+    def test_args(self, mock_write_report, mock_reporter_init,
+                  mock_validator_init, mock_get_resources, mock_client_init):
+        """Asserts that arguments are passed to the correct methods."""
+        mock_reporter_init.return_value = None
+        mock_validator_init.return_value = None
+        mock_client_init.return_value = None
+        mock_get_resources.return_value = []
+        for csv_filepath, published_only, invalid_only, schema_identifier, schema_filepath in [
+                ('myfile.csv', False, True, 'single_level_required.json', None),
+                ('test.csv', True, True, None, 'filepath/to/schema.json'),
+                ('testfile.csv', False, False, 'single_level_required', None),
+                ('file.csv', True, False, 'rac.json', None)]:
+            DACSspace(csv_filepath).run(
+                published_only,
+                invalid_only,
+                schema_identifier,
+                schema_filepath)
+            mock_validator_init.assert_called_with(
+                schema_identifier, schema_filepath)
+            mock_reporter_init.assert_called_with(csv_filepath)
+            mock_get_resources.assert_called_with(published_only)
+            mock_write_report.assert_called_with([], invalid_only)