Repository: PrefectHQ/prefect

Issue:
Put parameters into context inside of a flow run
When a flow runner is in a cloud context it makes a call to the client's `get_flow_run_info` function. That call queries for the parameters, version, and current_state. The parameters however are not used. Inside the flow runner it can take those parameters (from its `flow_run_info` object that it creates) and set it in the context here -> https://github.com/PrefectHQ/prefect/blob/master/src/prefect/engine/flow_runner.py#L189 if there are no other parameters passed into the runner.


Discussion:

Resolving patch:
diff --git a/src/prefect/client/client.py b/src/prefect/client/client.py
--- a/src/prefect/client/client.py
+++ b/src/prefect/client/client.py
@@ -36,37 +36,32 @@ class Client:
     token will only be present in the current context.
 
     Args:
-        - token (str, optional): Authentication token server connection
+        - api_server (str, optional): the URL to send all basic POST requests
+            to; if not provided, will be pulled from `cloud.api` config var
+        - graphql_server (str, optional): the URL to send all GraphQL requests
+            to; if not provided, will be pulled from `cloud.graphql` config var
     """
 
-    def __init__(self, token: str = None) -> None:
-        api_server = prefect.config.cloud.get("api", None)
-
+    def __init__(self, api_server: str = None, graphql_server: str = None) -> None:
         if not api_server:
-            raise ValueError("Could not determine API server.")
-
+            api_server = prefect.config.cloud.get("api", None)
+            if not api_server:
+                raise ValueError("Could not determine API server.")
         self.api_server = api_server
 
-        graphql_server = prefect.config.cloud.get("graphql", None)
-
-        # Default to the API server
         if not graphql_server:
-            graphql_server = api_server
-
+            graphql_server = prefect.config.cloud.get("graphql") or self.api_server
         self.graphql_server = graphql_server
 
-        if token is None:
-            token = prefect.config.cloud.get("auth_token", None)
+        token = prefect.config.cloud.get("auth_token", None)
 
-            if token is None:
-                token_path = os.path.expanduser("~/.prefect/.credentials/auth_token")
-                if os.path.exists(token_path):
-                    with open(token_path, "r") as f:
-                        token = f.read()
+        if token is None:
+            token_path = os.path.expanduser("~/.prefect/.credentials/auth_token")
+            if os.path.exists(token_path):
+                with open(token_path, "r") as f:
+                    token = f.read()
 
         self.token = token
-        if self.token is None and prefect.config.get("prefect_cloud"):
-            self.login()
 
     # -------------------------------------------------------------------------
     # Utilities
@@ -185,8 +180,8 @@ def request_fn() -> "requests.models.Response":
 
     def login(
         self,
-        email: str = None,
-        password: str = None,
+        email: str,
+        password: str,
         account_slug: str = None,
         account_id: str = None,
     ) -> None:
@@ -194,10 +189,8 @@ def login(
         Login to the server in order to gain access
 
         Args:
-            - email (str): User's email on the platform; if not provided, pulled
-                from config
-            - password (str): User's password on the platform; if not provided,
-                pulled from config
+            - email (str): User's email on the platform
+            - password (str): User's password on the platform
             - account_slug (str, optional): Slug that is unique to the user
             - account_id (str, optional): Specific Account ID for this user to use
 
@@ -208,9 +201,6 @@ def login(
         # lazy import for performance
         import requests
 
-        email = email or prefect.config.cloud.email
-        password = password or prefect.config.cloud.password
-
         url = os.path.join(self.api_server, "login_email")
         response = requests.post(
             url,
@@ -251,6 +241,48 @@ def refresh_token(self) -> None:
         )
         self.token = response.json().get("token")
 
+    def deploy(
+        self, flow: "Flow", project_id: str, set_schedule_active: bool = False
+    ) -> GraphQLResult:
+        """
+        Push a new Flow to the database.
+
+        Args:
+            - flow (Flow): the prefect Flow to insert into the database
+            - project_id (str): the project ID to associate this Flow with (note
+                that this can be changed later)
+            - set_schedule_active (bool, optional): if `True`, will set the
+                schedule to active in the database and begin scheduling runs (if the Flow has a schedule).
+                Defaults to `False`
+
+        Returns:
+            - GraphQLResult: information about the newly created flow (e.g., its "id")
+        """
+        create_mutation = {
+            "mutation($input: createFlowInput!)": {
+                "createFlow(input: $input)": {"flow": {"id"}}
+            }
+        }
+        schedule_mutation = {
+            "mutation($input: setFlowScheduleIsActiveInput!)": {
+                "setFlowScheduleIsActive(input: $input)": {"flow": {"id"}}
+            }
+        }
+        res = self.graphql(
+            parse_graphql(create_mutation),
+            input=dict(
+                projectId=project_id, serializedFlow=json.dumps(flow.serialize())
+            ),
+        )
+        if set_schedule_active:
+            scheduled_res = self.graphql(
+                parse_graphql(schedule_mutation),
+                input=dict(
+                    flowId=res.createFlow.flow.id, isActive=True  # type: ignore
+                ),
+            )
+        return res.createFlow.flow  # type: ignore
+
     def get_flow_run_info(
         self, flow_run_id: str, result_handler: ResultHandler = None
     ) -> GraphQLResult:
diff --git a/src/prefect/engine/flow_runner.py b/src/prefect/engine/flow_runner.py
--- a/src/prefect/engine/flow_runner.py
+++ b/src/prefect/engine/flow_runner.py
@@ -160,10 +160,10 @@ def run(
             - ValueError: if any throttle values are `<= 0`
         """
 
