Esempio n. 1
0
def trigger(
        organization: str = Option("",
                                   "-o",
                                   "--organization",
                                   help=_organization_help),
        environment: str = Option("",
                                  "-e",
                                  "--environment",
                                  help=_environment_help),
        graph: Path = Option(None, exists=True, help=_graph_help),
        graph_version_id: str = Option("", help=_graph_version_id_help),
        type: TypeChoices = Option(TypeChoices.pubsub, hidden=True),
        node: Path = Argument(..., exists=True, help=_node_help),
):
    """Trigger a node on a deployed graph to run immediately"""
    ids = IdLookup(
        environment_name=environment,
        organization_name=organization,
        explicit_graph_path=graph,
        node_file_path=node,
        explicit_graph_version_id=graph_version_id,
        find_nearest_graph=True,
    )
    with abort_on_error("Error triggering node"):
        trigger_node(
            ids.node_id,
            ids.graph_version_id,
            ids.environment_id,
            execution_type=type,
        )

    sprint(f"[success]Triggered node {node}")
Esempio n. 2
0
def deploy(
        environment: str = Option("",
                                  "-e",
                                  "--environment",
                                  help=_environment_help),
        organization: str = Option("",
                                   "-o",
                                   "--organization",
                                   help=_organization_help),
        graph: Path = Option(None, help=_graph_help),
        graph_version_id: str = Option("", help=_graph_version_id_help),
):
    """Deploy a previously uploaded graph version

    You can specify either '--graph-version-id' to deploy a specific version, or
    '--graph' to deploy the latest uploaded version of a graph.
    """
    ids = IdLookup(
        environment_name=environment,
        organization_name=organization,
        explicit_graph_path=graph,
        explicit_graph_version_id=graph_version_id,
    )

    with abort_on_error("Deploy failed"):
        deploy_graph_version(ids.graph_version_id, ids.environment_id)

    sprint(f"[success]Graph deployed.")
Esempio n. 3
0
def clone(
        organization: str = Option("",
                                   "-o",
                                   "--organization",
                                   help=_organization_help),
        graph: str = Option("", help=_graph_help),
        graph_version_id: str = Option("",
                                       "-v",
                                       "--version",
                                       help=_graph_version_id_help),
        component: str = Option("", "--component", help=_component_help),
        directory: Path = Argument(None, exists=False, help=_graph_help),
):
    """Download the code for a graph"""
    if not graph and not directory and not component:
        if graph_version_id:
            abort(
                f"Missing graph directory argument."
                f"\ntry [code]patterns clone -v {graph_version_id} new_graph")
        else:
            abort(f"Missing graph argument."
                  f"\ntry [code]patterns clone graph-to-clone")
    component_match = COMPONENT_RE.fullmatch(component)
    if component and not component_match:
        abort(
            "Invalid component version. Must be in the form organization/component@v1"
        )

    component_name = component_match.group(2) if component_match else None

    ids = IdLookup(
        organization_name=organization,
        explicit_graph_name=graph or component_name or directory.name,
        explicit_graph_version_id=graph_version_id,
    )
    if not directory:
        if component:
            directory = Path(component_name)
        elif graph:
            directory = Path(graph)
        elif graph_version_id:
            with abort_on_error("Error"):
                directory = Path(ids.graph_name)
        else:
            abort("Specify --graph, --graph-version-id, or a directory")

    with abort_on_error("Error cloning graph"):
        if component:
            content = download_component_zip(component)
        else:
            content = download_graph_zip(ids.graph_version_id)
        editor = GraphDirectoryEditor(directory, overwrite=False)
        with ZipFile(io.BytesIO(content), "r") as zf:
            editor.add_node_from_zip("graph.yml", "graph.yml", zf)

    sprint(f"[success]Cloned graph into {directory}")
Esempio n. 4
0
def graph(
        name: str = Option("", "--name", "-n", help=_name_help),
        location: Path = Argument(None, metavar="GRAPH"),
):
    """Add a new node to a graph"""
    if not location:
        prompt = (
            "Enter a name for the new graph directory [prompt.default](e.g. my_graph)"
        )
        location = prompt_path(prompt, exists=False)
    with abort_on_error("Error creating graph"):
        path = resolve_graph_path(location, exists=False)
    name = name or location.stem
    GraphConfigEditor(path, read=False).set_name(name).write()

    sprint(f"\n[success]Created graph [b]{name}")
    sprint(f"\n[info]You can add nodes with [code]cd {location}[/code],"
           f" then [code]patterns create node[/code]")
