Repository: dagster-io/dagster (Python)

Issue:
`DagsterInvalidDefinitionError` regression introduced in version 1.6.9 for external asset names using hyphens
### Dagster version

1.6.9 and 1.6.10

### What's the issue?

I recently updated my Dagster version to 1.6.10 from 1.6.4 and started getting the following error on startup:
```
dagster._core.errors.DagsterInvalidDefinitionError: "adp-data_dp_all_ds_msci_msci_cl_munis_instr_wcu_ts" is not a valid name in Dagster. Names must be in regex ^[A-Za-z0-9_]+$.

  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_grpc/server.py", line 408, in __init__
    self._loaded_repositories: Optional[LoadedRepositories] = LoadedRepositories(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_grpc/server.py", line 242, in __init__
    loadable_targets = get_loadable_targets(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_grpc/utils.py", line 60, in get_loadable_targets
    else loadable_targets_from_python_package(package_name, working_directory)
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/workspace/autodiscovery.py", line 48, in loadable_targets_from_python_package
    module = load_python_module(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/code_pointer.py", line 134, in load_python_module
    return importlib.import_module(module_name)
  File "/usr/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/home/develop/dagster/dp_dagster/__init__.py", line 8, in <module>
    defs = Definitions(assets=assets, resources=resources, jobs=jobs, schedules=schedules)
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/definitions_class.py", line 433, in __init__
    self._created_pending_or_normal_repo = _create_repository_using_definitions_args(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/definitions_class.py", line 304, in _create_repository_using_definitions_args
    def created_repo():
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/decorators/repository_decorator.py", line 161, in __call__
    else CachingRepositoryData.from_list(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/repository_definition/repository_data.py", line 370, in from_list
    return build_caching_repository_data_from_list(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/repository_definition/repository_data_builder.py", line 266, in build_caching_repository_data_from_list
    external_asset_from_spec(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/external_asset.py", line 24, in external_asset_from_spec
    return external_assets_from_specs([spec])[0]
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/external_asset.py", line 124, in external_assets_from_specs
    def _external_assets_def(context: AssetExecutionContext) -> None:
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/decorators/asset_decorator.py", line 780, in inner
    op = _Op(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/decorators/op_decorator.py", line 127, in __call__
    op_def = OpDefinition.dagster_internal_init(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/op_definition.py", line 205, in dagster_internal_init
    return OpDefinition(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/decorator_utils.py", line 203, in wrapped_with_pre_call_fn
    return fn(*args, **kwargs)
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/op_definition.py", line 166, in __init__
    output_defs = _resolve_output_defs_from_outs(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/op_definition.py", line 483, in _resolve_output_defs_from_outs
    return [only_out.to_definition(annotation, name, description, default_code_version)]
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/output.py", line 419, in to_definition
    return klass(
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/output.py", line 74, in __init__
    self._name = check_valid_name(check.opt_str_param(name, "name", DEFAULT_OUTPUT))
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/utils.py", line 64, in check_valid_name
    check_valid_chars(name)
  File "/home/develop/dagster/.venv/lib/python3.10/site-packages/dagster/_core/definitions/utils.py", line 72, in check_valid_chars
    raise DagsterInvalidDefinitionError(
```

I performed a bisection search on the Dagster version and I have established that the regression was introduced in version 1.6.9.

The problem is due to the AssetKey containing a hyphen. It would appear that this now fails a validation check, whereas previously it didn't. The regression appears to only effect my dbt source assets. I have other assets in the project that also contain hyphens in the asset key and they are still ok.



### What did you expect to happen?

My AssetKeys containing hyphens should continue to work as the did before v1.6.9.

### How to reproduce?

Create a dagster-dbt project where the source models contain a hyphen. It should be possible to do this by creating a custom dbt dagster translator and override the `get_asset_key` method for sources.

If necessary I can create an example project and upload to github.

### Deployment type

Local

### Deployment details

