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

Add CLI to Flower #2942

Merged
merged 46 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
b004c7b
Update mt-pytorch example (#2933)
jafermarq Feb 13, 2024
8eb8b56
Merge branch 'main' into flower_cli
tanertopal Feb 13, 2024
fc10257
Add CLI
tanertopal Feb 13, 2024
ede60a7
Reset dependencies
tanertopal Feb 13, 2024
785f562
Reset dependencies
tanertopal Feb 13, 2024
036877c
Add pytest-watcher instead of unmainted pytest-watch
tanertopal Feb 13, 2024
57d05d2
Format
tanertopal Feb 13, 2024
52fd6e9
Merge branch 'main' into flower_cli
tanertopal Feb 14, 2024
374fd90
Merge branch 'update_pytest_watch' into flower_cli
tanertopal Feb 14, 2024
3783e15
Improve CLI
tanertopal Feb 14, 2024
863cc83
Merge branch 'main' into flower_cli
tanertopal Feb 14, 2024
0c45224
Fix
tanertopal Feb 14, 2024
1b92064
Improve CLI
tanertopal Feb 14, 2024
eae980a
Fix error
tanertopal Feb 14, 2024
d99759a
Fix issue with potential directory name conflict
tanertopal Feb 15, 2024
24908ac
Merge branch 'main' into flower_cli
tanertopal Feb 15, 2024
d716a03
Merge branch 'main' into flower_cli
tanertopal Feb 15, 2024
17897d1
Update pyproject.toml
tanertopal Feb 15, 2024
84f50b0
Merge branch 'main' into flower_cli
tanertopal Feb 16, 2024
0783605
Merge branch 'main' into flower_cli
danieljanes Feb 18, 2024
a654ff6
Update src/py/flwr/cli/__init__.py
tanertopal Feb 18, 2024
d07d53c
Update src/py/flwr/cli/app.py
tanertopal Feb 18, 2024
21c0136
Update src/py/flwr/cli/app.py
tanertopal Feb 18, 2024
771746f
Update src/py/flwr/cli/new/new.py
tanertopal Feb 18, 2024
07f78af
Update src/py/flwr/cli/utils.py
tanertopal Feb 18, 2024
df5c365
Update src/py/flwr/cli/new/templates/requirements.pytorch.txt.tpl
tanertopal Feb 18, 2024
02bb301
Update src/py/flwr/cli/new/templates/requirements.pytorch.txt.tpl
tanertopal Feb 18, 2024
ab0d42f
Update src/py/flwr/cli/new/templates/requirements.tensorflow.txt.tpl
tanertopal Feb 18, 2024
ce9ef21
Update src/py/flwr/cli/new/templates/README.md.tpl
tanertopal Feb 18, 2024
4a90f32
Update src/py/flwr/cli/new/templates/README.md.tpl
tanertopal Feb 18, 2024
91ee651
Update src/py/flwr/cli/new/templates/README.md.tpl
tanertopal Feb 18, 2024
6f12510
Update src/py/flwr/cli/new/templates/README.md.tpl
tanertopal Feb 18, 2024
62dce46
Update src/py/flwr/cli/new/templates/flower.toml.tpl
tanertopal Feb 18, 2024
1d510ba
Update src/py/flwr/cli/app.py
tanertopal Feb 18, 2024
7303815
Apply suggestions from code review
tanertopal Feb 18, 2024
36c635f
Apply fixes
tanertopal Feb 18, 2024
e8bf318
Fix
tanertopal Feb 18, 2024
5d08331
Merge branch 'main' into flower_cli
danieljanes Feb 19, 2024
723ec1b
Merge branch 'main' into flower_cli
tanertopal Feb 20, 2024
62bb7a6
Update src/py/flwr/cli/new/templates/flower.toml.tpl
tanertopal Feb 20, 2024
08f92b3
Update src/py/flwr/cli/new/templates/README.md.tpl
tanertopal Feb 20, 2024
a67ebfe
Update src/py/flwr/cli/new/templates/README.md.tpl
tanertopal Feb 20, 2024
bf1c746
Update src/py/flwr/cli/new/templates/README.md.tpl
tanertopal Feb 20, 2024
955c360
Merge branch 'main' into flower_cli
tanertopal Feb 20, 2024
7baa1ee
Add additional code files
tanertopal Feb 20, 2024
bf72ee0
Apply suggestions from code review
danieljanes Feb 20, 2024
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
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ exclude = [
]

[tool.poetry.scripts]
flwr = "flwr.cli.app:app"
flower-driver-api = "flwr.server:run_driver_api"
flower-fleet-api = "flwr.server:run_fleet_api"
flower-superlink = "flwr.server:run_superlink"
Expand All @@ -67,6 +68,7 @@ protobuf = "^4.25.2"
cryptography = "^41.0.2"
pycryptodome = "^3.18.0"
iterators = "^0.0.2"
typer = { version = "^0.9.0", extras=["all"] }
danieljanes marked this conversation as resolved.
Show resolved Hide resolved
# Optional dependencies (VCE)
ray = { version = "==2.6.3", optional = true }
pydantic = { version = "<2.0.0", optional = true }
Expand Down
15 changes: 15 additions & 0 deletions src/py/flwr/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower command line interface."""
35 changes: 35 additions & 0 deletions src/py/flwr/cli/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower command line interface."""

import typer

from .example import example
from .new import new

app = typer.Typer(
help=typer.style(
"flwr is the Flower command line interface.",
fg=typer.colors.BRIGHT_YELLOW,
bold=True,
),
no_args_is_help=True,
)

app.command()(new)
app.command()(example)

if __name__ == "__main__":
app()
64 changes: 64 additions & 0 deletions src/py/flwr/cli/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower command line interface `example` command."""

import json
import os
import subprocess
import tempfile
import urllib.request

from .utils import prompt_options


def example() -> None:
"""Clone a Flower example.

All examples available in the Flower repository are available through this command.
"""
# Load list of examples directly from GitHub
url = "https://api.github.com/repos/adap/flower/git/trees/main"
with urllib.request.urlopen(url) as res:
data = json.load(res)
examples_directory_url = [
item["url"] for item in data["tree"] if item["path"] == "examples"
][0]

with urllib.request.urlopen(examples_directory_url) as res:
data = json.load(res)
example_names = [
item["path"] for item in data["tree"] if item["path"] not in [".gitignore"]
]

example_name = prompt_options(
"Please select example by typing in the number",
example_names,
)

with tempfile.TemporaryDirectory() as tmpdirname:
subprocess.check_output(
[
"git",
"clone",
"--depth=1",
"https://github.com/adap/flower.git",
tmpdirname,
]
)
examples_dir = os.path.join(tmpdirname, "examples", example_name)
subprocess.check_output(["mv", examples_dir, "."])

print()
print(f"Example ready to use in {os.path.join(os.getcwd(), example_name)}")
21 changes: 21 additions & 0 deletions src/py/flwr/cli/new/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower command line interface `new` command."""
tanertopal marked this conversation as resolved.
Show resolved Hide resolved

from .new import new as new

__all__ = [
"new",
]
130 changes: 130 additions & 0 deletions src/py/flwr/cli/new/new.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower command line interface `new` command."""

import os
from enum import Enum
from string import Template
from typing import Dict, Optional

import typer
from typing_extensions import Annotated

from ..utils import prompt_options


class MlFramework(str, Enum):
"""Available frameworks."""

PYTORCH = "PyTorch"
TENSORFLOW = "TensorFlow"


class TemplateNotFound(Exception):
"""Raised when template does not exist."""


def load_template(name: str) -> str:
"""Load template from template directory and return as text."""
tpl_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "templates"))
tpl_file_path = os.path.join(tpl_dir, name)

if not os.path.isfile(tpl_file_path):
raise TemplateNotFound(f"Template '{name}' not found")

with open(tpl_file_path, encoding="utf-8") as tpl_file:
return tpl_file.read()


def render_template(template: str, data: Dict[str, str]) -> str:
"""Render template."""
tpl_file = load_template(template)
tpl = Template(tpl_file)
result = tpl.substitute(data)
return result


def create_file(file_path: str, content: str) -> None:
"""Create file including all nessecary directories and write content into file."""
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)


def render_and_create(file_path: str, template: str, context: Dict[str, str]) -> None:
"""Render template and write to file."""
content = render_template(template, context)
create_file(file_path, content)


def new(
project_name: Annotated[
str,
typer.Argument(metavar="project_name", help="The name of the project"),
],
framework: Annotated[
Optional[MlFramework],
typer.Option(case_sensitive=False, help="The ML framework to use"),
] = None,
) -> None:
"""Create new Flower project."""
print(f"Creating Flower project {project_name}...")

if framework is not None:
framework_str = str(framework.value)
else:
framework_value = prompt_options(
"Please select ML framework by typing in the number",
[mlf.value for mlf in MlFramework],
)
selected_value = [
name
for name, value in vars(MlFramework).items()
if value == framework_value
]
framework_str = selected_value[0]

# Set project directory path
cwd = os.getcwd()
pnl = project_name.lower()
project_dir = os.path.join(cwd, pnl)

# List of files to render
files = {
"README.md": {
"template": "app/README.md.tpl",
},
"requirements.txt": {
"template": f"app/requirements.{framework_str.lower()}.txt.tpl"
},
"flower.toml": {"template": "app/flower.toml.tpl"},
f"{pnl}/__init__.py": {"template": "app/code/__init__.py.tpl"},
f"{pnl}/server.py": {
"template": f"app/code/server.{framework_str.lower()}.py.tpl"
},
f"{pnl}/client.py": {
"template": f"app/code/client.{framework_str.lower()}.py.tpl"
},
}
context = {"project_name": project_name}

for file_path, value in files.items():
render_and_create(
file_path=os.path.join(project_dir, file_path),
template=value["template"],
context=context,
)

print("Project creation successful.")
93 changes: 93 additions & 0 deletions src/py/flwr/cli/new/new_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Test for Flower command line interface `new` command."""

import os

from .new import MlFramework, create_file, load_template, new, render_template


def test_load_template() -> None:
"""Test if load_template returns a string."""
# Prepare
filename = "app/README.md.tpl"

# Execute
text = load_template(filename)

# Assert
assert isinstance(text, str)


def test_render_template() -> None:
"""Test if a string is correctly substituted."""
# Prepare
filename = "app/README.md.tpl"
data = {"project_name": "FedGPT"}

# Execute
result = render_template(filename, data)

# Assert
assert "# FedGPT" in result


def test_create_file(tmp_path: str) -> None:
"""Test if file with content is created."""
# Prepare
file_path = os.path.join(tmp_path, "test.txt")
content = "Foobar"

# Execute
create_file(file_path, content)

# Assert
with open(file_path, encoding="utf-8") as f:
text = f.read()

assert text == "Foobar"


def test_new(tmp_path: str) -> None:
"""Test if project is created for framework."""
# Prepare
project_name = "FedGPT"
framework = MlFramework.PYTORCH
expected_files_top_level = {
"requirements.txt",
"fedgpt",
"README.md",
"flower.toml",
}
expected_files_module = {
"__init__.py",
"server.py",
"client.py",
}

# Change into the temprorary directory
os.chdir(tmp_path)

# Execute
new(project_name=project_name, framework=framework)

# Assert
file_list = os.listdir(os.path.join(tmp_path, project_name.lower()))
assert set(file_list) == expected_files_top_level

file_list = os.listdir(
os.path.join(tmp_path, project_name.lower(), project_name.lower())
)
assert set(file_list) == expected_files_module
15 changes: 15 additions & 0 deletions src/py/flwr/cli/new/templates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2024 Flower Labs GmbH. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Flower CLI `new` command templates."""
Loading