Repository: dbt-labs/dbt-core

Issue:
[CT-851] [Feature] Friendlier error messages when packages.yml is malformed
### Is this your first time opening an issue?

- [X] I have read the [expectations for open source contributors](https://docs.getdbt.com/docs/contributing/oss-expectations)

### Describe the Feature

I just tried to freehand install a package. It went horribly. 

## Mistake 1: No version defined for a Hub-hosted package
```yaml
packages:
  - package: dbt_utils
```
```
(dbt-prod) joel@Joel-Labes joel-sandbox % dbt clean && dbt deps
00:41:46  Encountered an error while reading the project:
00:41:46    ERROR: Runtime Error
  The packages.yml file in this project is malformed. Please double check
  the contents of this file and fix any errors before retrying.
  
  You can find more information on the syntax for this file here:
  https://docs.getdbt.com/docs/package-management
  
  Validator Error:
  {'package': 'dbt_utils'} is not valid under any of the given schemas
```
### Instead: 
I would have expected it to say something like "When installing from the package index, `version` is a required property". I know that it's technically possible that they provided `version: 0.8.6` as the singular property and not `package`, but that seems way less likely so even if we just protect against the most common issue that'd be good. 

## Mistake 2: No company namespace 
```yaml
packages:
  - package: dbt_utils
    version: ">0.8.0"
```
```
00:45:50  Encountered an error:
Package dbt_utils was not found in the package index
```
### Instead:
If there's not a `/` in the package name, we could say "dbt_utils was not found in the package index. Packages on the index require a namespace, e.g. `dbt-labs/dbt_utils`" (note that this would be a hardcoded example, it hasn't magically gone and worked out which package we meant. But it sure is a good guess!)

## Eventually: 
I clicked through to the correct syntax from the error message, so thanks for including the link to that! 

### Describe alternatives you've considered

_No response_

### Who will this benefit?

People with hubris

### Are you interested in contributing this feature?

Yep! I think I can deal with part 2 myself, but will probably need a pointer on the first part

### Anything else?

_No response_


Discussion:
Thanks for opening @joellabes! Such a good exercise to wind ourselves back to an earlier set of knowledge and assumptions, and see how we might fare.

> People with hubris

For trying to install software without errors, the gods will never forgive us

> Yep! I think I can deal with part 2 myself, but will probably need a pointer on the first part

The error message that's being raised comes from dbt's (hologram's) not-so-friendly dataclass validation. Specifically, when `package` isn't supplied, it raises an error because it's not an "optional" field:

https://github.com/dbt-labs/dbt-core/blob/e03d35a9fc606f5b1fc7e94c0bc04dbe88f558cf/core/dbt/contracts/project.py#L74-L78

Unfortunately, the exact code to get this working is escaping me, so we'll need the Language team to advise on that piece.

I'd hope for something like:
```python
@dataclass
class RegistryPackage(Package):
    package: str
    version: Optional[Union[RawVersion, List[RawVersion]]] = None
    install_prerelease: Optional[bool] = False

    ...
            
    @classmethod
    def validate(cls, data):
        if not data.get('version'):
            raise ValidationError("All Hub packages must have versions")
            # or, should this just always install 'latest'?
            # that's a feature request :)
        super().validate(data)
```
But that doesn't seem to do the trick. Nor does switching out `validate` for `__pre_deserialize__`.

Resolving patch:
diff --git a/core/dbt/contracts/project.py b/core/dbt/contracts/project.py
--- a/core/dbt/contracts/project.py
+++ b/core/dbt/contracts/project.py
@@ -91,6 +91,23 @@ def get_versions(self) -> List[str]:
 class PackageConfig(dbtClassMixin, Replaceable):
     packages: List[PackageSpec]
 
+    @classmethod
+    def validate(cls, data):
+        for package in data.get("packages", data):
+            if isinstance(package, dict) and package.get("package"):
+                if not package["version"]:
+                    raise ValidationError(
+                        f"{package['package']} is missing the version. When installing from the Hub "
+                        "package index, version is a required property"
+                    )
+
+                if "/" not in package["package"]:
+                    raise ValidationError(
+                        f"{package['package']} was not found in the package index. Packages on the index "
+                        "require a namespace, e.g dbt-labs/dbt_utils"
+                    )
+        super().validate(data)
+
 
 @dataclass
 class ProjectPackageMetadata:


Test patch:
diff --git a/test/unit/test_deps.py b/test/unit/test_deps.py
--- a/test/unit/test_deps.py
+++ b/test/unit/test_deps.py
@@ -22,7 +22,6 @@
 
 from dbt.dataclass_schema import ValidationError
 
-
 class TestLocalPackage(unittest.TestCase):
     def test_init(self):
         a_contract = LocalPackage.from_dict({'local': '/path/to/package'})
@@ -654,3 +653,27 @@ def test_dependency_resolution_allow_prerelease(self):
         resolved = resolve_packages(package_config.packages, mock.MagicMock(project_name='test'))
         self.assertEqual(resolved[0].name, 'dbt-labs-test/a')
         self.assertEqual(resolved[0].version, '0.1.4a1')
+
+    def test_validation_error_when_version_is_missing_from_package_config(self):
+        
+        packages_data = {"packages": [{'package': 'dbt-labs-test/b', 'version': None}]}
+        
+        with self.assertRaises(ValidationError) as exc:
+            a = PackageConfig.validate(data=packages_data)
+
+        msg = (
+            "dbt-labs-test/b is missing the version. When installing from the Hub package index, version is a required property"
+        )
+        assert msg in str(exc.exception)
+
+    def test_validation_error_when_namespace_is_missing_from_package_config(self):
+        
+        packages_data = {"packages": [{'package': 'dbt-labs', 'version': '1.0.0'}]}
+        
+        with self.assertRaises(ValidationError) as exc:
+            a = PackageConfig.validate(data=packages_data)
+
+        msg = (
+            "dbt-labs was not found in the package index. Packages on the index require a namespace, e.g dbt-labs/dbt_utils"
+        )
+        assert msg in str(exc.exception)