_No response_

### Additional information

_No response_

### Message from the maintainers

Impacted by this issue? Give it a 👍! We factor engagement into prioritization.


Discussion:
Here is a straightforward way to reproduce:

1. Bootstrap a new project with `dagster project scaffold`
2. Copy the following into the `assets.py`
```python
from dagster import AssetKey, SourceAsset, asset, AssetSpec, external_assets_from_specs

# SourceAsset is fine with a hyphen
my_source_asset = SourceAsset(key=AssetKey(["with-hyphen", "my_source_asset"]))


# Asset is fine with a hyphen
@asset(key=AssetKey(["with-hyphen", "my_asset"]))
def my_asset():
    return [1]


# External asset throws a DagsterInvalidDefinitionError with a hyphen in Dagster 1.6.9 and 1.6.10.
my_external_asset = AssetSpec(["with-hyphen", "my_external_asset"])

external_assets = external_assets_from_specs([my_external_asset])
```
3. Run `dagster dev`




Thanks so much for root-causing this and creating a fix PR! I will triage to our engineering team for review 👍



Patch:
diff --git a/python_modules/dagster/dagster/_core/definitions/decorators/asset_decorator.py b/python_modules/dagster/dagster/_core/definitions/decorators/asset_decorator.py
index 000635ec8d5e7..1c53fc10b56b2 100644
--- a/python_modules/dagster/dagster/_core/definitions/decorators/asset_decorator.py
+++ b/python_modules/dagster/dagster/_core/definitions/decorators/asset_decorator.py
@@ -660,8 +660,7 @@ def inner(fn: Callable[..., Any]) -> AssetsDefinition:
         elif specs:
             output_tuples_by_asset_key = {}
             for asset_spec in specs:
-                # output names are asset keys joined with _
-                output_name = "_".join(asset_spec.key.path)
+                output_name = asset_spec.key.to_python_identifier()
                 output_tuples_by_asset_key[asset_spec.key] = (
                     output_name,
                     Out(


Test patch:
diff --git a/python_modules/dagster/dagster_tests/asset_defs_tests/test_assets.py b/python_modules/dagster/dagster_tests/asset_defs_tests/test_assets.py
index 719186707e60e..431bfca4f672b 100644
--- a/python_modules/dagster/dagster_tests/asset_defs_tests/test_assets.py
+++ b/python_modules/dagster/dagster_tests/asset_defs_tests/test_assets.py
@@ -1863,7 +1863,10 @@ def users():
         pass
 
     assert len(users.op.output_defs) == 2
-    assert {out_def.name for out_def in users.op.output_defs} == {"sales_users", "marketing_users"}
+    assert {out_def.name for out_def in users.op.output_defs} == {
+        "sales__users",
+        "marketing__users",
+    }
 
 
 def test_asset_spec_deps():
diff --git a/python_modules/dagster/dagster_tests/definitions_tests/test_external_assets.py b/python_modules/dagster/dagster_tests/definitions_tests/test_external_assets.py
index f917b5de48626..bf56bdb2d5a76 100644
--- a/python_modules/dagster/dagster_tests/definitions_tests/test_external_assets.py
+++ b/python_modules/dagster/dagster_tests/definitions_tests/test_external_assets.py
@@ -54,6 +54,23 @@ def test_external_asset_basic_creation() -> None:
     assert not assets_def.is_executable
 
 
+def test_external_asset_with_hyphens() -> None:
+    key = AssetKey(["with-hyphen", "external_asset_one"])
+    assets_def = next(
+        iter(
+            external_assets_from_specs(
+                specs=[
+                    AssetSpec(
+                        key=key,
+                    )
+                ]
+            )
+        )
+    )
+    assert isinstance(assets_def, AssetsDefinition)
+    assert assets_def.key == key
+
+
 def test_multi_external_asset_basic_creation() -> None:
     for assets_def in external_assets_from_specs(
         specs=[