Repository: PrefectHQ/prefect

Issue:
Prefect serializes flows based on flow name ignoring project
Hi,

I searched the slack channel and did not find anything that pertained directly to this.  We use Prefect for two departments that have virtually the same flows, but different sources and different processing rules.  What we have found is that flows with the same name overwrite each other regardless of the project.  I am not sure if that is intended or if there is a way to work around this issue.

This is a minimal example that I confirmed in a fresh install of Prefect 0.14.0 running the server backend in docker with a local agent all on the same machine.

```
jaykae@DT-JKNICKERBOCKER: ~: ls -1 prefect_test prefect_test/proj*
prefect_test:
proj1
proj2

prefect_test/proj1:
WriteHello.py

prefect_test/proj2:
WriteHello.py
```
Just a folder with two subfolders, projects 1 and 2, each folder holds a file with nearly the same content.  Note the file paths are different and also the project names are different.

**proj1/WriteHello.py**
```
jaykae@DT-JKNICKERBOCKER: ~/prefect_test cat proj1/WriteHello.py
import prefect
from prefect import task, Flow

@task
def write_hello():
        with open("proj1/hello.txt","w") as hello_file:
                hello_file.write("Hello Project 1!!!")

with Flow("WriteHello") as flow:
        write_it = write_hello()

flow.register(project_name="Project1")
```

**proj2/WriteHello.py**
```
jaykae@DT-JKNICKERBOCKER: ~/prefect_test cat proj2/WriteHello.py
import prefect
from prefect import task, Flow

@task
def write_hello():
        with open("proj2/hello.txt","w") as hello_file:
                hello_file.write("Hello Project 2!!!")

with Flow("WriteHello") as flow:
        write_it = write_hello()

flow.register(project_name="Project2")
```

I register proj1/HelloWorld.py and then proj2/HelloWorld.py, I would expect that registering two flows with different file paths, as well as different project names would lead result in writing a text file in the directory for each flow when it is called.  However, when I run proj1/HelloWorld.py the file is written in the proj2 directory and says Hello Project 2!!!.

```
jaykae@DT-JKNICKERBOCKER: ~/prefect_test ls proj*
proj1:
WriteHello.py

proj2:
WriteHello.py
jaykae@DT-JKNICKERBOCKER: ~/prefect_test prefect run flow --name WriteHello --project Project1
Flow Run: http://localhost:8080/default/flow-run/e05b8f99-945d-4954-ab0d-7e5e89df0965
jaykae@DT-JKNICKERBOCKER: ~/prefect_test ls proj*
proj1:
WriteHello.py

proj2:
WriteHello.py  hello.txt
```

I tried changing the .py file name and that did not help at all, so I assume it has to do with the flow name.  Is there a way to have two flows with the same name, but different content, in separate projects and peacefully coexist?


Discussion:
After more digging it looks like this code here is where the magic happens?
[prefect/src/prefect/storage/local.py](https://github.com/PrefectHQ/prefect/blob/98c46d0a3f96a9998c3460ad32b43a731a06341d/src/prefect/storage/local.py#L141
)
Would it be as simple as checking if the project is set and including that in the file name?  I'm happy to create a PR, but before I do, I would like to make sure I'm not heading in the wrong direction.
I'm not sure what the best solution for this is in the long run, I imagine one of the maintainers of this repo will have a stronger opinion.

In the meantime, you can definitely just change the flow storage directories so they don't overlap e.g.

```python
flow.storage = Local(directory=os.path.join(prefect.config.home_dir, "flows", "project1"))
```
Related to #1913. @jkknickerbocker we could make this change to use both the flow name and the project name as a default for the local storage. In the meantime, similar to what @madkinsz said you can specify different directories to store your flows or give your flows different names.
Thank you both!

I went the route of updating my storage location (it was tedious for 50+ flows), but if we can include this as an enhancement (I might attempt to implement it) I think it would save some future headaches.

Resolving patch:
diff --git a/src/prefect/storage/local.py b/src/prefect/storage/local.py
--- a/src/prefect/storage/local.py
+++ b/src/prefect/storage/local.py
@@ -2,6 +2,8 @@
 import socket
 from typing import TYPE_CHECKING, Any, Dict, List
 
