Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shareable Kedro-viz backend implementation #1498

Merged
merged 76 commits into from
Sep 27, 2023
Merged
Changes from 1 commit
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
5af4d9f
nodes working
rashidakanchwala Feb 17, 2023
283a4f5
added pipeline
rashidakanchwala Feb 17, 2023
96bb1d2
Merge branch 'kedro-viz-save-load' of https://github.com/kedro-org/ke…
rashidakanchwala Feb 17, 2023
7de3f65
fix path
rashidakanchwala Feb 17, 2023
3a023f9
fix bug and lint
rashidakanchwala Feb 17, 2023
3c2b625
fix lint
rashidakanchwala Feb 17, 2023
5da7410
Merge remote-tracking branch 'origin' into kedro-viz-save-load
rashidakanchwala Aug 18, 2023
4762c2f
update responses
rashidakanchwala Aug 18, 2023
4bc45e5
test
rashidakanchwala Aug 18, 2023
6081e85
fix rebase
rashidakanchwala Aug 21, 2023
d044bd1
fixing stuff
rashidakanchwala Aug 21, 2023
b986c85
refactored to latest
rashidakanchwala Sep 1, 2023
d0f7e65
some refactor
rashidakanchwala Sep 1, 2023
703b64c
fix lint - WIP
rashidakanchwala Sep 1, 2023
9ecb395
Merge branch 'main' into shareable-flowchart
rashidakanchwala Sep 4, 2023
6b01350
refactor work
rashidakanchwala Sep 4, 2023
8b91f7e
Merge branch 'shareable-flowchart' of https://github.com/kedro-org/ke…
rashidakanchwala Sep 4, 2023
30e5f8a
fix lint
rashidakanchwala Sep 4, 2023
fd75407
further refactor
rashidakanchwala Sep 4, 2023
480bd51
add error handling and debugging
rashidakanchwala Sep 5, 2023
e94f0ec
fix based on review
rashidakanchwala Sep 5, 2023
a6ecd7c
modify upload static files logic
ravi-kumar-pilla Sep 5, 2023
1232e92
Merge branch 'shareable-flowchart' of https://github.com/kedro-org/ke…
ravi-kumar-pilla Sep 5, 2023
737af74
refactor upload api with latest fsspec
ravi-kumar-pilla Sep 5, 2023
9e24189
fix unit tests_1
ravi-kumar-pilla Sep 5, 2023
ef4617f
revert os logic to pathlib
rashidakanchwala Sep 6, 2023
9e6d9a5
fix static folder issue
rashidakanchwala Sep 6, 2023
5b0a860
fix format and lint errors
ravi-kumar-pilla Sep 6, 2023
a6781ac
add unit tests for shareable viz s3deployer
ravi-kumar-pilla Sep 7, 2023
6d28f6b
add pytests for responses module
ravi-kumar-pilla Sep 7, 2023
16673a4
add s3fs as dependency
ravi-kumar-pilla Sep 7, 2023
605c450
add temporary no cover for apps
ravi-kumar-pilla Sep 7, 2023
ad8d089
update lower reqs
rashidakanchwala Sep 8, 2023
ae2992a
update fsspec
rashidakanchwala Sep 8, 2023
52a2a8a
check kedro latest version as 18.0 in e2e tests
ravi-kumar-pilla Sep 8, 2023
a0e7052
update fsspec and s3fs requirements to support earliest kedro version
ravi-kumar-pilla Sep 8, 2023
0046755
add timestamp file for deploy
ravi-kumar-pilla Sep 11, 2023
ef093d2
merged main
ravi-kumar-pilla Sep 11, 2023
77e9ad4
add pytest for timestamp route
ravi-kumar-pilla Sep 11, 2023
bf9f106
fix lint and format errors
ravi-kumar-pilla Sep 11, 2023
c300d74
Merge branch 'main' into shareable-flowchart
tynandebold Sep 13, 2023
2dab4c0
fix server changes and test e2e scenarios
ravi-kumar-pilla Sep 13, 2023
031c59a
try to catch versionConflicterror
rashidakanchwala Sep 14, 2023
a6e7ef0
add route /api/project-metadata to provide package version info
ravi-kumar-pilla Sep 14, 2023
7854c6a
remove frontend build for backend unit tests
ravi-kumar-pilla Sep 14, 2023
d162a8f
remove s3fs requirement to test
ravi-kumar-pilla Sep 14, 2023
1415f00
add s3fs without specific version
ravi-kumar-pilla Sep 14, 2023
3533925
adjust requirements and add pytests for project metadata
ravi-kumar-pilla Sep 15, 2023
455eeec
test open s3fs requirement
ravi-kumar-pilla Sep 15, 2023
948d420
test open s3fs requirement
ravi-kumar-pilla Sep 15, 2023
aa2d5e4
test open s3fs requirement
ravi-kumar-pilla Sep 15, 2023
b401f2f
add version info and modify route name from /api/timestamp to /api/de…
ravi-kumar-pilla Sep 15, 2023
4174d43
add pytests for updated api
ravi-kumar-pilla Sep 15, 2023
008624d
Merge branch 'main' into shareable-flowchart
rashidakanchwala Sep 18, 2023
de88895
undo all new requirements
rashidakanchwala Sep 18, 2023
74132c6
undo all fsspec changes
rashidakanchwala Sep 18, 2023
44681d4
added s3fs as dependency
rashidakanchwala Sep 18, 2023
16a01e5
Merge branch 'shareable-flowchart' of https://github.com/kedro-org/ke…
rashidakanchwala Sep 18, 2023
08551e3
fix unit tests
rashidakanchwala Sep 18, 2023
b8720b4
clean up tests
rashidakanchwala Sep 18, 2023
13f4425
lint
rashidakanchwala Sep 18, 2023
d69f7c5
fix lint
rashidakanchwala Sep 18, 2023
f26a53e
fix test and lint and compatibility response
rashidakanchwala Sep 18, 2023
a6ea86e
add packaging
rashidakanchwala Sep 18, 2023
269ed26
packaging reqs
rashidakanchwala Sep 18, 2023
72a7e18
fix api endpoint
rashidakanchwala Sep 20, 2023
4cea3e9
fixes based on reviews
rashidakanchwala Sep 20, 2023
c3de82f
Merge branch 'main' into shareable-flowchart
rashidakanchwala Sep 22, 2023
33f568d
changes based on reviews
rashidakanchwala Sep 22, 2023
890f92e
Merge branch 'shareable-flowchart' of https://github.com/kedro-org/ke…
rashidakanchwala Sep 22, 2023
3809add
fix lint
rashidakanchwala Sep 22, 2023
e912b92
fixes based on review
rashidakanchwala Sep 26, 2023
d2de353
updated cli help definition
rashidakanchwala Sep 26, 2023
29a0fab
update filpath to directory
rashidakanchwala Sep 26, 2023
76bd6dd
update filpath to directory
rashidakanchwala Sep 26, 2023
635634b
add s3 protocol in the backend
rashidakanchwala Sep 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
added pipeline
Signed-off-by: Rashida Kanchwala <[email protected]>
rashidakanchwala committed Feb 17, 2023
commit 283a4f566431bfc79daee83920eda8da81d488df
19 changes: 18 additions & 1 deletion package/kedro_viz/api/apps.py
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@
import time
from pathlib import Path