-        state = state or Pending()
         context = context or {}
         return_tasks = set(return_tasks or [])
         executor = executor or DEFAULT_EXECUTOR
+        parameters = parameters or {}
         throttle = throttle or self.flow.throttle
         if min(throttle.values(), default=1) <= 0:
             bad_tags = ", ".join(
@@ -176,13 +176,22 @@ def run(
             )
 
         # Initialize CloudHandler and get flow run version
+        db_state = None
         if config.get("prefect_cloud", None):
             flow_run_info = self.client.get_flow_run_info(
                 flow_run_id=prefect.context.get("flow_run_id", ""),
                 result_handler=self.flow.result_handler,
             )
             context.update(_flow_run_version=flow_run_info.version)  # type: ignore
+            db_state = flow_run_info.state  # type: ignore
 
+            ## update parameters, prioritizing kwarg-provided params
+            db_parameters = flow_run_info.parameters or {}  # type: ignore
+            for key, value in db_parameters:  # type: ignore
+                if key not in parameters:
+                    parameters[key] = value
+
+        state = state or db_state or Pending()  # needs to remain below cloud check
         if return_tasks.difference(self.flow.tasks):
             raise ValueError("Some tasks in return_tasks were not found in the flow.")
 
diff --git a/src/prefect/utilities/logging.py b/src/prefect/utilities/logging.py
--- a/src/prefect/utilities/logging.py
+++ b/src/prefect/utilities/logging.py
@@ -19,7 +19,6 @@ def emit(self, record):
             from prefect.client import Client
 
             self.client = Client()
-            self.client.login(email=config.email, password=config.password)
         r = self.client.post(path="", server=self.logger_server, **record.__dict__)
 
 


Test patch:
diff --git a/tests/client/test_client.py b/tests/client/test_client.py
--- a/tests/client/test_client.py
+++ b/tests/client/test_client.py
@@ -70,7 +70,7 @@ def test_client_initializes_from_config():
     assert client.token == "token"
 
 
-def test_client_token_initializes_from_kwarg():
+def test_client_initializes_and_prioritizes_kwargs():
     with set_temporary_config(
         {
             "cloud.api": "api_server",
@@ -78,10 +78,20 @@ def test_client_token_initializes_from_kwarg():
             "cloud.auth_token": "token",
         }
     ):
-        client = Client(token="init-token")
+        client = Client(api_server="my-api", graphql_server="my-graphql")
+    assert client.api_server == "my-api"
+    assert client.graphql_server == "my-graphql"
+    assert client.token == "token"
+
+
+def test_client_graphql_server_falls_back_to_api_server():
+    with set_temporary_config(
+        {"cloud.api": "api_server", "cloud.graphql": None, "cloud.auth_token": "token"}
+    ):
+        client = Client()
     assert client.api_server == "api_server"
-    assert client.graphql_server == "graphql_server"
-    assert client.token == "init-token"
+    assert client.graphql_server == "api_server"
+    assert client.token == "token"
 
 
 def test_client_token_initializes_from_file(monkeypatch):
@@ -122,7 +132,8 @@ def test_client_logs_in_and_saves_token(monkeypatch):
     )
 
 
