diff --git a/.gitignore b/.gitignore index 8f81258..35777e3 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ test __pycache__ .python-version .DS_Store +.vscode/launch.json diff --git a/poetry.lock b/poetry.lock index 41cbea4..e5d7a3d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -55,32 +55,30 @@ tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] [[package]] name = "beta9" -version = "0.1.64" +version = "0.1.65" description = "" optional = false -python-versions = "^3.8" -files = [] -develop = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "beta9-0.1.65-py3-none-any.whl", hash = "sha256:3f772b1541cbefddfc25b16cccd6b1625aac388955891d9e1e5fadb2292a2ff8"}, + {file = "beta9-0.1.65.tar.gz", hash = "sha256:5cf79ed236e3be2ce600ac04467db8f6a58a2652383364d7f696dfed8ba0eb3a"}, +] [package.dependencies] -asgiref = "^3.8.1" +asgiref = ">=3.8.1,<4.0.0" betterproto-beta9 = {version = "2.0.0b7", extras = ["compiler"]} -click = "^8.1.7" -cloudpickle = "^3.0.0" -croniter = "^2.0.3" -fastapi = "^0.110.2" -grpcio = "^1.60.0" -grpclib = "^0.4.7" -protobuf = "^4.25.1" -rich = "^13.7.0" -typeguard = "^2.13.3" -typing-extensions = "^4.7.1" -uvicorn = "^0.29.0" -watchdog = "^4.0.0" - -[package.source] -type = "directory" -url = "../beta9/sdk" +click = ">=8.1.7,<9.0.0" +cloudpickle = ">=3.0.0,<4.0.0" +croniter = ">=2.0.3,<3.0.0" +fastapi = ">=0.110.2,<0.111.0" +grpcio = ">=1.60.0,<2.0.0" +grpclib = ">=0.4.7,<0.5.0" +protobuf = ">=4.25.1,<5.0.0" +rich = ">=13.7.0,<14.0.0" +typeguard = ">=2.13.3,<3.0.0" +typing-extensions = ">=4.7.1,<5.0.0" +uvicorn = ">=0.29.0,<0.30.0" +watchdog = ">=4.0.0,<5.0.0" [[package]] name = "betterproto-beta9" @@ -1053,28 +1051,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.4.10" +version = "0.5.6" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"}, - {file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"}, - {file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"}, - {file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"}, - {file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"}, - {file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"}, - {file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"}, - {file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"}, + {file = "ruff-0.5.6-py3-none-linux_armv6l.whl", hash = "sha256:a0ef5930799a05522985b9cec8290b185952f3fcd86c1772c3bdbd732667fdcd"}, + {file = "ruff-0.5.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b652dc14f6ef5d1552821e006f747802cc32d98d5509349e168f6bf0ee9f8f42"}, + {file = "ruff-0.5.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:80521b88d26a45e871f31e4b88938fd87db7011bb961d8afd2664982dfc3641a"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9bc8f328a9f1309ae80e4d392836e7dbc77303b38ed4a7112699e63d3b066ab"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d394940f61f7720ad371ddedf14722ee1d6250fd8d020f5ea5a86e7be217daf"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111a99cdb02f69ddb2571e2756e017a1496c2c3a2aeefe7b988ddab38b416d36"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e395daba77a79f6dc0d07311f94cc0560375ca20c06f354c7c99af3bf4560c5d"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c476acb43c3c51e3c614a2e878ee1589655fa02dab19fe2db0423a06d6a5b1b6"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2ff8003f5252fd68425fd53d27c1f08b201d7ed714bb31a55c9ac1d4c13e2eb"}, + {file = "ruff-0.5.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c94e084ba3eaa80c2172918c2ca2eb2230c3f15925f4ed8b6297260c6ef179ad"}, + {file = "ruff-0.5.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f77c1c3aa0669fb230b06fb24ffa3e879391a3ba3f15e3d633a752da5a3e670"}, + {file = "ruff-0.5.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f908148c93c02873210a52cad75a6eda856b2cbb72250370ce3afef6fb99b1ed"}, + {file = "ruff-0.5.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:563a7ae61ad284187d3071d9041c08019975693ff655438d8d4be26e492760bd"}, + {file = "ruff-0.5.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:94fe60869bfbf0521e04fd62b74cbca21cbc5beb67cbb75ab33fe8c174f54414"}, + {file = "ruff-0.5.6-py3-none-win32.whl", hash = "sha256:e6a584c1de6f8591c2570e171cc7ce482bb983d49c70ddf014393cd39e9dfaed"}, + {file = "ruff-0.5.6-py3-none-win_amd64.whl", hash = "sha256:d7fe7dccb1a89dc66785d7aa0ac283b2269712d8ed19c63af908fdccca5ccc1a"}, + {file = "ruff-0.5.6-py3-none-win_arm64.whl", hash = "sha256:57c6c0dd997b31b536bff49b9eee5ed3194d60605a4427f735eeb1f9c1b8d264"}, + {file = "ruff-0.5.6.tar.gz", hash = "sha256:07c9e3c2a8e1fe377dd460371c3462671a728c981c3205a5217291422209f642"}, ] [[package]] @@ -1318,4 +1317,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "a925a608e0b5475097e7a7be266beb5f231ac3776126082396178f2ee9144984" +content-hash = "2eb261f75becefc57b75006e3062164efb1c4a7ca6a550911185d156680396ff" diff --git a/pyproject.toml b/pyproject.toml index 80b434f..398852f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "beam-client" -version = "0.2.62" +version = "0.2.63" description = "" authors = ["beam.cloud "] packages = [ @@ -17,7 +17,7 @@ websockets = "^12.0" [tool.poetry.group.dev.dependencies] pytest = "^8.1.1" pytest-env = "^1.1.3" -ruff = "^0.4.2" +ruff = "*" [tool.poetry.scripts] beam = "beam.cli.main:cli" @@ -31,9 +31,5 @@ pythonpath = ["src"] [tool.ruff] line-length = 100 -ignore-init-module-imports = true exclude = [] src = ["src", "test", "bin"] - -[tool.ruff.per-file-ignores] -"src/beam/__init__.py" = ["F403"] diff --git a/src/beam/cli/example.py b/src/beam/cli/example.py new file mode 100644 index 0000000..347b3cf --- /dev/null +++ b/src/beam/cli/example.py @@ -0,0 +1,163 @@ +import io +import os +import zipfile +from collections import defaultdict +from dataclasses import dataclass, field +from pathlib import Path +from typing import DefaultDict, List, Optional + +import click +import requests +from beta9 import terminal +from beta9.cli.extraclick import ClickCommonGroup, ClickManagementGroup +from rich.table import Column, Table, box + +# GIT_REPO_ZIP_FILE defaults to the main branch. +repo_zip = os.getenv("GIT_REPO_ZIP_FILE") or "main.zip" +repo_uri = os.getenv("GIT_REPO_URL") or "https://github.com/beam-cloud/examples" +repo_archive_uri = f"{repo_uri}/archive/refs/heads/{repo_zip}" + + +@click.group(cls=ClickCommonGroup) +def common(**_): + pass + + +@click.group( + name="example", + cls=ClickManagementGroup, + help="Manage example apps.", +) +def management(**_): + pass + + +@common.command( + name="create-app", + help="Downloads an examlpe app.", +) +@click.argument( + "name", + type=str, + nargs=1, + required=True, +) +@click.pass_context +def create_app(ctx: click.Context, name: str): + ctx.invoke(download_example, name=name) + + +@management.command( + name="download", + help="Downloads an example app.", +) +@click.argument( + "name", + type=str, + nargs=1, + required=True, +) +def download_example(name: str): + dirs = download_repo() + if not dirs: + return terminal.error(f"No files found in the repository {repo_uri}.") + + app_dir = find_app_dirs_by_name(name, dirs) + if not app_dir: + return terminal.error(f"App example '{name}' not found in repository {repo_uri}.") + + terminal.header(f"Creating app {name}...") + for file in app_dir.files: + terminal.detail(f"Writing {file.path}") + file.path.parent.mkdir(parents=True, exist_ok=True) + file.path.write_bytes(file.content) + + terminal.success(f"App example '{name}' created! 🎉") + + +@management.command( + name="list", + help="List all available example apps.", +) +def list_examples(): + dirs = download_repo() + app_dirs = find_app_dirs(dirs) + + table = Table( + Column("Name"), + Column("Size", justify="right"), + box=box.SIMPLE, + ) + + for app in app_dirs: + table.add_row( + app.path.as_posix(), + terminal.humanize_memory(sum(len(f.content) for f in app.files)), + ) + + table.add_section() + table.add_row(f"[bold]{len(app_dirs)} items") + terminal.print(table) + + +@dataclass +class RepoFile: + path: Path + content: bytes + + +@dataclass +class RepoDir: + path: Path = Path() + files: List[RepoFile] = field(default_factory=list) + + +def download_repo(url: str = repo_archive_uri) -> List[RepoDir]: + """ + Downloads the repository into memory and returns a list of RepoDirs. + """ + response = requests.get(url) + response.raise_for_status() + + files: List[RepoFile] = [] + with zipfile.ZipFile(io.BytesIO(response.content)) as zip: + for file_info in zip.infolist(): + if file_info.is_dir(): + continue + with zip.open(file_info) as file: + path = Path(*file_info.filename.split("/")[1:]) + files.append(RepoFile(path=path, content=file.read())) + + files.sort(key=lambda f: f.path.name) + + dirs: DefaultDict[Path, RepoDir] = defaultdict(RepoDir) + for file in files: + repo_dir = dirs[file.path.parent] + repo_dir.path = file.path.parent + repo_dir.files.append(file) + + return list(dirs.values()) + + +def find_app_dirs(dirs: List[RepoDir]) -> List[RepoDir]: + """ + Finds example app dirs. + + An example app dir is a directory containing a README.md file. + """ + return sorted( + [RepoDir(path=d.path, files=d.files) for d in dirs if has_readme(d.files)], + key=lambda d: d.path.as_posix(), + ) + + +def find_app_dirs_by_name(name: str, dirs: List[RepoDir]) -> Optional[RepoDir]: + """ + Finds an example app dir by name. + """ + apps = find_app_dirs(dirs) + return next((app for app in apps if app.path.as_posix() == name), None) + + +def has_readme(files: List[RepoFile]) -> bool: + return any(f.path.name.upper().endswith("README.MD") for f in files) diff --git a/src/beam/cli/main.py b/src/beam/cli/main.py index fd906e0..31adc78 100644 --- a/src/beam/cli/main.py +++ b/src/beam/cli/main.py @@ -5,7 +5,7 @@ from beta9 import config from beta9.cli.main import load_cli -from . import configure, login, logs, quickstart +from . import configure, example, login, logs, quickstart @dataclass @@ -27,4 +27,5 @@ class SDKSettings(config.SDKSettings): cli.register(quickstart) cli.register(login) cli.register(logs) +cli.register(example) cli.load_version("beam-client")