Esempio n. 5
0
def execute_oauth_flow(
    url: str,
    request_handler_class: Type[BaseOAuthRequestHandler],
    on_request: Callable[[BaseOAuthRequestHandler], None] | None = None,
):
    """Execute an oauth flow, opening a web browser and handling callbacks.

    Parameters:
        url: URL to open in the browser
        request_handler_class: Class which will handle the web request made by the
                               browser (for an oauth callback)
        on_request: Optional callback which is invoked before the actual handler is run
    """

    # noinspection PyTypeChecker
    server = OAuthHttpServer(
        ("localhost", LOCAL_OAUTH_PORT), request_handler_class, on_request
    )
    webbrowser.open(url, new=1, autoraise=True)

    server_thread = ServerThread(server)
    server_thread.start()

    try:
        server.wait_for_request_started()
    except BrokenBarrierError:
        server.shutdown()
        abort("Timed out waiting for the web browser to hit callback url")

    try:
        server.wait_for_request_finished()
    except BrokenBarrierError:
        server.shutdown()
        abort("Timed out waiting for the request to finish")

    server.shutdown()
    server_thread.join(timeout=5)

    if server.error_result:
        abort(server.error_result)
    elif server.success_result:
        sprint(f"[info]{server.success_result}")
    else:
        abort("OAuth server finished without an error or success result")
Esempio n. 6
0
def delete(
    graph_id: str = Option(""),
    force: bool = Option(False, "-f", "--force", help=_force_help),
    graph: Path = Argument(None, exists=True, help=_graph_help),
):
    """Delete a graph from the Patterns studio.

    This will not delete any files locally.
    """
    ids = IdLookup(
        explicit_graph_path=graph,
        explicit_graph_id=graph_id,
    )

    with abort_on_error("Deleting graph failed"):
        if not force:
            Confirm.ask(f"Delete graph {ids.graph_name}?")
        delete_graph(ids.graph_id)

    sprint(f"[success]Graph deleted from Patterns studio.")
Esempio n. 7
0
def webhook(
        explicit_graph: Path = Option(None,
                                      "--graph",
                                      "-g",
                                      exists=True,
                                      help=_graph_help),
        name: str = Argument(..., help=_webhook_name_help),
):
    """Add a new webhook node to a graph"""
    ids = IdLookup(explicit_graph_path=explicit_graph)

    with abort_on_error("Adding webhook failed"):
        editor = GraphConfigEditor(ids.graph_file_path)
        editor.add_webhook(name, id=random_node_id())
        editor.write()

    sprint(f"\n[success]Created webhook [b]{name}")
    sprint(
        f"\n[info]Once you've deployed the graph, use "
        f"[code]patterns list webhooks[/code] to get the url of the webhook")
Esempio n. 8
0
def _print_objects(objects: list, print_json: bool, headers: Iterable[str] = ()):
    if not objects:
        if not print_json:
            sprint("[info]No data found")
        return

    if print_json:
        for o in objects:
            print(json.dumps(o))
    else:
        table = Table()
        for k in headers:
            table.add_column(k)
        for k in objects[0].keys():
            if k not in headers:
                table.add_column(k)
        columns = [str(c.header) for c in table.columns]
        for o in objects:
            table.add_row(*(str(o.get(c, "")) for c in columns))
        sprint(table)
Esempio n. 9
0
def pull(
        organization: str = Option("",
                                   "-o",
                                   "--organization",
                                   help=_organization_help),
        graph_version_id: str = Option("", help=_graph_version_id_help),
        force: bool = Option(False, "-f", "--force", help=_force_help),
        graph: Path = Argument(None, exists=True, help=_pull_graph_help),
):
    """Update the code for the current graph"""
    ids = IdLookup(
        organization_name=organization,
        explicit_graph_version_id=graph_version_id,
        explicit_graph_path=graph,
    )
    with abort_on_error("Error downloading graph"):
        b = io.BytesIO(download_graph_zip(ids.graph_version_id))
        editor = GraphDirectoryEditor(ids.graph_file_path, overwrite=force)

    with abort_on_error("Error downloading graph"):
        try:
            with ZipFile(b, "r") as zf:
                editor.add_node_from_zip("graph.yml", "graph.yml", zf)
        except FileOverwriteError as e:
            sprint(f"[error]{e}")
            sprint(
                "[info]Run this command with --force to overwrite local files")
            raise typer.Exit(1)
    sprint(f"[success]Pulled graph content")
Esempio n. 10
0
def config(
    organization: str = Option("", "-o", "--organization", help=_config_help),
    environment: str = Option("",
                              "-e",
                              "--environment",
                              help=_environment_help),
):
    """Change the default values used by other commands"""
    ids = IdLookup(
        organization_name=organization,
        environment_name=environment,
        ignore_cfg_environment=True,
    )
    if organization:
        ids.cfg.organization_id = ids.organization_id
    if environment:
        ids.cfg.environment_id = ids.environment_id
    write_devkit_config(ids.cfg)

    sprint(f"[info]Your patterns config is located at "
           f"[code]{get_devkit_config_path().as_posix()}")

    t = Table(show_header=False)
    try:
        name = get_organization_by_id(ids.organization_id)["name"]
        t.add_row("organization", name)
    except Exception:
        t.add_row("organization_id", ids.organization_id)
    try:
        name = get_environment_by_id(ids.environment_id)["name"]
        t.add_row("environment", name)
    except Exception:
        t.add_row("environment_id", ids.environment_id)
    if ids.cfg.auth_server:
        t.add_row("auth_server.domain", ids.cfg.auth_server.domain)
        t.add_row("auth_server.audience", ids.cfg.auth_server.audience)
        t.add_row("auth_server.devkit_client_id",
                  ids.cfg.auth_server.devkit_client_id)
    sprint(t)
