Skip to content

Commit

Permalink
Add Recap CLI
Browse files Browse the repository at this point in the history
Recap now has a CLI! 💫

The CLI supports two commands: `ls` and `schema`. These behave the same way as
they do in Recap's gateway.

I've updated the Recap gateway to share the same logic as the CLI. I did this by
moving everything into a `commands` file, and calling it from both `cli` and
`gateway`.
  • Loading branch information
criccomini committed Aug 29, 2023
1 parent 7393fcc commit c36acee
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 53 deletions.
65 changes: 63 additions & 2 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,25 @@ json = [
"referencing>=0.30.0",
"httpx>=0.24.1",
]
gateway = [
app = [
"fastapi>=0.103.0",
"pydantic>=2.3.0",
"pydantic-settings>=2.0.3",
"typer>=0.9.0",
"uvicorn>=0.23.2",
"rich>=13.5.2",
]

[build-system]
requires = ["pdm-backend"]
build-backend = "pdm.backend"

[project.scripts]
recap = "recap.cli:app"

[tool.pdm.build]
# Need this to make `recap` an implicit namespace package, so it works with
# packages like recap-gateway and recap-cli.
# other recap.build packages that start with `recap.`.
includes = ["recap"]

[tool.pdm.dev-dependencies]
Expand Down Expand Up @@ -113,7 +118,7 @@ unit = "pytest tests/unit -vv"
spec = "pytest tests/spec -vv"
integration = "pytest tests/integration -vv"
test = {composite = ["unit", "spec"]}
serve ="uvicorn recap.gateway.app:app --reload"
serve ="uvicorn recap.gateway:app --reload"

[tool.pytest.ini_options]
addopts = [
Expand Down
29 changes: 29 additions & 0 deletions recap/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Annotated

import typer
from rich import print_json

from recap import commands
from recap.types import to_dict

app = typer.Typer()


@app.command()
def ls(path: Annotated[str, typer.Argument(help="Path to list children of.")] = "/"):
"""
List the children of a path.
"""

if children := commands.ls(path):
print_json(data=children)


@app.command()
def schema(path: Annotated[str, typer.Argument(help="Path to get schema of.")]):
"""
Get the schema of a path.
"""

if recap_struct := commands.schema(path):
print_json(data=to_dict(recap_struct))
39 changes: 39 additions & 0 deletions recap/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from recap.clients import create_client
from recap.settings import RecapSettings
from recap.types import StructType

settings = RecapSettings()


def ls(path: str = "/") -> list[str] | None:
"""
List the children of a path.
:param path: Path to list children of. Defaults to root.
:return: List of children.
"""

system, *args = _args(path) if path not in ("", "/") else [None]
if not system:
return list(settings.systems.keys())
if system and (url := settings.systems.get(system)):
with create_client(url.unicode_string()) as client:
return client.ls(*args)


def schema(path: str) -> StructType | None:
"""
Get the schema of a path.
:param path: Path to get schema of.
:return: Schema of path.
"""

system, *args = _args(path)
if system and (url := settings.systems.get(system)):
with create_client(url.unicode_string()) as client:
return client.get_schema(*args)


def _args(path: str | None) -> list[str]:
return path.strip("/").split("/") if path else []
35 changes: 35 additions & 0 deletions recap/gateway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from fastapi import FastAPI, HTTPException

from recap import commands
from recap.types import to_dict

app = FastAPI()


@app.get("/ls/{path:path}")
async def ls(path: str = "/") -> list[str]:
"""
List the children of a path.
"""

children = commands.ls(path)
if children is not None:
return children
raise HTTPException(status_code=404, detail="Path not found")


@app.get("/schema/{path:path}")
async def schema(path: str) -> dict:
"""
Get the schema of a path.
"""

if recap_struct := commands.schema(path):
recap_dict = to_dict(recap_struct)
if not isinstance(recap_dict, dict):
raise HTTPException(
status_code=503,
detail=f"Expected a schema dict, but got {type(recap_dict)}",
)
return recap_dict
raise HTTPException(status_code=404, detail="Path not found")
Empty file removed recap/gateway/__init__.py
Empty file.
48 changes: 0 additions & 48 deletions recap/gateway/app.py

This file was deleted.

File renamed without changes.

0 comments on commit c36acee

Please sign in to comment.