import zipfile

from fastapi import FastAPI, HTTPException
from fastapi.requests import Request
from fastapi.responses import HTMLResponse, JSONResponse, Response
@@ -110,6 +112,21 @@ async def index():

@app.get("/api/main", response_class=JSONResponse)
async def main():
return json.loads(Path(filepath).read_text(encoding="utf8"))
with zipfile.ZipFile(f'{filepath}.zip', 'r') as zip_file:
main = zip_file.read('main')
return json.loads(main)

@app.get("/api/nodes/{node_id}", response_class=JSONResponse)
async def get_node_metadata(node_id):
with zipfile.ZipFile(f'{filepath}.zip', 'r') as zip_file:
node_metdata = zip_file.read(f'nodes/{node_id}')
return json.loads(node_metdata)

@app.get("/api/pipelines/{pipeline_id}", response_class=JSONResponse)
async def get_registered_pipeline(pipeline_id):
with zipfile.ZipFile(f'{filepath}.zip', 'r') as zip_file:
pipeline = zip_file.read(f'pipelines/{pipeline_id}')
return json.loads(pipeline)

return app

89 changes: 75 additions & 14 deletions package/kedro_viz/api/rest/responses.py
Original file line number Diff line number Diff line change
@@ -4,11 +4,21 @@
from typing import Any, Dict, List, Optional, Union

import orjson
from fastapi.responses import ORJSONResponse
from fastapi.responses import ORJSONResponse, JSONResponse
from pydantic import BaseModel

from kedro_viz.data_access import data_access_manager

