Repository: dbt-labs/dbt-core

Issue:
Running a snapshot with missing required configurations results in uncaught Python exception
### Describe the bug
In an attempt to educate myself on how snapshots work, I created a snapshot to test out, but neglected to realize that the configuration object was required. When I ran `dbt snapshot`, it failed with a fairly non-actionable error message:

```
2021-05-20T22:41:00.291112Z: uncaught python exception
'NoneType' object has no attribute 'lower' Code: -32000
```

### Steps To Reproduce
1. Create a snapshot without a config object, i.e.
```
{% snapshot snap_demo %}

select * from {{ ref('snapshot_demo_source') }}

{% endsnapshot %}
```
2. Run `dbt snapshot`

### Expected behavior
It would be helpful to get an error message that indicates what actually went wrong, and guides the user to provide required configurations

### Screenshots and log output
If applicable, add screenshots or log output to help explain your problem.

### System information
**Which database are you using dbt with?**
- [ ] postgres
- [ ] redshift
- [ ] bigquery
- [X] snowflake
- [ ] other (specify: ____________)


**The output of `dbt --version`:**
```
0.19.1
```

**The operating system you're using:**
MacOS

**The output of `python --version`:**

### Additional context
Add any other context about the problem here.



Discussion:
@jnatkins Such a good issue, and a great catch, thank you for opening!

I think there's a pretty straightforward resolution here, which is adding a check to the `validate` method of `SnapshotConfig`:

https://github.com/fishtown-analytics/dbt/blob/e775f2b38e52e8b9f25f8ac98c942d22b2c8c55d/core/dbt/contracts/graph/model_config.py#L448-L485

This method already exists to ensure that snapshots with certain strategies (`timestamp`, `check`) also contain the required associated configurations. We should first just check to make sure the [three required configurations](https://docs.getdbt.com/docs/building-a-dbt-project/snapshots#snapshot-configurations) are present. Something like:
```python
        if not data.get('strategy') or not data.get('unique_key') or not data.get('target_schema'):
            raise ValidationError(
                "Snapshots must be configured with a 'strategy', 'unique_key', and 'target_schema'.")
```

So instead of `'NoneType' object has no attribute 'lower'`, you'd get:
```
Compilation Error in snapshot my_snap (snapshots/my_snap.sql)
  at path []: Snapshots must be configured with a 'strategy', 'unique_key', and 'target_schema'.
```

Whether those configs are set via in-file `config()` blocks, or in `dbt_project.yml`, doesn't much matter. They just need to be set one way or another.

I'm going to tag this as a `good first issue`. Is this a fix you (or anyone else) might be interested in contributing? :)

Resolving patch:
diff --git a/core/dbt/contracts/graph/model_config.py b/core/dbt/contracts/graph/model_config.py
--- a/core/dbt/contracts/graph/model_config.py
+++ b/core/dbt/contracts/graph/model_config.py
@@ -457,6 +457,11 @@ class SnapshotConfig(EmptySnapshotConfig):
     @classmethod
     def validate(cls, data):
         super().validate(data)
+        if not data.get('strategy') or not data.get('unique_key') or not \
+                data.get('target_schema'):
+            raise ValidationError(
+                "Snapshots must be configured with a 'strategy', 'unique_key', "
+                "and 'target_schema'.")
         if data.get('strategy') == 'check':
             if not data.get('check_cols'):
                 raise ValidationError(


Test patch:
diff --git a/test/integration/004_simple_snapshot_test/test_simple_snapshot.py b/test/integration/004_simple_snapshot_test/test_simple_snapshot.py
--- a/test/integration/004_simple_snapshot_test/test_simple_snapshot.py
+++ b/test/integration/004_simple_snapshot_test/test_simple_snapshot.py
@@ -531,7 +531,7 @@ def test__postgres__invalid(self):
         with self.assertRaises(dbt.exceptions.CompilationException) as exc:
             self.run_dbt(['compile'], expect_pass=False)
 
-        self.assertIn('Compilation Error in model ref_snapshot', str(exc.exception))
+        self.assertIn('Snapshots must be configured with a \'strategy\'', str(exc.exception))
 
 
 class TestCheckCols(TestSimpleSnapshotFiles):
diff --git a/test/unit/test_contracts_graph_parsed.py b/test/unit/test_contracts_graph_parsed.py
--- a/test/unit/test_contracts_graph_parsed.py
+++ b/test/unit/test_contracts_graph_parsed.py
@@ -1343,6 +1343,22 @@ def test_invalid_missing_check_cols(basic_check_snapshot_config_dict):
     del wrong_fields['check_cols']
     with pytest.raises(ValidationError, match=r"A snapshot configured with the check strategy"):
         SnapshotConfig.validate(wrong_fields)
+        
+def test_missing_snapshot_configs(basic_check_snapshot_config_dict):
+    wrong_fields = basic_check_snapshot_config_dict
+    del wrong_fields['strategy']
+    with pytest.raises(ValidationError, match=r"Snapshots must be configured with a 'strategy'"):
+        SnapshotConfig.validate(wrong_fields)
+    
+    wrong_fields['strategy'] = 'timestamp'
+    del wrong_fields['unique_key']
+    with pytest.raises(ValidationError, match=r"Snapshots must be configured with a 'strategy'"):
+        SnapshotConfig.validate(wrong_fields)
+        
+    wrong_fields['unique_key'] = 'id'
+    del wrong_fields['target_schema']
+    with pytest.raises(ValidationError, match=r"Snapshots must be configured with a 'strategy'"):
+        SnapshotConfig.validate(wrong_fields)
 
 
 def test_invalid_check_value(basic_check_snapshot_config_dict):