Repository: RockefellerArchiveCenter/DACSspace

Issue:
Update "Usage" section in README
Explain what a person would need to do to run DACSspace and what the output looks like. This should include adding information about using different schemas. 

Comments:

Patch:
diff --git a/README.md b/README.md
index 3649ae3..104e9ea 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@
 # DACSspace
 
-A simple Python script to evaluate your ArchivesSpace instance for DACS [single-level minimum](http://www2.archivists.org/standards/DACS/part_I/chapter_1) required elements.
+A Python package to evaluate your ArchivesSpace instance for DACS [single-level required](https://saa-ts-dacs.github.io/dacs/06_part_I/02_chapter_01.html#single-level-required) elements.
 
-DACSspace utilizes the ArchivesSpace API to check resources for DACS compliance and produces a csv containing a list of evaluated resources. If a DACS field is present its content will be written to the csv, if a field is missing the csv will read "FALSE" for that item.
+DACSspace utilizes the ArchivesSpace API and a default JSON schema to validate resources. The output is a CSV containing a list of invalid URIs with the following fields: validation status, error count, and explanation.
+
+DACSspace also allows users to specify a schema to validate against other than the default DACS single-level required schema, see [Usage](https://github.com/RockefellerArchiveCenter/DACSspace#usage) section for more information.
 
 ## Requirements
 
@@ -10,13 +12,6 @@ DACSspace utilizes the ArchivesSpace API to check resources for DACS compliance
 *   [ArchivesSnake](https://github.com/archivesspace-labs/ArchivesSnake) (Python library) (0.9.1 or higher)
 *   Requests module
 *   JSONschema
-*   [tox](https://tox.readthedocs.io/) (for running tests)
-*   [pre-commit](https://pre-commit.com/) (for running linters before committing)
-    *   After locally installing pre-commit, install the git-hook scripts in the DACSSpace directory:
-
-    ```
-    pre-commit install
-    ```  
 
 ## Installation
 
@@ -24,9 +19,7 @@ Download and install [Python](https://www.python.org/downloads/)
 
 * If you are using Windows, add Python to your [PATH variable](https://docs.python.org/2/using/windows.html)
 
-Download or [clone](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository) this repository
-
-Install requirements from within the main DACSspace directory: ```pip install -r requirements.txt```
+Install DACSspace and its requirements: ```pip install dacsspace```
 
 ## Setup
 
@@ -44,39 +37,104 @@ pass a different filepath via the `as_config` command-line argument.
 
 ## Usage
 
-Using the command line navigate to the directory containing the DACSspace repository and run `single-level.py` to execute the script.
+DACSspace can be used as a command line utility to evaluate your ArchivesSpace repository for DACS compliance, and it can also be used as part of another Python program.
+
+Required arguments:
+- `csv_filepath`
+
+Use this argument to set the filepath to the CSV file where the output of DACSspace will print. Your CSV filepath must have a .csv extension and cannot contain the following characters: * ? : " < > | '
+
+Optional arguments:
+- `--published_only`
+- `--invalid_only`
+- `--schema_identifier`
+- `--schema_filepath`
+
+For use cases on how these optional arguments can be employed, look under the next section, Running DACSspace from the command line.
+
+### Running DACSspace from the command line
+
+In the command line, run `dacsspace`. You also need to pass in the `csv_filepath` argument with the name of your CSV filepath in order to run the script (see [above]((https://github.com/RockefellerArchiveCenter/DACSspace#usage))).
+
+You can use the different DACSspace optional arguments to decide what data DACSspace will fetch, what data it will report out on, and what schema it will validate the data against.
+
+#### What data to fetch
+
+If you plan to only evaluate DACS compliance on resources in your ArchivesSpace repository that are published, pass in the argument `--published_only` into the command line. This tells the DACSspace client class to only fetch data from published resources.
+
+#### What data to report on
+
+If you want to limit your CSV file to contain information on resources that do not meet DACS compliance, pass in the argument `--invalid_only` into the command line. This tells the DACSspace reporter class to only write information on invalid results of the validation to your CSV file.
+
+The output to your CSV will include the following field names:
+- uri: The ArchivesSpace object's unique identifier (ex. /repositories/2/resources/1234)
+- valid: A boolean indication of the validation result (True or False)
+- error_count: An integer representation of the number of validation errors (ex. 1)
+- explanation: An explanation of any validation errors (You are missing the following fields ...)
 
-DACSspace will prompt you to answer two questions allowing you to limit which resources you'd like the script to evaluate:
+If you are using Microsoft Excel to view the CSV file, consult the following links to avoid encoding issues: [Excel 2007](https://www.itg.ias.edu/content/how-import-csv-file-uses-utf-8-character-encoding-0), [Excel 2013](https://www.ias.edu/itg/how-import-csv-file-uses-utf-8-character-encoding).
 
-```
-Welcome to DACSspace!
-I'll ask you a series of questions to refine how to script works.
-If you want to use the default value for a question press the ENTER key.
+#### What schema to validate your data against
 
-Do you want DACSspace to include unpublished resources? y/n (default is n):
-Do you want to further limit the script by a specific resource id? If so, enter a string that must be present in the resource id (enter to skip):
-```
+The default JSON schema that DACSspace will run the data it fetches from your ArchivesSpace repository against is the single_level_required JSON schema. If you want to validate your data against a different schema, you have two options:
 
-Pressing the ENTER key for both questions will use the default version of the script which will get ALL resources.
+1. To run DACSspace against a schema other than single_level_required within the `schemas` directory in dacsspace, use the command line argument `--schema_identifier` and specify the identifier for that schema. The identifier must be entered in as a string.
+2. To run DACSspace against a schema that is external to dacsspace, use the command line argument `schema_filepath` and specify the filepath to this external schema. The filepath must be entered in as a string.
 
-The script will create a list of evaluated resources in a csv file (default is `dacs_singlelevel_report.csv`).
+### Using DACSspace in another Python program
 
-A sample csv file will look like this:
+Different components of the DACSspace package can be incorporated into other Python programs.
 
-| title | publish | resource | extent | date| language | repository | creator | scope | restrictions
-|---|---|---|---|---|---|---|---|---|---|
-| #resource title | TRUE | #resourceId | 20.8 | inclusive|  eng   | #NameofRepository | FALSE | #scopenote| #accessrestriction
-| #resource title | TRUE | #resourceId | 50.6 | single   |  FALSE | #NameofRepository | #creator | FALSE| FALSE
+For example, say you had a set of data that has already been exported from ArchivesSpace into another sort of container. You do not need to run the entire DACSspace package, but you do want to validate your data set against a JSON schema. To do this, add this code to your script:
 
-If you are using Microsoft Excel to view the csv file, consult the following links to avoid encoding issues: [Excel 2007](https://www.itg.ias.edu/content/how-import-csv-file-uses-utf-8-character-encoding-0), [Excel 2013](https://www.itg.ias.edu/node/985).
+`from dacsspace.validator import Validator
+
+exported_data = [{"title": "Archival object" ... }, { ...}]
+validator = Validator("single_level_required.json", None)
+results = [validator.validate_data(obj) for obj in exported_data]
+print(results)`
 
 ## Contributing
 
-Pull requests accepted!
+Found a bug? [File an issue.](https://github.com/RockefellerArchiveCenter/DACSspace/issues/new/choose)
+
+Pull requests accepted! To contribute:
+
+1. File an issue in the repository or work on an issue already documented
+2. Fork the repository and create a new branch for your work
+3. After you have completed your work, push your branch back to the repository and open a pull request
+
+### Contribution standards
+
+#### Style
+
+DACSspace uses the Python PEP8 community style guidelines. To conform to these guidelines, the following linters are part of the pre-commit:
+
+* autopep8 formats the code automatically
+* flake8 checks for style problems as well as errors and complexity
+* isort sorts imports alphabetically, and automatically separated into sections and by type
+
+After locally installing pre-commit, install the git-hook scripts in the DACSSpace directory:
+
+    ```
+    pre-commit install
+    ```  
+
+#### Documentation
+
+Docstrings should explain what a module, class, or function does by explaining its syntax and the semantics of its components. They focus on specific elements of the code, and less on how the code works. The point of docstrings is to provide information about the code you have written; what it does, any exceptions it raises, what it returns, relevant details about the parameters, and any assumptions which might not be obvious. Docstrings should describe a small segment of code and not the way the code is implemented in a larger environment.
+
+DACSspace adheres to [Google’s docstring style guide](https://google.github.io/styleguide/pyguide.html#381-docstrings). There are two types of docstrings: one-liners and multi-line docstrings. A one-line docstring may be perfectly appropriate for obvious cases where the code is immediately self-explanatory. Use multiline docstrings for all other cases.
+
+#### Tests
+
+New code should  have unit tests. Tests are written in unittest style and run using [tox](https://tox.readthedocs.io/). 
 
 ## Authors
 
-Hillel Arnold and Amy Berish
+Initial version: Hillel Arnold and Amy Berish.
+
+Version 2.0: Hillel Arnold, Amy Berish, Bonnie Gordon, Katie Martin, and Darren Young.
 
 ## License
 
diff --git a/dacsspace/client.py b/dacsspace/client.py
index b83eae7..fc19600 100644
--- a/dacsspace/client.py
+++ b/dacsspace/client.py
@@ -1,5 +1,3 @@
-from configparser import ConfigParser
-
 from asnake.aspace import ASpace
 
 
@@ -10,14 +8,12 @@ class ASnakeConfigError(Exception):
 class ArchivesSpaceClient:
     """Handles communication with ArchivesSpace."""
 
-    def __init__(self, as_config):
-        config = ConfigParser()
-        config.read(as_config)
-        self.aspace = ASpace(baseurl=config.get('ArchivesSpace', 'baseurl'),
-                             username=config.get('ArchivesSpace', 'user'),
-                             password=config.get('ArchivesSpace', 'password'))
-        self.repo = self.aspace.repositories(
-            config.get('ArchivesSpace', 'repository'))
+    def __init__(self, baseurl, username, password, repo_id):
+        self.aspace = ASpace(
+            baseurl=baseurl,
+            username=username,
+            password=password)
+        self.repo = self.aspace.repositories(repo_id)
         if isinstance(self.repo, dict):
             raise ASnakeConfigError(
                 "Error getting repository: {}".format(
diff --git a/dacsspace/dacsspace.py b/dacsspace/dacsspace.py
index 0488dc1..75a3d81 100644
--- a/dacsspace/dacsspace.py
+++ b/dacsspace/dacsspace.py
@@ -1,4 +1,5 @@
 import re
+from configparser import ConfigParser
 from os.path import isfile
 
 from .client import ArchivesSpaceClient
@@ -10,21 +11,33 @@ class DACSspace:
     """Base DACSspace class. Fetches data from AS, validates and reports results."""
 
     def __init__(self, as_config, csv_filepath):
-        """Checks csv filepath to make sure it has the proper extension and characters."""
+        """Checks CSV and AS config filepaths.
+
+        Args:
+            as_config (str): filepath to ArchivesSpace configuration file.
+            csv_filepath (str): filepath at which to save results file.
+        """
         if not csv_filepath.endswith(".csv"):
             raise ValueError("File must have .csv extension")
         if re.search(r'[*?:"<>|]', csv_filepath):
             raise ValueError(
                 'File name cannot contain the following characters: * ? : " < > | ')
         self.csv_filepath = csv_filepath
+
         if not isfile(as_config):
             raise IOError(
                 "Could not find an ArchivesSpace configuration file at {}".format(as_config))
-        self.as_config = as_config
+        config = ConfigParser()
+        config.read(as_config)
+        self.as_config = (
+            config.get('ArchivesSpace', 'baseurl'),
+            config.get('ArchivesSpace', 'user'),
+            config.get('ArchivesSpace', 'password'),
+            config.get('ArchivesSpace', 'repository'))
 
     def run(self, published_only, invalid_only,
             schema_identifier, schema_filepath):
-        client = ArchivesSpaceClient(self.as_config)
+        client = ArchivesSpaceClient(*self.as_config)
         validator = Validator(schema_identifier, schema_filepath)
         reporter = CSVReporter(self.csv_filepath)
         data = client.get_resources(published_only)
diff --git a/setup-old.py b/setup-old.py
deleted file mode 100644
index 9d23c53..0000000
--- a/setup-old.py
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env python
-
-import os
-import sys
-
-current_dir = os.path.dirname(__file__)
-
-file_path = os.path.join(current_dir, "local_settings.cfg")
-
-
-def check_response(response, yes):
-    if response != yes:
-        print("Exiting!")
-        sys.exit()
-
-
-def start_section(section_name):
-    cfg_file.write("\n[{}]\n".format(section_name))
-
-
-def write_value(name, default, value=None):
-    if value:
-        line = ("{}: {}\n".format(name, value))
-    else:
-        line = ("{}: {}\n".format(name, default))
-    cfg_file.write(line)
-
-
-def main():
-    global cfg_file
-    print("This script will create a configuration file with settings to connect and download JSON files from ArchivesSpace.\nYou\'ll need to know a few things in order to do this:\n\n1. The base URL of the backend of your ArchivesSpace installation, including the port number.\n2. The ID for the ArchivesSpace repository from which you want to export JSON files.\n3. A user name and password for a user with read writes to the ArchivesSpace repository.\n")
-    response = input("Do you want to continue? (y/n): ")
-    check_response(response, "y")
-
-    if os.path.isfile(file_path):
-        print("\nIt looks like a configuration file already exists. This script will replace that file.\n")
-        response = input("Do you want to continue? (y/n): ")
-        check_response(response, "y")
-
-    cfg_file = open(file_path, 'w+')
-    print("\nOK, let's create this file! I\'ll ask you to enter a bunch of values. If you want to use the default value you can just hit the Enter key.\n")
-    start_section("ArchivesSpace")
-    print("We\'ll start with some values for your ArchivesSpace instance.")
-    baseURL = input(
-        "Enter the base URL of your ArchivesSpace installation (default is 'http://localhost:8089'): ")
-    write_value("baseURL", "http://localhost:8089", baseURL)
-    repoId = input(
-        "Enter the repository ID for your ArchivesSpace installation (default is '2'): ")
-    write_value("repository", "2", repoId)
-    username = input(
-        "Enter the username for your ArchivesSpace installation (default is 'admin'): ")
-    write_value("user", "admin", username)
-    password = input(
-        "Enter the password associated with this username (default is 'admin'): ")
-    write_value("password", "admin", password)
-
-    start_section("Destinations")
-    print("\nNow you need to tell me where you want to save the spreadsheet that will be created. Unless you know what you\'re doing, you should probably leave the defaults in place.\n")
-    directory = input(
-        "Enter the directory in which you want to save the spreadsheet (default is the current directory): ")
-    write_value("directory", "", directory)
-    filename = input(
-        "Now tell me the filename of the CSV spreadsheet you want to create (default is 'dacs_singlelevel_report.csv'): ")
-    write_value("filename", "dacs_singlelevel_report.csv", filename)
-
-    cfg_file.close()
-
-    print("You\'re all set! I created a configuration file at {}. You can edit that file at any time, or run this script again if you want to replace those configurations.".format(file_path))
-
-    sys.exit()
-
-
-main()


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 59059bb..e7e65c7 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -1,6 +1,3 @@
-import configparser
-import os
-import shutil
 import unittest
 from unittest.mock import Mock, patch
 
@@ -8,56 +5,15 @@ from asnake.client.web_client import ASnakeAuthError
 
 from dacsspace.client import ArchivesSpaceClient, ASnakeConfigError
 
-CONFIG_FILEPATH = "as_config.cfg"
-
 
 class ArchivesSpaceClientTests(unittest.TestCase):
     def setUp(self):
         """Move existing config file and replace with sample config."""
-        if os.path.isfile(CONFIG_FILEPATH):
-            shutil.move(CONFIG_FILEPATH, "as_config.old")
-        shutil.copy("as_config.example", CONFIG_FILEPATH)
-
-    @patch("requests.Session.get")
-    @patch("requests.Session.post")
-    def test_config_file(self, mock_post, mock_get):
-        """Asserts that configuration files are correctly handled:
-            - Configuration files without all the necessary values cause an exception to be raised.
-            - Valid configuration file allows for successful instantiation of ArchivesSpaceClient class.
-        """
-        # mock returns from ASpace
-        mock_post.return_value.status_code = 200
-        mock_post.return_value.text = "{\"session\": \"12355\"}"
-        mock_get.return_value.status_code = 200
-        mock_get.return_value.text = "v3.0.2"
-
-        # Valid config file
-        ArchivesSpaceClient(CONFIG_FILEPATH)
-
-        # remove baseurl from ArchivesSpace section
-        config = configparser.ConfigParser()
-        config.read(CONFIG_FILEPATH)
-        config.remove_option('ArchivesSpace', 'baseurl')
-        with open(CONFIG_FILEPATH, "w") as cf:
-            config.write(cf)
-
-        # Configuration file missing necessary options
-        with self.assertRaises(configparser.NoOptionError) as err:
-            ArchivesSpaceClient(CONFIG_FILEPATH)
-        self.assertEqual(str(err.exception),
-                         "No option 'baseurl' in section: 'ArchivesSpace'")
-
-        # remove ArchivesSpace section
-        config = configparser.ConfigParser()
-        config.read(CONFIG_FILEPATH)
-        config.remove_section('ArchivesSpace')
-        with open(CONFIG_FILEPATH, "w") as cf:
-            config.write(cf)
-
-        # Configuration file missing necessary section
-        with self.assertRaises(configparser.NoSectionError) as err:
-            ArchivesSpaceClient(CONFIG_FILEPATH)
-        self.assertEqual(str(err.exception), "No section: 'ArchivesSpace'")
+        self.as_config = (
+            "https://sandbox.archivesspace.org/api/",
+            "admin",
+            "admin",
+            101)
 
     @patch("requests.Session.get")
     @patch("requests.Session.post")
@@ -71,14 +27,14 @@ class ArchivesSpaceClientTests(unittest.TestCase):
         # Incorrect authentication credentials
         mock_post.return_value.status_code = 403
         with self.assertRaises(ASnakeAuthError) as err:
-            ArchivesSpaceClient(CONFIG_FILEPATH)
+            ArchivesSpaceClient(*self.as_config)
         self.assertEqual(str(err.exception),
                          "Failed to authorize ASnake with status: 403")
 
         # Incorrect base URL
         mock_post.return_value.status_code = 404
         with self.assertRaises(ASnakeAuthError) as err:
-            ArchivesSpaceClient(CONFIG_FILEPATH)
+            ArchivesSpaceClient(*self.as_config)
         self.assertEqual(str(err.exception),
                          "Failed to authorize ASnake with status: 404")
 
@@ -90,7 +46,7 @@ class ArchivesSpaceClientTests(unittest.TestCase):
         mock_get.return_value.json.return_value = {
             'error': 'Repository not found'}
         with self.assertRaises(ASnakeConfigError) as err:
-            ArchivesSpaceClient(CONFIG_FILEPATH)
+            ArchivesSpaceClient(*self.as_config)
         self.assertEqual(str(err.exception),
                          "Error getting repository: Repository not found")
 
@@ -101,7 +57,7 @@ class ArchivesSpaceClientTests(unittest.TestCase):
         """Asserts that the `published_only` flag is handled correctly."""
         mock_get.return_value.text = "v3.0.2"  # Allows ArchivesSpaceClient to instantiate
         # Instantiates ArchivesSpaceClient for testing
-        client = ArchivesSpaceClient(CONFIG_FILEPATH)
+        client = ArchivesSpaceClient(*self.as_config)
 
         # Test search for only published resources
         list(client.get_resources(True))
@@ -121,7 +77,7 @@ class ArchivesSpaceClientTests(unittest.TestCase):
     def test_data(self, mock_authorize, mock_get, mock_search):
         """Asserts that individual resources are returned"""
         mock_get.return_value.text = "v3.0.2"
-        client = ArchivesSpaceClient(CONFIG_FILEPATH)
+        client = ArchivesSpaceClient(*self.as_config)
 
         # create a mocked object which acts like an
         # `asnake.jsonmodel.JSONModel` object
@@ -133,8 +89,3 @@ class ArchivesSpaceClientTests(unittest.TestCase):
         result = list(client.get_resources(True))
         self.assertEqual(len(result), 1)
         self.assertEqual(result[0], expected_return)
-
-    def tearDown(self):
-        """Replace sample config with existing config."""
-        if os.path.isfile("as_config.old"):
-            shutil.move("as_config.old", CONFIG_FILEPATH)
diff --git a/tests/test_dacsspace.py b/tests/test_dacsspace.py
index f20fef4..82c1d4c 100644
--- a/tests/test_dacsspace.py
+++ b/tests/test_dacsspace.py
@@ -1,3 +1,4 @@
+import configparser
 import os
 import shutil
 from unittest import TestCase
@@ -32,7 +33,39 @@ class TestDACSspace(TestCase):
                          "File must have .csv extension")
 
     def test_as_config(self):
-        """Asserts missing files raise an exception."""
+        """Asserts that ArchivesSpace configuration file is correctly handled:
+            - Configuration files without all the necessary values cause an exception to be raised.
+            - Valid configuration file allows for successful instantiation of DACSspace class.
+            - Missing configuration file raises exception.
+        """
+        DACSspace(CONFIG_FILEPATH, "csv_filepath.csv")
+
+        # remove baseurl from ArchivesSpace section
+        config = configparser.ConfigParser()
+        config.read(CONFIG_FILEPATH)
+        config.remove_option('ArchivesSpace', 'baseurl')
+        with open(CONFIG_FILEPATH, "w") as cf:
+            config.write(cf)
+
+        # Configuration file missing necessary options
+        with self.assertRaises(configparser.NoOptionError) as err:
+            DACSspace(CONFIG_FILEPATH, "csv_filepath.csv")
+        self.assertEqual(str(err.exception),
+                         "No option 'baseurl' in section: 'ArchivesSpace'")
+
+        # remove ArchivesSpace section
+        config = configparser.ConfigParser()
+        config.read(CONFIG_FILEPATH)
+        config.remove_section('ArchivesSpace')
+        with open(CONFIG_FILEPATH, "w") as cf:
+            config.write(cf)
+
+        # Configuration file missing necessary section
+        with self.assertRaises(configparser.NoSectionError) as err:
+            DACSspace(CONFIG_FILEPATH, "csv_filepath.csv")
+        self.assertEqual(str(err.exception), "No section: 'ArchivesSpace'")
+
+        # missing configuration file
         os.remove(CONFIG_FILEPATH)
         with self.assertRaises(IOError) as err:
             DACSspace(CONFIG_FILEPATH, "csv_filepath.csv")