+import cloudpickle
+import pendulum
 from slugify import slugify
 
 import prefect
@@ -99,7 +101,8 @@ def get_flow(self, flow_location: str = None) -> "Flow":
                 if self.stored_as_script:
                     return extract_flow_from_file(file_path=flow_location)
                 else:
-                    return prefect.core.flow.Flow.load(flow_location)
+                    with open(flow_location, "rb") as f:
+                        return cloudpickle.load(f)
             # otherwise the path is given in the module format
             else:
                 return extract_flow_from_module(module_str=flow_location)
@@ -138,9 +141,13 @@ def add_flow(self, flow: "Flow") -> str:
                 flow_location = self.path
             else:
                 flow_location = os.path.join(
-                    self.directory, "{}.prefect".format(slugify(flow.name))
+                    self.directory,
+                    slugify(flow.name),
+                    slugify(pendulum.now("utc").isoformat()),
                 )
-            flow_location = flow.save(flow_location)
+            os.makedirs(os.path.dirname(flow_location), exist_ok=True)
+            with open(flow_location, "wb") as f:
+                cloudpickle.dump(flow, f)
 
         self.flows[flow.name] = flow_location
         self._flows[flow.name] = flow


Test patch:
diff --git a/tests/storage/test_local_storage.py b/tests/storage/test_local_storage.py
--- a/tests/storage/test_local_storage.py
+++ b/tests/storage/test_local_storage.py
@@ -1,6 +1,5 @@
 import os
 import socket
-import tempfile
 
 import cloudpickle
 import pytest
@@ -41,21 +40,22 @@ def test_create_local_storage_without_validation():
     assert storage.result.dir == storage.directory
 
 
-def test_add_flow_to_storage():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        storage = Local(directory=tmpdir)
-        f = Flow("test")
-        assert f.name not in storage
+def test_add_flow_to_storage(tmpdir):
+    storage = Local(directory=str(tmpdir))
+    f = Flow("test")
+    assert f.name not in storage
 
-        res = storage.add_flow(f)
-        assert res.endswith("test.prefect")
-        assert f.name in storage
+    res = storage.add_flow(f)
 
-        with open(os.path.join(tmpdir, "test.prefect"), "rb") as f:
-            wat = f.read()
+    flow_dir = os.path.join(tmpdir, "test")
+    assert os.path.exists(flow_dir)
+    assert len(os.listdir(flow_dir)) == 1
+    assert res.startswith(flow_dir)
 
-    assert isinstance(wat, bytes)
-    assert cloudpickle.loads(wat).name == "test"
+    assert f.name in storage
+
+    f2 = storage.get_flow(res)
+    assert f2.name == "test"
 
 
 def test_add_flow_file_to_storage(tmpdir):
@@ -79,14 +79,13 @@ def test_add_flow_file_to_storage(tmpdir):
     assert f.name in storage
 
 
-def test_add_flow_raises_if_name_conflict():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        storage = Local(directory=tmpdir)
-        f = Flow("test")
-        res = storage.add_flow(f)
-        g = Flow("test")
-        with pytest.raises(ValueError, match='name "test"'):
-            storage.add_flow(g)
+def test_add_flow_raises_if_name_conflict(tmpdir):
+    storage = Local(directory=str(tmpdir))
+    f = Flow("test")
+    res = storage.add_flow(f)
+    g = Flow("test")
+    with pytest.raises(ValueError, match='name "test"'):
+        storage.add_flow(g)
 
 
 def test_get_env_runner_raises():
@@ -102,13 +101,12 @@ def test_get_flow_raises_if_flow_not_present():
         s.get_flow("test")
 
 
-def test_get_flow_returns_flow():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        storage = Local(directory=tmpdir)
-        f = Flow("test")
-        loc = storage.add_flow(f)
-        runner = storage.get_flow(loc)
-        assert runner == f
+def test_get_flow_returns_flow(tmpdir):
+    storage = Local(directory=str(tmpdir))
+    f = Flow("test")
+    loc = storage.add_flow(f)
+    runner = storage.get_flow(loc)
+    assert runner == f
 
 
 def test_get_flow_from_file_returns_flow(tmpdir):