-def test_client_logs_in_automatically(monkeypatch):
+@pytest.mark.parametrize("cloud", [True, False])
+def test_client_doesnt_login_if_no_tokens_available(monkeypatch, cloud):
     post = MagicMock(
         return_value=MagicMock(
             ok=True, json=MagicMock(return_value=dict(token="secrettoken"))
@@ -134,50 +145,15 @@ def test_client_logs_in_automatically(monkeypatch):
     with set_temporary_config(
         {
             "cloud.api": "http://my-cloud.foo",
-            "prefect_cloud": True,
+            "prefect_cloud": cloud,
             "cloud.auth_token": None,
             "cloud.email": "test@example.com",
             "cloud.password": "1234",
         }
     ):
         client = Client()
-    assert post.called
-    assert post.call_args[0][0] == "http://my-cloud.foo/login_email"
-    assert post.call_args[1]["auth"] == ("test@example.com", "1234")
-    assert client.token == "secrettoken"
-    assert mock_file.call_args[0] == (
-        os.path.expanduser("~/.prefect/.credentials/auth_token"),
-        "w+",
-    )
-
-
-def test_client_logs_in_from_config_credentials(monkeypatch):
-    post = MagicMock(
-        return_value=MagicMock(
-            ok=True, json=MagicMock(return_value=dict(token="secrettoken"))
-        )
-    )
-    monkeypatch.setattr("os.path.exists", MagicMock(return_value=True))
-    mock_file = mock_open()
-    monkeypatch.setattr("builtins.open", mock_file)
-    monkeypatch.setattr("requests.post", post)
-    with set_temporary_config(
-        {
-            "cloud.api": "http://my-cloud.foo",
-            "cloud.email": "test@example.com",
-            "cloud.password": "1234",
-        }
-    ):
-        client = Client()
-        client.login()
-    assert post.called
-    assert post.call_args[0][0] == "http://my-cloud.foo/login_email"
-    assert post.call_args[1]["auth"] == ("test@example.com", "1234")
-    assert client.token == "secrettoken"
-    assert mock_file.call_args[0] == (
-        os.path.expanduser("~/.prefect/.credentials/auth_token"),
-        "w+",
-    )
+    assert not post.called
+    assert client.token is None
 
 
 def test_client_logs_out_and_deletes_auth_token(monkeypatch):
@@ -224,8 +200,10 @@ def test_client_posts_to_api_server(monkeypatch):
         return_value=MagicMock(json=MagicMock(return_value=dict(success=True)))
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.api": "http://my-cloud.foo"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
     result = client.post("/foo/bar")
     assert result == {"success": True}
     assert post.called
@@ -242,8 +220,10 @@ def test_client_posts_retries_if_token_needs_refreshing(monkeypatch):
         )
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.api": "http://my-cloud.foo"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
     with pytest.raises(requests.HTTPError) as exc:
         result = client.post("/foo/bar")
     assert exc.value is error
@@ -259,12 +239,18 @@ def test_client_posts_graphql_to_graphql_server(monkeypatch):
         )
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {
+            "cloud.api": "http://my-cloud.foo",
+            "cloud.auth_token": "secret_token",
+            "cloud.graphql": None,
+        }
+    ):
+        client = Client()
     result = client.graphql("{projects{name}}")
     assert result == {"success": True}
     assert post.called
