Three fewer things to remember
**The portal link puts itself back up.** It already retried a connection that raised, but a session that ended *cleanly* — a portal restarting, a proxy closing an idle socket — returned normally and went straight back round the loop with no wait at all, so an engine could spin against a portal that was merely saying goodbye politely. Every ending now reconnects on a delay, and the delay turns on whether the attempt got as far as attaching: one that stood up and dropped is a network event and retries at once, one that never stood up waits longer each time. Jittered, so a portal coming back is not met by every installation it serves in the same instant. Ping timeouts are named rather than defaulted, since they are what bounds how long a suspended laptop's dead socket looks alive, and the keepalive task is awaited so the reason a link went reaches the log instead of the garbage collector. **`fluksio enroll <code>`** is the whole command now; hub.fluksio.com is the default and `--portal` names another. The one command run before anything works should not need two flags. **`fluksio run` syncs first.** The reason a run exists is usually the edit before it, so remembering to sync was remembering to do something the computer could do — including the worker refresh, which is what makes an edit to your own package take effect at all. `--no-sync` opts out for a tight loop. That last one needed discovery fixed: it only ever looked at top-level `*.py`, so a repository whose code is in a package — the ordinary shape — found nothing from its own root. It now descends into the packages it holds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ue1tkFWB1bcGy3aWhCKpU
This commit is contained in:
@@ -94,8 +94,21 @@ def discover(targets: list[str]) -> list[Flow]:
|
||||
for info in pkgutil.walk_packages(package.__path__, f"{dotted}."):
|
||||
importlib.import_module(info.name)
|
||||
continue
|
||||
# A plain directory — a repository root, usually. Its own modules,
|
||||
# and the packages inside it: `myresearch/` beside a `README` is the
|
||||
# ordinary shape, and naming it explicitly should not be the price of
|
||||
# keeping your code in a package.
|
||||
for module in sorted(path.glob("*.py")):
|
||||
_import(*_module_of(module))
|
||||
for child in sorted(path.iterdir()):
|
||||
if child.name.startswith(".") or not (child / "__init__.py").exists():
|
||||
continue
|
||||
root, dotted = _package_of(child)
|
||||
_import(root, dotted)
|
||||
for info in pkgutil.walk_packages(
|
||||
sys.modules[dotted].__path__, f"{dotted}."
|
||||
):
|
||||
importlib.import_module(info.name)
|
||||
return list(FLOWS.values())
|
||||
|
||||
|
||||
@@ -125,8 +138,9 @@ def cmd_sync(args: argparse.Namespace) -> int:
|
||||
return _fail(str(exc))
|
||||
if not flows:
|
||||
return _fail(
|
||||
f"no flows declared in {', '.join(targets)} — a flow is a `flow(...)` "
|
||||
"call at module level"
|
||||
f"no flows declared in {', '.join(targets)} — a flow is a `Flow(...)` "
|
||||
"at module level. Name the package if it is somewhere else: "
|
||||
"`fluksio sync src/myresearch`."
|
||||
)
|
||||
|
||||
repo = repo_root(targets[0])
|
||||
@@ -218,9 +232,37 @@ def _params(definition: dict[str, Any], rest: list[str]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
|
||||
def _sync_first(client: Client) -> None:
|
||||
"""Upload what the working directory declares, before running it.
|
||||
|
||||
The reason a run exists is usually the edit that came before it, and
|
||||
remembering to sync is remembering to do the thing the computer could have
|
||||
done. So `run` syncs by default — including the worker refresh, which is
|
||||
what makes an edit to your own package take effect at all.
|
||||
|
||||
A directory that declares nothing is not an error: a flow drawn on the
|
||||
canvas is run the same way, and has nothing to upload.
|
||||
"""
|
||||
try:
|
||||
flows = discover(["."])
|
||||
except (ImportError, SyncError) as exc:
|
||||
# Do not fail a run for a module the run may not even need.
|
||||
_say(f"warning: nothing synced — {exc}")
|
||||
return
|
||||
if not flows:
|
||||
return
|
||||
repo = repo_root(".")
|
||||
reports = sync(flows, client, origin=origin_of(repo))
|
||||
changed = [r for r in reports if not r.unchanged]
|
||||
if changed:
|
||||
_say(f"synced {', '.join(r.flow for r in changed)}")
|
||||
|
||||
|
||||
def cmd_run(args: argparse.Namespace, rest: list[str]) -> int:
|
||||
try:
|
||||
client = Client(url=args.url, token=args.token)
|
||||
if not args.no_sync:
|
||||
_sync_first(client)
|
||||
stored = client.get_flow(args.flow)
|
||||
if stored is None:
|
||||
return _fail(f"no flow '{args.flow}' on that engine")
|
||||
@@ -301,12 +343,17 @@ def add_parsers(subparsers: Any) -> None:
|
||||
parser.set_defaults(func=cmd_sync)
|
||||
|
||||
parser = subparsers.add_parser(
|
||||
"run", help="start a run, passing the flow's inputs as --name value"
|
||||
"run", help="sync this directory, then start a run of one of its flows"
|
||||
)
|
||||
parser.add_argument("flow")
|
||||
parser.add_argument("--seed", type=int, default=None)
|
||||
parser.add_argument("--wait", action="store_true", help="block until it finishes")
|
||||
parser.add_argument("--timeout", type=float, default=0.0)
|
||||
parser.add_argument(
|
||||
"--no-sync",
|
||||
action="store_true",
|
||||
help="run what is already on the engine, without uploading first",
|
||||
)
|
||||
with_engine(parser)
|
||||
parser.set_defaults(func=cmd_run)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user