Repository: dask/distributed

Issue:
Add `.get_client()` method to Cluster class
I'm sitting with @costrouc and he's typing `cluster.get_client()` based on his experience with Dask Gateway.  Should we upstream this to `dask.distributed.deploy.Cluster`?

cc @jacobtomlinson @jcrist 


Discussion:
I spoke with @jsignell and @jacobtomlinson about this.  They were hesitant about this method because it seemed magical and created a second way to do something.  My inclination was to go ahead and do it.  It seems unharmful to me.  I don't strongly care though.  cc @jcrist in case he has experience with dask-gateway
Specifically my worry is that `get_client` already exists on distributed.worker and returns an existing client that the worker is connected to. `distributed.worker.get_client` is what `get_scheduler` in dask itself relies on. That being said, I am not clear on what the impact of having multiple client object that point to the same cluster. Maybe that is fine?

I would be totally fine with this method if it is something like:

```python
class Cluster:
    ....
    def get_client(self):
        from distributed.worker import get_client as get_current_client
    
        current_client = get_current_client()
        if current_client and current_client.cluster == self:
            return current_client
        else:
            return Client(self)
```

@jsignell that sounds reasonable.

My main concern was that `client = cluster.get_client()` is just `client = Client(cluster)` and we should try to avoid having multiple equally straightforward ways to do the same thing. But if there is nuance around `distributed.worker.get_client` that definitely adds value to implementing this method.
Mostly the concern that I feel is when writing basic materials I have to ask users to import different things from different libraries.

```python
from dask_jobqueue import SLURMCluster
cluster = SLURMCluster()

from dask.distributed import Client
client = Client(cluster)
```

I remember reading Spark materials and getting confused about the Conf and Session and SQLSession objects that I had to create.  The ceremony to get a basic example running felt odd.

Adding `Cluster.get_client` would let us rework our common basic example to the following:

```python
from dask_jobqueue import SLURMCluster
client = SLURMCluster().get_client()
```

Not a big deal.  My guess is that it's less friction for early users.  Not a big deal though.
I agree that the mulitple imports isn't very elegant and would totally support an implementation like I outlined in https://github.com/dask/distributed/issues/6732#issuecomment-1187674320

Resolving patch:
diff --git a/distributed/deploy/cluster.py b/distributed/deploy/cluster.py
--- a/distributed/deploy/cluster.py
+++ b/distributed/deploy/cluster.py
@@ -321,6 +321,22 @@ def get_logs(self, cluster=True, scheduler=True, workers=True):
     def logs(self, *args, **kwargs):
         return self.get_logs(*args, **kwargs)
 
+    def get_client(self):
+        """Return client for the cluster
+
+        If a client has already been initialized for the cluster, return that
+        otherwise initialize a new client object.
+        """
+        from distributed.client import Client
+
+        try:
+            current_client = Client.current()
+            if current_client and current_client.cluster == self:
+                return current_client
+        except ValueError:
+            pass
+        return Client(self)
+
     @property
     def dashboard_link(self):
         try:


Test patch:
diff --git a/distributed/deploy/tests/test_local.py b/distributed/deploy/tests/test_local.py
--- a/distributed/deploy/tests/test_local.py
+++ b/distributed/deploy/tests/test_local.py
@@ -1267,3 +1267,14 @@ def test_localcluster_start_exception(loop):
             loop=loop,
         ):
             pass
+
+
+def test_localcluster_get_client():
+    with LocalCluster(
+        n_workers=0, asynchronous=False, dashboard_address=":0"
+    ) as cluster:
+        client1 = cluster.get_client()
+        assert client1.cluster == cluster
+        client2 = Client(cluster)
+        assert client1 != client2
+        assert client2 == cluster.get_client()