-    assert post.call_args[0][0] == "http://my-cloud.foo/graphql"
+    assert post.call_args[0][0] == "http://my-cloud.foo"
 
 
 def test_client_graphql_retries_if_token_needs_refreshing(monkeypatch):
@@ -277,13 +263,19 @@ def test_client_graphql_retries_if_token_needs_refreshing(monkeypatch):
         )
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {
+            "cloud.api": "http://my-cloud.foo",
+            "cloud.auth_token": "secret_token",
+            "cloud.graphql": None,
+        }
+    ):
+        client = Client()
     with pytest.raises(requests.HTTPError) as exc:
         result = client.graphql("{}")
     assert exc.value is error
     assert post.call_count == 3  # first call -> refresh token -> last call
-    assert post.call_args[0][0] == "http://my-cloud.foo/graphql"
+    assert post.call_args[0][0] == "http://my-cloud.foo"
     assert client.token == "new-token"
 
 
@@ -295,18 +287,47 @@ def test_graphql_errors_get_raised(monkeypatch):
         )
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
     with pytest.raises(ValueError) as exc:
         res = client.graphql("query: {}")
     assert "GraphQL issue!" in str(exc.value)
 
 
+def test_client_deploy(monkeypatch):
+    response = """
+{
+    "createFlow": {
+        "flow": {
+            "id": "long-id"
+        }
+    }
+}
+    """
+    post = MagicMock(
+        return_value=MagicMock(
+            json=MagicMock(return_value=dict(data=json.loads(response)))
+        )
+    )
+    monkeypatch.setattr("requests.post", post)
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
+    flow = prefect.Flow(name="test")
+    result = client.deploy(flow, project_id="my-default-0000")
+    assert isinstance(result, GraphQLResult)
+    assert result.id == "long-id"
+
+
 def test_get_flow_run_info(monkeypatch):
     response = """
 {
     "flow_run_by_pk": {
         "version": 0,
+        "parameters": {},
         "current_state": {
             "serialized_state": {
                 "type": "Pending",
@@ -325,14 +346,17 @@ def test_get_flow_run_info(monkeypatch):
         )
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
     result = client.get_flow_run_info(flow_run_id="74-salt")
     assert isinstance(result, GraphQLResult)
     assert isinstance(result.state, Pending)
     assert result.state.result == 42
     assert result.state.message is None
     assert result.version == 0
+    assert result.parameters == dict()
 
 
 def test_get_flow_run_info_raises_informative_error(monkeypatch):
@@ -347,8 +371,10 @@ def test_get_flow_run_info_raises_informative_error(monkeypatch):
         )
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
     with pytest.raises(ValueError) as exc:
         result = client.get_flow_run_info(flow_run_id="74-salt")
     assert "not found" in str(exc.value)
@@ -370,8 +396,10 @@ def test_set_flow_run_state(monkeypatch):
         )
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
     result = client.set_flow_run_state(
         flow_run_id="74-salt", version=0, state=Pending()
     )
@@ -405,8 +433,10 @@ def test_get_task_run_info(monkeypatch):
         )
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
     result = client.get_task_run_info(
         flow_run_id="74-salt", task_id="72-salt", map_index=None
     )
@@ -434,8 +464,10 @@ def test_set_task_run_state(monkeypatch):
         )
     )
     monkeypatch.setattr("requests.post", post)
-    with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-        client = Client(token="secret_token")
+    with set_temporary_config(
+        {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+    ):
+        client = Client()
     result = client.set_task_run_state(
         task_run_id="76-salt", version=0, state=Pending()
     )
@@ -448,8 +480,10 @@ def test_set_flow_run_state_calls_result_handler(self, monkeypatch):
         monkeypatch.setattr("requests.post", MagicMock())
         monkeypatch.setattr(prefect.client.client, "json", MagicMock())
 
-        with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-            client = Client(token="secret_token")
+        with set_temporary_config(
+            {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+        ):
+            client = Client()
 
         serializer = MagicMock(return_value="empty")
         result = client.set_flow_run_state(
@@ -465,8 +499,10 @@ def test_set_task_run_state_calls_result_handler(self, monkeypatch):
         monkeypatch.setattr("requests.post", MagicMock())
         monkeypatch.setattr(prefect.client.client, "json", MagicMock())
 
-        with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-            client = Client(token="secret_token")
+        with set_temporary_config(
+            {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+        ):
+            client = Client()
 
         serializer = MagicMock(return_value="empty")
         result = client.set_task_run_state(
@@ -505,8 +541,10 @@ def test_get_flow_run_info_doesnt_call_result_handler_if_result_is_none(
             )
         )
         monkeypatch.setattr("requests.post", post)
-        with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-            client = Client(token="secret_token")
+        with set_temporary_config(
+            {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+        ):
+            client = Client()
 
         result = client.get_flow_run_info(
             flow_run_id="74-salt", result_handler=AddOneHandler()
@@ -543,8 +581,10 @@ def test_get_task_run_info_doesnt_call_result_handler_if_result_is_none(
             )
         )
         monkeypatch.setattr("requests.post", post)
-        with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-            client = Client(token="secret_token")
+        with set_temporary_config(
+            {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+        ):
+            client = Client()
 
         result = client.get_task_run_info(
             flow_run_id="74-salt",
@@ -579,8 +619,10 @@ def test_get_flow_run_info_calls_result_handler(self, monkeypatch):
             )
         )
         monkeypatch.setattr("requests.post", post)
-        with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-            client = Client(token="secret_token")
+        with set_temporary_config(
+            {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+        ):
+            client = Client()
 
         result = client.get_flow_run_info(
             flow_run_id="74-salt", result_handler=AddOneHandler()
@@ -615,8 +657,10 @@ def test_get_task_run_info_calls_result_handler(self, monkeypatch):
             )
         )
         monkeypatch.setattr("requests.post", post)
-        with set_temporary_config({"cloud.graphql": "http://my-cloud.foo/graphql"}):
-            client = Client(token="secret_token")
+        with set_temporary_config(
+            {"cloud.api": "http://my-cloud.foo", "cloud.auth_token": "secret_token"}
+        ):
+            client = Client()
 
         result = client.get_task_run_info(
             flow_run_id="74-salt",
diff --git a/tests/engine/test_flow_runner.py b/tests/engine/test_flow_runner.py
--- a/tests/engine/test_flow_runner.py
+++ b/tests/engine/test_flow_runner.py
@@ -310,6 +310,25 @@ def test_required_parameters_must_be_provided():
     assert "required but not provided" in str(flow_state.result[y]).lower()
 
 
+def test_parameters_are_placed_into_context():
+    flow = prefect.Flow()
+    y = prefect.Parameter("y", default=99)
+    flow.add_task(y)
+    flow_state = FlowRunner(flow=flow).run(return_tasks=[y], parameters=dict(y=42))
+    assert isinstance(flow_state, Success)
+    assert flow_state.result[y].result == 42
+
+
+def test_parameters_are_placed_into_context_and_override_current_context():
+    flow = prefect.Flow()
+    y = prefect.Parameter("y", default=99)
+    flow.add_task(y)
+    with prefect.context(_parameters=dict(y=88, z=55)):
+        flow_state = FlowRunner(flow=flow).run(return_tasks=[y], parameters=dict(y=42))
+    assert isinstance(flow_state, Success)
+    assert flow_state.result[y].result == 42
+
+
 def test_flow_run_state_determined_by_reference_tasks():
     flow = prefect.Flow()
     t1 = ErrorTask()