from kedro_viz.models.flowchart import (
DataNode,
DataNodeMetadata,
ParametersNodeMetadata,
TaskNode,
TaskNodeMetadata,
TranscodedDataNode,
TranscodedDataNodeMetadata,
)


class APIErrorMessage(BaseModel):
message: str
@@ -241,6 +251,24 @@ class GraphAPIResponse(BaseAPIResponse):
modular_pipelines: ModularPipelinesTreeAPIResponse
selected_pipeline: str

class EnhancedORJSONResponse(ORJSONResponse):
@staticmethod
def encode_to_human_readable(content: Any) -> bytes:
"""A method to encode the given content to JSON, with the
proper formatting to write a human-readable file.

Returns:
A bytes object containing the JSON to write.

"""
return orjson.dumps(
content,
option=orjson.OPT_INDENT_2
| orjson.OPT_NON_STR_KEYS
| orjson.OPT_SERIALIZE_NUMPY,
)



def get_default_response() -> GraphAPIResponse:
"""Default response for `/api/main`."""
@@ -270,20 +298,53 @@ def get_default_response() -> GraphAPIResponse:
selected_pipeline=default_selected_pipeline_id,
)

def get_node_metadata_response(node_id: str):

node = data_access_manager.nodes.get_node_by_id(node_id)
if not node:
return JSONResponse(status_code=404, content={"message": "Invalid node ID"})

class EnhancedORJSONResponse(ORJSONResponse):
@staticmethod
def encode_to_human_readable(content: Any) -> bytes:
"""A method to encode the given content to JSON, with the
proper formatting to write a human-readable file.
if not node.has_metadata():
return JSONResponse(content={})

Returns:
A bytes object containing the JSON to write.
if isinstance(node, TaskNode):
return TaskNodeMetadata(node)

"""
return orjson.dumps(
content,
option=orjson.OPT_INDENT_2
| orjson.OPT_NON_STR_KEYS
| orjson.OPT_SERIALIZE_NUMPY,
if isinstance(node, DataNode):
return DataNodeMetadata(node)

if isinstance(node, TranscodedDataNode):
return TranscodedDataNodeMetadata(node)

return ParametersNodeMetadata(node)


def get_selected_pipeline_response(registered_pipeline_id: str):
if not data_access_manager.registered_pipelines.has_pipeline(
registered_pipeline_id
):
return JSONResponse(status_code=404, content={"message": "Invalid pipeline ID"})

modular_pipelines_tree = (
data_access_manager.create_modular_pipelines_tree_for_registered_pipeline(
registered_pipeline_id
)
)

return GraphAPIResponse(
nodes=data_access_manager.get_nodes_for_registered_pipeline(
registered_pipeline_id
),
edges=data_access_manager.get_edges_for_registered_pipeline(
registered_pipeline_id
),
tags=data_access_manager.tags.as_list(),
layers=data_access_manager.get_sorted_layers_for_registered_pipeline(
registered_pipeline_id
),
pipelines=data_access_manager.registered_pipelines.as_list(),
selected_pipeline=registered_pipeline_id,
modular_pipelines=modular_pipelines_tree,
)


47 changes: 4 additions & 43 deletions package/kedro_viz/api/rest/router.py
Original file line number Diff line number Diff line change
@@ -19,6 +19,8 @@
GraphAPIResponse,
NodeMetadataAPIResponse,
get_default_response,
get_node_metadata_response,
get_selected_pipeline_response
)

router = APIRouter(
@@ -38,53 +40,12 @@ async def main():
response_model_exclude_none=True,
)
async def get_single_node_metadata(node_id: str):
node = data_access_manager.nodes.get_node_by_id(node_id)
if not node:
return JSONResponse(status_code=404, content={"message": "Invalid node ID"})

if not node.has_metadata():
return JSONResponse(content={})

if isinstance(node, TaskNode):
return TaskNodeMetadata(node)

if isinstance(node, DataNode):
return DataNodeMetadata(node)

if isinstance(node, TranscodedDataNode):
return TranscodedDataNodeMetadata(node)

return ParametersNodeMetadata(node)
return get_node_metadata_response(node_id)


@router.get(
"/pipelines/{registered_pipeline_id}",
response_model=GraphAPIResponse,
)
async def get_single_pipeline_data(registered_pipeline_id: str):
if not data_access_manager.registered_pipelines.has_pipeline(
registered_pipeline_id
):
return JSONResponse(status_code=404, content={"message": "Invalid pipeline ID"})

