Skip to content

Commit

Permalink
Add flwr run to command line interface (#3049)
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel J. Beutel <[email protected]>
Co-authored-by: Javier <[email protected]>
  • Loading branch information
3 people authored Mar 6, 2024
1 parent 6b09797 commit 2118cea
Show file tree
Hide file tree
Showing 6 changed files with 593 additions and 1 deletion.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ cryptography = "^42.0.4"
pycryptodome = "^3.18.0"
iterators = "^0.0.2"
typer = { version = "^0.9.0", extras=["all"] }
# Optional dependencies (VCE)
tomli = "^2.0.1"
# Optional dependencies (Simulation Engine)
ray = { version = "==2.6.3", optional = true }
pydantic = { version = "<2.0.0", optional = true }
# Optional dependencies (REST transport layer)
Expand Down
2 changes: 2 additions & 0 deletions src/py/flwr/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from .example import example
from .new import new
from .run import run

app = typer.Typer(
help=typer.style(
Expand All @@ -30,6 +31,7 @@

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

if __name__ == "__main__":
app()
151 changes: 151 additions & 0 deletions src/py/flwr/cli/flower_toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# 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.
# ==============================================================================
"""Utility to validate the `flower.toml` file."""

import importlib
import os
from typing import Any, Dict, List, Optional, Tuple

import tomli


def load_flower_toml(path: Optional[str] = None) -> Optional[Dict[str, Any]]:
"""Load flower.toml and return as dict."""
if path is None:
cur_dir = os.getcwd()
toml_path = os.path.join(cur_dir, "flower.toml")
else:
toml_path = path

if not os.path.isfile(toml_path):
return None

with open(toml_path, encoding="utf-8") as toml_file:
data = tomli.loads(toml_file.read())
return data


def validate_flower_toml_fields(
config: Dict[str, Any]
) -> Tuple[bool, List[str], List[str]]:
"""Validate flower.toml fields."""
errors = []
warnings = []

if "project" not in config:
errors.append("Missing [project] section")
else:
if "name" not in config["project"]:
errors.append('Property "name" missing in [project]')
if "version" not in config["project"]:
errors.append('Property "version" missing in [project]')
if "description" not in config["project"]:
warnings.append('Recommended property "description" missing in [project]')
if "license" not in config["project"]:
warnings.append('Recommended property "license" missing in [project]')
if "authors" not in config["project"]:
warnings.append('Recommended property "authors" missing in [project]')

if "flower" not in config:
errors.append("Missing [flower] section")
elif "components" not in config["flower"]:
errors.append("Missing [flower.components] section")
else:
if "serverapp" not in config["flower"]["components"]:
errors.append('Property "serverapp" missing in [flower.components]')
if "clientapp" not in config["flower"]["components"]:
errors.append('Property "clientapp" missing in [flower.components]')

return len(errors) == 0, errors, warnings


def validate_object_reference(ref: str) -> Tuple[bool, Optional[str]]:
"""Validate object reference.
Returns
-------
Tuple[bool, Optional[str]]
A boolean indicating whether an object reference is valid and
the reason why it might not be.
"""
module_str, _, attributes_str = ref.partition(":")
if not module_str:
return (
False,
f"Missing module in {ref}",
)
if not attributes_str:
return (
False,
f"Missing attribute in {ref}",
)

# Load module
try:
module = importlib.import_module(module_str)
except ModuleNotFoundError:
return False, f"Unable to load module {module_str}"

# Recursively load attribute
attribute = module
try:
for attribute_str in attributes_str.split("."):
attribute = getattr(attribute, attribute_str)
except AttributeError:
return (
False,
f"Unable to load attribute {attributes_str} from module {module_str}",
)

return (True, None)


def validate_flower_toml(config: Dict[str, Any]) -> Tuple[bool, List[str], List[str]]:
"""Validate flower.toml."""
is_valid, errors, warnings = validate_flower_toml_fields(config)

if not is_valid:
return False, errors, warnings

# Validate serverapp
is_valid, reason = validate_object_reference(
config["flower"]["components"]["serverapp"]
)
if not is_valid and isinstance(reason, str):
return False, [reason], []

# Validate clientapp
is_valid, reason = validate_object_reference(
config["flower"]["components"]["clientapp"]
)

if not is_valid and isinstance(reason, str):
return False, [reason], []

return True, [], []


def apply_defaults(
config: Dict[str, Any],
defaults: Dict[str, Any],
) -> Dict[str, Any]:
"""Apply defaults to config."""
for key in defaults:
if key in config:
if isinstance(config[key], dict) and isinstance(defaults[key], dict):
apply_defaults(config[key], defaults[key])
else:
config[key] = defaults[key]
return config
Loading

0 comments on commit 2118cea

Please sign in to comment.