Esempio n. 11
0
def login():
    """Log in to your Patterns account"""

    with abort_on_error("Login failed"):
        login_service.login()

    ids = IdLookup(ignore_local_cfg=True)
    with abort_on_error("Fetching account failed"):
        update_devkit_config(organization_id=ids.organization_id,
                             environment_id=ids.environment_id)

    with abort_on_error("Fetching user profile failed"):
        profile = me()

    sprint(
        f"\n[success]Logged in to Patterns organization [b]{ids.organization_name}[/b] "
        f"as [b]{profile['username']}[/b] ([b]{profile['email']}[/b])")
    sprint(f"\n[info]Your login information is stored at "
           f"{get_devkit_config_path().as_posix()}")
    sprint(f"\n[info]If you want to create a new graph, run "
           f"[code]patterns create graph[/code] get started")
Esempio n. 12
0
def upload(
    deploy: bool = Option(True, "--deploy/--no-deploy", help=_deploy_help),
    organization: str = Option("", "-o", "--organization", help=_organization_help),
    environment: str = Option("", "-e", "--environment", help=_environment_help),
    graph: Path = Argument(None, exists=True, help=_graph_help),
    publish_component: bool = Option(False, help=_component_help),
):
    """Upload a new version of a graph to Patterns"""
    ids = IdLookup(
        environment_name=environment,
        organization_name=organization,
        explicit_graph_path=graph,
    )

    with abort_on_error("Upload failed"):
        resp = upload_graph_version(
            ids.graph_file_path,
            ids.organization_id,
            add_missing_node_ids=not publish_component,
        )

    graph_version_id = resp["uid"]
    ui_url = resp["ui_url"]
    sprint(f"\n[success]Uploaded new graph version with id [b]{graph_version_id}")
    errors = resp.get("errors", [])
    if publish_component:
        errors = [
            e
            for e in errors
            if not e["message"].startswith("Top level input is not connected")
            and not (
                e["message"].startswith("Parameter")
                and e["message"].endswith("has no default or value")
            )
        ]
    if errors:
        sprint(f"[error]Graph contains the following errors:")
        for error in errors:
            sprint(f"\t[error]{error}")

    if publish_component:
        with abort_on_error("Error creating component"):
            resp = create_graph_component(graph_version_id)
            resp_org = resp["organization"]["slug"]
            resp_version = resp["version_name"]
            resp_component = resp["component"]["slug"]
            resp_id = resp["uid"]
            sprint(
                f"[success]Published graph component "
                f"[b]{resp_org}/{resp_component}[/b] "
                f"with version [b]{resp_version}[/b] "
                f"at id [b]{resp_id}"
            )
    elif deploy:
        with abort_on_error("Deploy failed"):
            deploy_graph_version(graph_version_id, ids.environment_id)
        sprint(f"[success]Graph deployed")

    sprint(f"\n[info]Visit [code]{ui_url}[/code] to view your graph")
Esempio n. 13
0
def node(
        title: str = Option("", "--title", "-n", help=_name_help),
        component: str = Option("", "-c", "--component", help=_component_help),
        location: Path = Argument(None),
):
    """Add a new node to a graph

    patterns create node --name='My Node' mynode.py
    """
    if component and location:
        abort("Specify either a component or a node location, not both")

    if component:
        ids = IdLookup(find_nearest_graph=True)
        GraphConfigEditor(ids.graph_file_path).add_component_uses(
            component_key=component).write()
        sprint(f"[success]Added component {component} to graph")
        return

    if not location:
        sprint("[info]Nodes can be python files like [code]ingest.py")
        sprint("[info]Nodes can be sql files like [code]aggregate.sql")
        sprint(
            "[info]You also can add a subgraph like [code]processor/graph.yml")
        message = "Enter a name for the new node file"
        location = prompt_path(message, exists=False)

    if location.exists():
        abort(f"Cannot create node: {location} already exists")

    ids = IdLookup(node_file_path=location, find_nearest_graph=True)
    # Update the graph yaml
    node_file = "/".join(location.absolute().relative_to(
        ids.graph_directory).parts)
    node_title = title or (location.parent.name
                           if location.name == "graph.yml" else location.stem)
    with abort_on_error("Adding node failed"):
        editor = GraphConfigEditor(ids.graph_file_path)
        editor.add_node(
            title=node_title,
            node_file=node_file,
            id=str(random_node_id()),
        )

    # Write to disk last to avoid partial updates
    if location.suffix == ".py":
        location.write_text(_PY_FILE_TEMPLATE)
    elif location.suffix == ".sql":
        location.write_text(_SQL_FILE_TEMPLATE)
    elif location.name == "graph.yml":
        location.parent.mkdir(exist_ok=True, parents=True)
        GraphConfigEditor(location, read=False).set_name(node_title).write()
    else:
        abort("Node file must be graph.yml or end in .py or .sql")
    editor.write()

    sprint(f"\n[success]Created node [b]{location}")
    sprint(
        f"\n[info]Once you've edited the node and are ready to run the graph, "
        f"use [code]patterns upload")