modular_pipelines_tree = (
data_access_manager.create_modular_pipelines_tree_for_registered_pipeline(
registered_pipeline_id
)
)

return GraphAPIResponse(
nodes=data_access_manager.get_nodes_for_registered_pipeline(
registered_pipeline_id
),
edges=data_access_manager.get_edges_for_registered_pipeline(
registered_pipeline_id
),
tags=data_access_manager.tags.as_list(),
layers=data_access_manager.get_sorted_layers_for_registered_pipeline(
registered_pipeline_id
),
pipelines=data_access_manager.registered_pipelines.as_list(),
selected_pipeline=registered_pipeline_id,
modular_pipelines=modular_pipelines_tree,
)
return get_selected_pipeline_response(registered_pipeline_id)
3 changes: 3 additions & 0 deletions package/kedro_viz/data_access/repositories/graph.py
Original file line number Diff line number Diff line change
@@ -29,6 +29,9 @@ def as_list(self) -> List[GraphNode]:
def as_dict(self) -> Dict[str, GraphNode]:
return self.nodes_dict

def get_node_ids(self) -> List[str]:
return self.nodes_dict.keys()

def get_nodes_by_ids(self, node_ids: Set[str]) -> List[GraphNode]:
return [n for n in self.nodes_list if n.id in node_ids]

Original file line number Diff line number Diff line change
@@ -20,6 +20,9 @@ def add_node(self, pipeline_id: str, node_id: str):

def get_pipeline_by_id(self, pipeline_id: str) -> Optional[RegisteredPipeline]:
return self.pipelines_dict.get(pipeline_id)

def get_pipeline_ids(self) -> List[str]:
return self.pipelines_dict.keys()

def has_pipeline(self, pipeline_id: str) -> bool:
return pipeline_id in self.pipelines_dict
41 changes: 34 additions & 7 deletions package/kedro_viz/server.py
Original file line number Diff line number Diff line change
@@ -3,14 +3,18 @@
from pathlib import Path
from typing import Any, Dict, Optional

import zipfile
import io
import json

import uvicorn
from fastapi.encoders import jsonable_encoder
from kedro.io import DataCatalog
from kedro.pipeline import Pipeline
from watchgod import run_process

from kedro_viz.api import apps
from kedro_viz.api.rest.responses import EnhancedORJSONResponse, get_default_response
from kedro_viz.api.rest.responses import EnhancedORJSONResponse, get_default_response, get_node_metadata_response, get_selected_pipeline_response
from kedro_viz.constants import DEFAULT_HOST, DEFAULT_PORT
from kedro_viz.data_access import DataAccessManager, data_access_manager
from kedro_viz.database import create_db_engine
@@ -81,22 +85,45 @@ def run_server(
catalog, pipelines, session_store_location = kedro_data_loader.load_data(
path, env, extra_params
)

pipelines = (
pipelines
if pipeline_name is None
else {pipeline_name: pipelines[pipeline_name]}
)
populate_data(data_access_manager, catalog, pipelines, session_store_location)

if save_file:
default_response = get_default_response()
jsonable_default_response = jsonable_encoder(default_response)
encoded_default_response = EnhancedORJSONResponse.encode_to_human_readable(
jsonable_default_response

default_pipeline_response = get_default_response()
jsonable_default_pipeline_response = jsonable_encoder(
default_pipeline_response)
encoded_default_pipeline_response = EnhancedORJSONResponse.encode_to_human_readable(
jsonable_default_pipeline_response
)
Path(save_file).write_bytes(encoded_default_response)

encoded_node_response = {}

for node in data_access_manager.nodes.get_node_ids():
node_response = get_node_metadata_response(node)
jsonable_node_response = jsonable_encoder(node_response)
encoded_node_response[node] = EnhancedORJSONResponse.encode_to_human_readable(
jsonable_node_response
)

zip_buffer = io.BytesIO()

with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
zip_file.writestr('main', encoded_default_pipeline_response)
for node in encoded_node_response:
zip_file.writestr(f'nodes/{node}', encoded_node_response[node])

with open(f'/{project_path}/{save_file}.zip', 'wb') as zip_file:
zip_file.write(zip_buffer.getvalue())

app = apps.create_api_app_from_project(path, autoreload)
else:
app = apps.create_api_app_from_file(load_file)
app = apps.create_api_app_from_file(f'{project_path}/{load_file}')

if browser and is_localhost(host):
webbrowser.open_new(f"http://{host}:{port}/")