From 8f6e128f9f1fcde897cd6aa3ce7fc2e780d90e00 Mon Sep 17 00:00:00 2001 From: Chris White Date: Sun, 29 Nov 2020 14:11:26 -0800 Subject: [PATCH 1/4] Add new get_task_run_info route for Cloud API compatibility --- src/prefect_server/graphql/runs.py | 23 +++++++- .../graphql/schema/runs.graphql | 16 +++++- tests/graphql/test_runs.py | 55 ++++++++++++++++++- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/prefect_server/graphql/runs.py b/src/prefect_server/graphql/runs.py index 819bf486..10f0d08b 100644 --- a/src/prefect_server/graphql/runs.py +++ b/src/prefect_server/graphql/runs.py @@ -5,13 +5,34 @@ import prefect from prefect import api -from prefect_server.database import postgres +from prefect_server.database import models, postgres from prefect_server.utilities import context from prefect_server.utilities.graphql import mutation, query state_schema = prefect.serialization.state.StateSchema() +@query.field("get_task_run_info") +async def resolve_get_task_run_info( + obj: Any, info: GraphQLResolveInfo, task_run_id: str +) -> dict: + """ + Retrieve details about a task run. + """ + task_run = await models.TaskRun.where(id=task_run_id).first( + {"version", "serialized_state", "state"} + ) + if not task_run: + raise ValueError("Invalid task run ID") + + return { + "version": task_run.version, + "serialized_state": task_run.serialized_state, + "state": task_run.state, + "id": task_run_id, + } + + @query.field("mapped_children") async def resolve_mapped_children( obj: Any, info: GraphQLResolveInfo, task_run_id: str diff --git a/src/prefect_server/graphql/schema/runs.graphql b/src/prefect_server/graphql/schema/runs.graphql index ba769597..d6761746 100644 --- a/src/prefect_server/graphql/schema/runs.graphql +++ b/src/prefect_server/graphql/schema/runs.graphql @@ -1,5 +1,12 @@ extend type Query { - mapped_children(task_run_id: UUID!): mapped_children_payload + mapped_children(task_run_id: UUID!): mapped_children_payload + + """ + Given a task run ID, retrieve current task run info + """ + get_task_run_info( + task_run_id: UUID! + ): task_run_info_payload } @@ -127,6 +134,13 @@ type task_run_id_payload { id: UUID } +type task_run_info_payload { + id: UUID + version: Int + serialized_state: JSON + state: String +} + type get_or_create_mapped_task_run_children_payload { ids: [UUID!] } diff --git a/tests/graphql/test_runs.py b/tests/graphql/test_runs.py index c3b6d100..f2c26bc9 100644 --- a/tests/graphql/test_runs.py +++ b/tests/graphql/test_runs.py @@ -6,7 +6,7 @@ import prefect from prefect import api, models -from prefect.engine.state import Pending, Scheduled +from prefect.engine.state import Pending, Scheduled, Success class TestCreateFlowRun: @@ -167,6 +167,59 @@ async def test_create_flow_run_without_parameters_raises_error( assert "Required parameters" in result.errors[0].message +class TestGetTaskRunInfo: + query = """ + query($task_run_id: UUID!) { + get_task_run_info(task_run_id: $task_run_id) { + id + serialized_state + state + version + } + } + """ + + async def test_get_task_run_info( + self, + run_query, + task_run_id, + ): + result = await run_query( + query=self.query, + variables=dict(task_run_id=task_run_id), + ) + + output = result.data.get_task_run_info + assert output.id == task_run_id + assert output.version == 1 + assert output.serialized_state.type == "Pending" + assert output.state == "Pending" + + await api.states.set_task_run_state(task_run_id, state=Success("hi")) + + result = await run_query( + query=self.query, + variables=dict(task_run_id=task_run_id), + ) + + assert result.data.get_task_run_info.version == 2 + assert result.data.get_task_run_info.serialized_state.type == "Success" + assert result.data.get_task_run_info.state == "Success" + assert result.data.get_task_run_info.serialized_state.message == "hi" + + async def test_get_task_run_info_handles_bad_ids( + self, + run_query, + ): + result = await run_query( + query=self.query, + variables=dict(task_run_id=str(uuid.uuid4())), + ) + + assert result.errors[0].message == "Invalid task run ID" + assert result.data.get_task_run_info is None + + class TestGetOrCreateTaskRun: mutation = """ mutation($input: get_or_create_task_run_input!) { From 87e5d96692145644d64ff82a1777b6b22e3a972e Mon Sep 17 00:00:00 2001 From: Chris White Date: Sun, 29 Nov 2020 14:32:22 -0800 Subject: [PATCH 2/4] Add new API logic for get or create task run info --- src/prefect_server/api/runs.py | 111 ++++++++++++++++++--------------- tests/api/test_runs.py | 79 +++++++++++++++++++++++ 2 files changed, 138 insertions(+), 52 deletions(-) diff --git a/src/prefect_server/api/runs.py b/src/prefect_server/api/runs.py index f240d309..f47eebca 100644 --- a/src/prefect_server/api/runs.py +++ b/src/prefect_server/api/runs.py @@ -268,67 +268,74 @@ async def get_or_create_task_run( raise ValueError("Invalid ID") -@register_api("runs.get_or_create_mapped_task_run_children") -async def get_or_create_mapped_task_run_children( - flow_run_id: str, task_id: str, max_map_index: int -) -> List[str]: +@register_api("runs.get_or_create_task_run_info") +async def get_or_create_task_run_info( + flow_run_id: str, task_id: str, map_index: int = None +) -> dict: """ - Creates and/or retrieves mapped child task runs for a given flow run and task. + Given a flow_run_id, task_id, and map_index, return details about the corresponding task run. + If the task run doesn't exist, it will be created. - Args: - - flow_run_id (str): the flow run associated with the parent task run - - task_id (str): the task ID to create and/or retrieve - - max_map_index (int,): the number of mapped children e.g., a value of 2 yields 3 mapped children + Returns: + - dict: a dict of details about the task run, including its id, version, and state. """ - # grab task info - task = await models.Task.where(id=task_id).first({"cache_key", "tenant_id"}) - # generate task runs to upsert - task_runs = [ - models.TaskRun( - tenant_id=task.tenant_id, - flow_run_id=flow_run_id, - task_id=task_id, - map_index=i, - cache_key=task.cache_key, - ) - for i in range(max_map_index + 1) - ] - # upsert the mapped children - task_runs = ( - await models.TaskRun().insert_many( - objects=task_runs, - on_conflict=dict( - constraint="task_run_unique_identifier_key", - update_columns=["cache_key"], - ), - selection_set={"returning": {"id", "map_index"}}, - ) - )["returning"] - task_runs.sort(key=lambda task_run: task_run.map_index) - # get task runs without states - stateless_runs = await models.TaskRun.where( + + if map_index is None: + map_index = -1 + + task_run = await models.TaskRun.where( { "flow_run_id": {"_eq": flow_run_id}, "task_id": {"_eq": task_id}, - # this syntax indicates "where there are no states" - "_not": {"states": {}}, + "map_index": {"_eq": map_index}, } - ).get({"id", "map_index", "version"}) - # create and insert states for stateless task runs - task_run_states = [ - models.TaskRunState( - tenant_id=task.tenant_id, - task_run_id=task_run.id, - **models.TaskRunState.fields_from_state( - Pending(message="Task run created") - ), + ).first({"id", "version", "state", "serialized_state"}) + + if task_run: + return dict( + id=task_run.id, + version=task_run.version, + state=task_run.state, + serialized_state=task_run.serialized_state, ) - for task_run in stateless_runs - ] - await models.TaskRunState().insert_many(task_run_states) - # return the task run ids - return [task_run.id for task_run in task_runs] + # if it isn't found, add it to the DB + task = await models.Task.where(id=task_id).first({"cache_key", "tenant_id"}) + if not task: + raise ValueError("Invalid task ID") + + db_task_run = models.TaskRun( + tenant_id=task.tenant_id, + flow_run_id=flow_run_id, + task_id=task_id, + map_index=map_index, + cache_key=task.cache_key, + version=0, + ) + + db_task_run_state = models.TaskRunState( + tenant_id=task.tenant_id, + state="Pending", + timestamp=pendulum.now(), + message="Task run created", + serialized_state=Pending(message="Task run created").serialize(), + ) + + db_task_run.states = [db_task_run_state] + run = await db_task_run.insert( + on_conflict=dict( + constraint="task_run_unique_identifier_key", + update_columns=["cache_key"], + ), + selection_set={"returning": {"id"}}, + ) + + return dict( + id=run.returning.id, + version=db_task_run.version, + state="Pending", + serialized_state=db_task_run.serialized_state, + ) @register_api("runs.update_flow_run_heartbeat") diff --git a/tests/api/test_runs.py b/tests/api/test_runs.py index 1a2c99a0..205c789f 100644 --- a/tests/api/test_runs.py +++ b/tests/api/test_runs.py @@ -409,6 +409,85 @@ async def test_idempotency_key_is_scoped_to_version_group_id( assert flow_run_id_1 != flow_run_id_3 +class TestGetOrCreateTaskRunInfo: + async def test_get_or_create_task_run_info_hits_db( + self, tenant_id, flow_run_id, task_id + ): + task_run = models.TaskRun( + id=str(uuid.uuid4()), + tenant_id=tenant_id, + flow_run_id=flow_run_id, + task_id=task_id, + map_index=12, + version=17, + state="Success", + serialized_state=dict(message="hi"), + ) + await task_run.insert() + + task_run_info = await api.runs.get_or_create_task_run_info( + flow_run_id=flow_run_id, task_id=task_id, map_index=task_run.map_index + ) + + assert task_run_info["id"] == task_run.id + assert task_run_info["version"] == task_run.version + assert task_run_info["state"] == task_run.state + assert task_run_info["serialized_state"] == task_run.serialized_state + + async def test_get_or_create_task_run_info_inserts_into_db( + self, flow_run_id, task_id + ): + assert not await models.TaskRun.where( + { + "flow_run_id": {"_eq": flow_run_id}, + "task_id": {"_eq": task_id}, + "map_index": {"_eq": 12}, + } + ).first({"id"}) + + task_run_info = await api.runs.get_or_create_task_run_info( + flow_run_id=flow_run_id, task_id=task_id, map_index=12 + ) + + task_run = await models.TaskRun.where( + { + "flow_run_id": {"_eq": flow_run_id}, + "task_id": {"_eq": task_id}, + "map_index": {"_eq": 12}, + } + ).first({"id"}) + + assert task_run_info["id"] == task_run.id + + task_run_state = await models.TaskRunState.where( + { + "task_run_id": {"_eq": task_run.id}, + } + ).first({"task_run_id", "state"}) + + assert task_run_info["id"] == task_run_state.task_run_id + assert task_run_info["state"] == task_run_state.state + + async def test_properly_inserts_run_and_state( + self, tenant_id, flow_run_id, task_id + ): + task_run_info = await api.runs.get_or_create_task_run_info( + flow_run_id=flow_run_id, task_id=task_id, map_index=12 + ) + + task_run = await models.TaskRun.where( + { + "flow_run_id": {"_eq": flow_run_id}, + "task_id": {"_eq": task_id}, + "map_index": {"_eq": 12}, + } + ).first({"id": True, "states": {"state", "task_run_id"}}) + assert task_run.id == task_run_info["id"] + assert len(task_run.states) == 1 + assert task_run.states[0].state == "Pending" + assert task_run.states[0].task_run_id == task_run_info["id"] + + class TestGetTaskRunInfo: async def test_task_run(self, flow_run_id, task_id): tr_id = await api.runs.get_or_create_task_run( From 19733ac3673cc3702c2fbe3fd61ad71d32aa92d1 Mon Sep 17 00:00:00 2001 From: Chris White Date: Sun, 29 Nov 2020 14:53:48 -0800 Subject: [PATCH 3/4] Remove unused route and add new get or create task run info route for Cloud API parity --- src/prefect_server/graphql/runs.py | 29 ++-- .../graphql/schema/runs.graphql | 23 ++- tests/api/test_runs.py | 150 ------------------ tests/graphql/test_runs.py | 131 +++++++-------- 4 files changed, 94 insertions(+), 239 deletions(-) diff --git a/src/prefect_server/graphql/runs.py b/src/prefect_server/graphql/runs.py index 10f0d08b..16994b80 100644 --- a/src/prefect_server/graphql/runs.py +++ b/src/prefect_server/graphql/runs.py @@ -33,6 +33,23 @@ async def resolve_get_task_run_info( } +@mutation.field("get_or_create_task_run_info") +async def resolve_get_or_create_task_run_info( + obj: Any, info: GraphQLResolveInfo, input: dict +) -> dict: + info = await api.runs.get_or_create_task_run_info( + flow_run_id=input["flow_run_id"], + task_id=input["task_id"], + map_index=input.get("map_index"), + ) + return { + "id": info["id"], + "version": info["version"], + "state": info["state"], + "serialized_state": info["serialized_state"], + } + + @query.field("mapped_children") async def resolve_mapped_children( obj: Any, info: GraphQLResolveInfo, task_run_id: str @@ -152,18 +169,6 @@ async def resolve_get_or_create_task_run( } -@mutation.field("get_or_create_mapped_task_run_children") -async def resolve_get_or_create_mapped_task_run_children( - obj: Any, info: GraphQLResolveInfo, input: dict -) -> List[dict]: - task_runs = await api.runs.get_or_create_mapped_task_run_children( - flow_run_id=input["flow_run_id"], - task_id=input["task_id"], - max_map_index=input["max_map_index"], - ) - return {"ids": task_runs} - - @mutation.field("delete_flow_run") async def resolve_delete_flow_run( obj: Any, info: GraphQLResolveInfo, input: dict diff --git a/src/prefect_server/graphql/schema/runs.graphql b/src/prefect_server/graphql/schema/runs.graphql index d6761746..fe0cae26 100644 --- a/src/prefect_server/graphql/schema/runs.graphql +++ b/src/prefect_server/graphql/schema/runs.graphql @@ -33,10 +33,12 @@ extend type Mutation { input: get_or_create_task_run_input! ): task_run_id_payload - "Gets or creates all mapped task run children for a parent task run." - get_or_create_mapped_task_run_children( - input: get_or_create_mapped_task_run_children_input! - ): get_or_create_mapped_task_run_children_payload + """ + Given a flow run, task, and map index, retrieve the corresponding task run id + """ + get_or_create_task_run_info( + input: get_or_create_task_run_info_input! + ): get_or_create_task_run_info_payload "Update a flow run's heartbeat. This indicates the flow run is alive and is called automatically by Prefect Core." update_flow_run_heartbeat( @@ -102,6 +104,12 @@ input get_or_create_task_run_input { map_index: Int } +input get_or_create_task_run_info_input { + flow_run_id: UUID! + task_id: UUID! + map_index: Int +} + input get_or_create_mapped_task_run_children_input { flow_run_id: UUID! task_id: UUID! @@ -141,8 +149,11 @@ type task_run_info_payload { state: String } -type get_or_create_mapped_task_run_children_payload { - ids: [UUID!] +type get_or_create_task_run_info_payload { + id: UUID + version: Int + state: String + serialized_state: JSON } type runs_in_queue_payload { diff --git a/tests/api/test_runs.py b/tests/api/test_runs.py index 205c789f..fec0eb60 100644 --- a/tests/api/test_runs.py +++ b/tests/api/test_runs.py @@ -663,156 +663,6 @@ async def test_task_run_doesnt_insert_state_if_tr_already_exists( assert new_task_run_state_count == task_run_state_count -class TestGetOrCreateMappedChildren: - async def test_get_or_create_mapped_children_creates_children( - self, flow_id, flow_run_id - ): - # get a task from the flow - task = await models.Task.where({"flow_id": {"_eq": flow_id}}).first({"id"}) - task_runs = await models.TaskRun.where({"task_id": {"_eq": task.id}}).get() - - mapped_children = await api.runs.get_or_create_mapped_task_run_children( - flow_run_id=flow_run_id, task_id=task.id, max_map_index=10 - ) - # confirm 11 children were returned as a result (indices 0, through 10) - assert len(mapped_children) == 11 - # confirm those 11 children are in the DB - assert len(task_runs) + 11 == len( - await models.TaskRun.where({"task_id": {"_eq": task.id}}).get() - ) - # confirm that those 11 children have api.states and the map indices are ordered - map_indices = [] - for child in mapped_children: - task_run = await models.TaskRun.where(id=child).first( - { - "map_index": True, - with_args( - "states", - {"order_by": {"version": EnumValue("desc")}, "limit": 1}, - ): {"id"}, - } - ) - map_indices.append(task_run.map_index) - assert task_run.states[0] is not None - assert map_indices == sorted(map_indices) - - async def test_get_or_create_mapped_children_retrieves_children( - self, flow_id, flow_run_id - ): - # get a task from the flow - task = await models.Task.where({"flow_id": {"_eq": flow_id}}).first( - {"id", "cache_key"} - ) - - # create some mapped children - task_run_ids = [] - for i in range(11): - task_run_ids.append( - await models.TaskRun( - flow_run_id=flow_run_id, - task_id=task.id, - map_index=i, - cache_key=task.cache_key, - ).insert() - ) - # retrieve those mapped children - mapped_children = await api.runs.get_or_create_mapped_task_run_children( - flow_run_id=flow_run_id, task_id=task.id, max_map_index=10 - ) - # confirm we retrieved 11 mapped children (0 through 10) - assert len(mapped_children) == 11 - # confirm those 11 children are the task api.runs we created earlier and that they're in order - map_indices = [] - for child in mapped_children: - task_run = await models.TaskRun.where(id=child).first({"map_index"}) - map_indices.append(task_run.map_index) - assert child in task_run_ids - assert map_indices == sorted(map_indices) - - async def test_get_or_create_mapped_children_does_not_retrieve_parent( - self, flow_id, flow_run_id - ): - # get a task from the flow - task = await models.Task.where({"flow_id": {"_eq": flow_id}}).first( - {"id", "cache_key"} - ) - # create a parent and its mapped children - for i in range(3): - await models.TaskRun( - flow_run_id=flow_run_id, - task_id=task.id, - map_index=i, - cache_key=task.cache_key, - ).insert() - - # retrieve those mapped children - mapped_children = await api.runs.get_or_create_mapped_task_run_children( - flow_run_id=flow_run_id, task_id=task.id, max_map_index=2 - ) - # confirm we retrieved 3 mapped children (0, 1, and 2) - assert len(mapped_children) == 3 - # but not the parent - for child in mapped_children: - task_run = await models.TaskRun.where(id=child).first({"map_index"}) - assert task_run.map_index > -1 - - async def test_get_or_create_mapped_children_handles_partial_children( - self, flow_id, flow_run_id - ): - # get a task from the flow - task = await models.Task.where({"flow_id": {"_eq": flow_id}}).first( - {"id", "cache_key"} - ) - - # create a few mapped children - await models.TaskRun( - flow_run_id=flow_run_id, - task_id=task.id, - map_index=3, - cache_key=task.cache_key, - ).insert() - stateful_child = await models.TaskRun( - flow_run_id=flow_run_id, - task_id=task.id, - map_index=6, - cache_key=task.cache_key, - states=[ - models.TaskRunState( - **models.TaskRunState.fields_from_state( - Pending(message="Task run created") - ), - ) - ], - ).insert() - - # retrieve mapped children - mapped_children = await api.runs.get_or_create_mapped_task_run_children( - flow_run_id=flow_run_id, task_id=task.id, max_map_index=10 - ) - assert len(mapped_children) == 11 - map_indices = [] - # confirm each of the mapped children has a state and is ordered properly - for child in mapped_children: - task_run = await models.TaskRun.where(id=child).first( - { - "map_index": True, - with_args( - "states", - {"order_by": {"version": EnumValue("desc")}, "limit": 1}, - ): {"id"}, - } - ) - map_indices.append(task_run.map_index) - assert task_run.states[0] is not None - assert map_indices == sorted(map_indices) - - # confirm the one child created with a state only has the one state - child_states = await models.TaskRunState.where( - {"task_run_id": {"_eq": stateful_child}} - ).get() - assert len(child_states) == 1 - - class TestUpdateFlowRunHeartbeat: async def test_update_heartbeat(self, flow_run_id): dt = pendulum.now() diff --git a/tests/graphql/test_runs.py b/tests/graphql/test_runs.py index f2c26bc9..24e2c251 100644 --- a/tests/graphql/test_runs.py +++ b/tests/graphql/test_runs.py @@ -167,6 +167,66 @@ async def test_create_flow_run_without_parameters_raises_error( assert "Required parameters" in result.errors[0].message +class TestGetOrCreateTaskRunInfo: + mutation = """ + mutation($input: get_or_create_task_run_info_input!) { + get_or_create_task_run_info(input: $input) { + id + version + state + serialized_state + } + } + """ + + async def test_get_existing_task_run_id( + self, run_query, task_run_id, task_id, flow_run_id + ): + result = await run_query( + query=self.mutation, + variables=dict(input=dict(flow_run_id=flow_run_id, task_id=task_id)), + ) + + task_run = await models.TaskRun.where(id=task_run_id).first( + {"id", "version", "state", "serialized_state"} + ) + + assert result.data.get_or_create_task_run_info.id == task_run.id + assert result.data.get_or_create_task_run_info.version == task_run.version + assert result.data.get_or_create_task_run_info.state == task_run.state + assert ( + result.data.get_or_create_task_run_info.serialized_state + == task_run.serialized_state + ) + + async def test_get_new_task_run_id( + self, run_query, task_run_id, task_id, flow_run_id + ): + assert not await models.TaskRun.where( + { + "flow_run_id": {"_eq": flow_run_id}, + "task_id": {"_eq": task_id}, + "map_index": {"_eq": 12}, + } + ).first({"id"}) + + result = await run_query( + query=self.mutation, + variables=dict( + input=dict(flow_run_id=flow_run_id, task_id=task_id, map_index=12) + ), + ) + + task_run = await models.TaskRun.where( + { + "flow_run_id": {"_eq": flow_run_id}, + "task_id": {"_eq": task_id}, + "map_index": {"_eq": 12}, + } + ).first({"id"}) + assert task_run.id == result.data.get_or_create_task_run_info.id + + class TestGetTaskRunInfo: query = """ query($task_run_id: UUID!) { @@ -263,77 +323,6 @@ async def test_get_or_create_task_run_new_task_run( ) -class TestGetOrCreateMappedTaskRunChildren: - mutation = """ - mutation($input: get_or_create_mapped_task_run_children_input!) { - get_or_create_mapped_task_run_children(input: $input) { - ids - } - } - """ - - async def test_get_or_create_mapped_task_run_children( - self, run_query, flow_run_id, flow_id - ): - # grab the task ID - task = await models.Task.where({"flow_id": {"_eq": flow_id}}).first({"id"}) - result = await run_query( - query=self.mutation, - variables=dict( - input=dict(flow_run_id=flow_run_id, task_id=task.id, max_map_index=5) - ), - ) - # should have 6 children, indices 0-5 - assert len(result.data.get_or_create_mapped_task_run_children.ids) == 6 - - async def test_get_or_create_mapped_task_run_children_with_partial_children( - self, run_query, flow_run_id, flow_id - ): - task = await models.Task.where({"flow_id": {"_eq": flow_id}}).first({"id"}) - # create a couple of children - preexisting_run_1 = await models.TaskRun( - flow_run_id=flow_run_id, - task_id=task.id, - map_index=3, - cache_key=task.cache_key, - ).insert() - preexisting_run_2 = await models.TaskRun( - flow_run_id=flow_run_id, - task_id=task.id, - map_index=6, - cache_key=task.cache_key, - states=[ - models.TaskRunState( - **models.TaskRunState.fields_from_state( - Pending(message="Task run created") - ), - ) - ], - ).insert() - # call the route - result = await run_query( - query=self.mutation, - variables=dict( - input=dict(flow_run_id=flow_run_id, task_id=task.id, max_map_index=10) - ), - ) - mapped_children = result.data.get_or_create_mapped_task_run_children.ids - # should have 11 children, indices 0-10 - assert len(mapped_children) == 11 - - # confirm the preexisting task runs were included in the results - assert preexisting_run_1 in mapped_children - assert preexisting_run_2 in mapped_children - - # confirm the results are ordered - map_indices = [] - for child in mapped_children: - map_indices.append( - (await models.TaskRun.where(id=child).first({"map_index"})).map_index - ) - assert map_indices == sorted(map_indices) - - class TestUpdateFlowRunHeartbeat: mutation = """ mutation($input: update_flow_run_heartbeat_input!) { From 8b135617a1168a8cd5acca372f573a7c2b4e74f6 Mon Sep 17 00:00:00 2001 From: Chris White Date: Sun, 29 Nov 2020 14:55:24 -0800 Subject: [PATCH 4/4] Add changelog entry --- changes/pr143.yaml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changes/pr143.yaml diff --git a/changes/pr143.yaml b/changes/pr143.yaml new file mode 100644 index 00000000..8b2b3eae --- /dev/null +++ b/changes/pr143.yaml @@ -0,0 +1,2 @@ +enhancement: + - "Add two new GraphQL routes for Core functionality - [#143](https://github.com/PrefectHQ/server/pull/143)"