@@ -127,81 +125,73 @@ def test_get_flow_from_file_returns_flow(tmpdir):
     assert flow.run()
 
 
-def test_containment():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        s = Local(directory=tmpdir)
-        f = Flow("test")
-        s.add_flow(f)
+def test_containment(tmpdir):
+    s = Local(directory=str(tmpdir))
+    f = Flow("test")
+    s.add_flow(f)
 
-        assert True not in s
-        assert f not in s
-        assert "test" in s
-        assert Flow("other") not in s
-        assert "other" not in s
+    assert True not in s
+    assert f not in s
+    assert "test" in s
+    assert Flow("other") not in s
+    assert "other" not in s
 
 
-def test_build_returns_self():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        s = Local(directory=tmpdir)
-        assert s.build() is s
+def test_build_returns_self(tmpdir):
+    s = Local(directory=str(tmpdir))
+    assert s.build() is s
 
-        f = Flow("test")
-        s.add_flow(f)
-        assert s.build() is s
+    f = Flow("test")
+    s.add_flow(f)
+    assert s.build() is s
 
 
-def test_multiple_flows_in_storage():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        s = Local(directory=tmpdir)
-        f = Flow("test")
-        g = Flow("other")
-        z = Flow("not")
-        f_loc = s.add_flow(f)
-        g_loc = s.add_flow(g)
+def test_multiple_flows_in_storage(tmpdir):
+    s = Local(directory=str(tmpdir))
+    f = Flow("test")
+    g = Flow("other")
+    z = Flow("not")
+    f_loc = s.add_flow(f)
+    g_loc = s.add_flow(g)
 
-        assert "test" in s
-        assert "other" in s
-        assert "not" not in s
+    assert "test" in s
+    assert "other" in s
+    assert "not" not in s
 
-        assert s.get_flow(f_loc) == f
-        assert s.get_flow(g_loc) == g
+    assert s.get_flow(f_loc) == f
+    assert s.get_flow(g_loc) == g
 
-        assert s.flows["test"] == f_loc
-        assert s.flows["other"] == g_loc
+    assert s.flows["test"] == f_loc
+    assert s.flows["other"] == g_loc
 
 
-def test_add_flow_with_weird_name_is_cleaned():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        s = Local(directory=tmpdir)
-        flow = Flow("WELL what do you know?!~? looks like a test!!!!")
-        loc = s.add_flow(flow)
-        assert "?" not in loc
-        assert "!" not in loc
-        assert " " not in loc
-        assert "~" not in loc
+def test_add_flow_with_weird_name_is_cleaned(tmpdir):
+    s = Local(directory=str(tmpdir))
+    flow = Flow("WELL what do you know?!~? looks like a test!!!!")
+    loc = s.add_flow(flow)
+    assert "?" not in loc
+    assert "!" not in loc
+    assert " " not in loc
+    assert "~" not in loc
 
 
-def test_build_healthchecks():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        s = Local(directory=tmpdir)
-        flow = Flow("TestFlow")
-        s.add_flow(flow)
-        assert s.build()
+def test_build_healthchecks(tmpdir):
+    s = Local(directory=str(tmpdir))
+    flow = Flow("TestFlow")
+    s.add_flow(flow)
+    assert s.build()
 
 
-def test_build_healthcheck_returns_on_no_flows():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        s = Local(directory=tmpdir)
-        assert s.build()
+def test_build_healthcheck_returns_on_no_flows(tmpdir):
+    s = Local(directory=str(tmpdir))
+    assert s.build()
 
 
-def test_labels_includes_hostname():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        s = Local(directory=tmpdir)
-        assert socket.gethostname() in s.labels
+def test_labels_includes_hostname(tmpdir):
+    s = Local(directory=str(tmpdir))
+    assert socket.gethostname() in s.labels
 
 
-def test_opt_out_of_hostname_label():
-    with tempfile.TemporaryDirectory() as tmpdir:
-        s = Local(directory=tmpdir, add_default_labels=False)
-        assert socket.gethostname() not in s.labels
+def test_opt_out_of_hostname_label(tmpdir):
+    s = Local(directory=str(tmpdir), add_default_labels=False)
+    assert socket.gethostname() not in s.labels