diff --git a/.ci/tests/examples/api_test.py b/.ci/tests/examples/api_test.py index 2bc9860ac..400ccb54e 100644 --- a/.ci/tests/examples/api_test.py +++ b/.ci/tests/examples/api_test.py @@ -1,5 +1,6 @@ import fire import yaml +from server_functions import ServerFunctions from fedn import APIClient @@ -32,6 +33,15 @@ def test_api_get_methods(): assert clients_count print("Clients count: ", clients_count, flush=True) + client_id = clients["result"][0]["client_id"] + client_obj = client.get_client(client_id) + assert client_obj + assert client_id == client_obj["client_id"] + print("Client: ", client_obj, flush=True) + + assert clients_count == len(clients["result"]) + assert clients_count == clients["count"] + # --- Combiners --- # combiners = client.get_combiners() @@ -46,6 +56,9 @@ def test_api_get_methods(): assert combiner print("Combiner: ", combiner, flush=True) + assert combiners_count == len(combiners["result"]) + assert combiners_count == combiners["count"] + # --- Controllers --- # status = client.get_controller_status() @@ -64,12 +77,16 @@ def test_api_get_methods(): models_from_trail = client.get_model_trail() assert models_from_trail - print("Models: ", models_from_trail, flush=True) + assert len(models_from_trail) == models_count + print("Models (model trail): ", models_from_trail, flush=True) active_model = client.get_active_model() assert active_model print("Active model: ", active_model, flush=True) + assert models_count == len(models["result"]) + assert models_count == models["count"] + # --- Packages --- # packages = client.get_packages() @@ -88,6 +105,9 @@ def test_api_get_methods(): assert checksum print("Checksum: ", checksum, flush=True) + assert packages_count == len(packages["result"]) + assert packages_count == packages["count"] + # --- Rounds --- # rounds = client.get_rounds() @@ -98,6 +118,9 @@ def test_api_get_methods(): assert rounds_count print("Rounds count: ", rounds_count, flush=True) + assert rounds_count == len(rounds["result"]) + assert rounds_count == rounds["count"] + # --- Sessions --- # sessions = client.get_sessions() @@ -108,6 +131,14 @@ def test_api_get_methods(): assert sessions_count print("Sessions count: ", sessions_count, flush=True) + session = client.get_session(id=sessions["result"][0]["session_id"]) + assert session + assert session["session_id"] == sessions["result"][0]["session_id"] + print("Session: ", session, flush=True) + + assert sessions_count == len(sessions["result"]) + assert sessions_count == sessions["count"] + # --- Statuses --- # statuses = client.get_statuses() @@ -118,6 +149,9 @@ def test_api_get_methods(): assert statuses_count print("Statuses count: ", statuses_count, flush=True) + assert statuses_count == len(statuses["result"]) + assert statuses_count == statuses["count"] + # --- Validations --- # validations = client.get_validations() @@ -128,6 +162,13 @@ def test_api_get_methods(): assert validations_count print("Validations count: ", validations_count, flush=True) + assert validations_count == len(validations["result"]) + assert validations_count == validations["count"] + + +def start_sf_session(name, rounds, helper): + client = APIClient(host="localhost", port=8092) + client.start_session(name=name, rounds=rounds, helper=helper, server_functions=ServerFunctions) if __name__ == '__main__': @@ -136,6 +177,7 @@ def test_api_get_methods(): 'set_seed': client.set_active_model, 'set_package': client.set_active_package, 'start_session': client.start_session, + 'start_sf_session': start_sf_session, 'get_client_config': _download_config, 'test_api_get_methods': test_api_get_methods, }) diff --git a/.ci/tests/examples/run.sh b/.ci/tests/examples/run.sh index 6b80e6a77..24a9b0d75 100755 --- a/.ci/tests/examples/run.sh +++ b/.ci/tests/examples/run.sh @@ -31,6 +31,9 @@ else up -d --build combiner api-server mongo minio client1 fi +# add server functions to python path to import server functions code +export PYTHONPATH="$PYTHONPATH:../server-functions" + >&2 echo "Wait for reducer to start" python ../../.ci/tests/examples/wait_for.py reducer @@ -46,8 +49,14 @@ python ../../.ci/tests/examples/wait_for.py clients >&2 echo "Upload seed" python ../../.ci/tests/examples/api_test.py set_seed --path seed.npz ->&2 echo "Start session" -python ../../.ci/tests/examples/api_test.py start_session --name "session" --rounds 3 --helper "$helper" +if [ "$example" == "server-functions" ]; then + >&2 echo "Start serverfunctions session" + python ../../.ci/tests/examples/api_test.py start_sf_session --name "session" --rounds 3 --helper "$helper" +else + >&2 echo "Start session" + python ../../.ci/tests/examples/api_test.py start_session --name "session" --rounds 3 --helper "$helper" +fi + >&2 echo "Checking rounds success" python ../../.ci/tests/examples/wait_for.py rounds diff --git a/.ci/tests/studio/studio.sh b/.ci/tests/studio/studio.sh index 575c18ae4..4856f0003 100755 --- a/.ci/tests/studio/studio.sh +++ b/.ci/tests/studio/studio.sh @@ -32,7 +32,7 @@ fi fedn studio login -u $STUDIO_USER -P $STUDIO_PASSWORD -H $STUDIO_HOST fedn project create -n citest -H $STUDIO_HOST --no-interactive sleep 5 -FEDN_PROJECT=$(fedn project list -H $STUDIO_HOST | awk 'NR>=1 {print $1; exit}') +FEDN_PROJECT=$(fedn project list -H $STUDIO_HOST --no-header | awk 'NR>=1 {print $3; exit}') fedn project set-context -id $FEDN_PROJECT -H $STUDIO_HOST pushd examples/$FEDN_EXAMPLE fedn client get-config -n test -g $FEDN_NR_CLIENTS -H $STUDIO_HOST @@ -43,4 +43,11 @@ for i in $(seq 0 $(($FEDN_NR_CLIENTS - 1))); do done popd sleep 5 -pytest .ci/tests/studio/tests.py \ No newline at end of file +# add server functions so we can import it in start_session +export PYTHONPATH="$PYTHONPATH:$(pwd)/examples/server-functions" +pytest .ci/tests/studio/tests.py -x +sleep 5 +# run with server functions +export FEDN_SERVER_FUNCTIONS="1" +export SESSION_NUMBER="2" +pytest .ci/tests/studio/tests.py -x \ No newline at end of file diff --git a/.ci/tests/studio/tests.py b/.ci/tests/studio/tests.py index 6834b049a..e2033cf3a 100644 --- a/.ci/tests/studio/tests.py +++ b/.ci/tests/studio/tests.py @@ -1,14 +1,15 @@ -import os +import os, sys import time import pytest from fedn import APIClient from fedn.cli.shared import get_token, get_project_url +from server_functions import ServerFunctions +from fedn.common.log_config import logger @pytest.fixture(scope="module") def fedn_client(): token = get_token(token=None, usr_token=False) host = get_project_url("", "", None, False) - print(f"Connecting to {host}") client = APIClient(host=host, token=token, secure=True, verify=True) return client @@ -19,11 +20,13 @@ def fedn_env(): "FEDN_ROUND_TIMEOUT": int(os.environ.get("FEDN_ROUND_TIMEOUT", 180)), "FEDN_BUFFER_SIZE": int(os.environ.get("FEDN_BUFFER_SIZE", -1)), "FEDN_NR_CLIENTS": int(os.environ.get("FEDN_NR_CLIENTS", 2)), - "FEDN_CLIENT_TIMEOUT": int(os.environ.get("FEDN_CLIENT_TIMEOUT", 60)), + "FEDN_CLIENT_TIMEOUT": int(os.environ.get("FEDN_CLIENT_TIMEOUT", 600)), "FEDN_FL_ALG": os.environ.get("FEDN_FL_ALG", "fedavg"), "FEDN_NR_EXPECTED_AGG": int(os.environ.get("FEDN_NR_EXPECTED_AGG", 2)), # Number of expected aggregated models per combiner "FEDN_SESSION_TIMEOUT": int(os.environ.get("FEDN_SESSION_TIMEOUT", 300)), # Session timeout in seconds, all rounds must be finished within this time - "FEDN_SESSION_NAME": os.environ.get("FEDN_SESSION_NAME", "test") + "FEDN_SESSION_NAME": os.environ.get("FEDN_SESSION_NAME", "test"), + "FEDN_SERVER_FUNCTIONS": os.environ.get("FEDN_SERVER_FUNCTIONS", 0), + "SESSION_NUMBER": os.environ.get("SESSION_NUMBER", 1) } @pytest.mark.order(1) @@ -49,14 +52,16 @@ def test_start_session(self, fedn_client, fedn_env): rounds=fedn_env["FEDN_NR_ROUNDS"], round_buffer_size=fedn_env["FEDN_BUFFER_SIZE"], min_clients=fedn_env["FEDN_NR_CLIENTS"], - requested_clients=fedn_env["FEDN_NR_CLIENTS"] + requested_clients=fedn_env["FEDN_NR_CLIENTS"], + server_functions=ServerFunctions if fedn_env["FEDN_SERVER_FUNCTIONS"] else None ) assert result["message"] == "Session started", f"Expected status 'Session started', got {result['message']}" @pytest.mark.order(3) def test_session_completion(self, fedn_client, fedn_env): session_obj = fedn_client.get_sessions() - assert session_obj["count"] == 1, f"Expected 1 session, got {session_obj['count']}" + session_number = int(fedn_env["SESSION_NUMBER"]) + assert session_obj["count"] == session_number, f"Expected {session_number} session/s, got {session_obj['count']}" session_result = session_obj["result"][0] start_time = time.time() @@ -77,13 +82,14 @@ def test_session_completion(self, fedn_client, fedn_env): @pytest.mark.order(4) def test_rounds_completion(self, fedn_client, fedn_env): start_time = time.time() + session_number = int(fedn_env["SESSION_NUMBER"]) while time.time() - start_time < fedn_env["FEDN_SESSION_TIMEOUT"]: rounds_obj = fedn_client.get_rounds() - if rounds_obj["count"] == fedn_env["FEDN_NR_ROUNDS"]: + if rounds_obj["count"] == session_number * fedn_env["FEDN_NR_ROUNDS"]: break time.sleep(5) else: - raise TimeoutError(f"Expected {fedn_env['FEDN_NR_ROUNDS']} rounds, but got {rounds_obj['count']} within {fedn_env['FEDN_SESSION_TIMEOUT']} seconds") + raise TimeoutError(f"Expected {session_number * fedn_env['FEDN_NR_ROUNDS']} rounds, but got {rounds_obj['count']} within {fedn_env['FEDN_SESSION_TIMEOUT']} seconds") rounds_result = rounds_obj["result"] for round in rounds_result: @@ -96,14 +102,15 @@ def test_rounds_completion(self, fedn_client, fedn_env): @pytest.mark.order(5) def test_validations(self, fedn_client, fedn_env): start_time = time.time() + session_number = int(fedn_env["SESSION_NUMBER"]) while time.time() - start_time < fedn_env["FEDN_SESSION_TIMEOUT"]: validation_obj = fedn_client.get_validations() - if validation_obj["count"] == fedn_env["FEDN_NR_ROUNDS"] * fedn_env["FEDN_NR_CLIENTS"]: + if validation_obj["count"] == session_number * fedn_env["FEDN_NR_ROUNDS"] * fedn_env["FEDN_NR_CLIENTS"]: break time.sleep(5) else: - raise TimeoutError(f"Expected {fedn_env['FEDN_NR_ROUNDS'] * fedn_env['FEDN_NR_CLIENTS']} validations, but got {validation_obj['count']} within {fedn_env['FEDN_SESSION_TIMEOUT']} seconds") + raise TimeoutError(f"Expected {session_number * fedn_env['FEDN_NR_ROUNDS'] * fedn_env['FEDN_NR_CLIENTS']} validations, but got {validation_obj['count']} within {fedn_env['FEDN_SESSION_TIMEOUT']} seconds") # We could assert or test model convergence here - print("All tests passed!", flush=True) \ No newline at end of file + logger.info("All tests passed!") \ No newline at end of file diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 867860c83..b0274da67 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -23,6 +23,7 @@ jobs: to_test: - "mnist-keras numpyhelper" - "mnist-pytorch numpyhelper" + - "server-functions numpyhelper" python_version: ["3.9", "3.10", "3.11", "3.12"] os: - ubuntu-24.04 diff --git a/.github/workflows/test-api-endpoints.yaml b/.github/workflows/test-api-endpoints.yaml index 99ef784ef..d04e8585b 100644 --- a/.github/workflows/test-api-endpoints.yaml +++ b/.github/workflows/test-api-endpoints.yaml @@ -27,14 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install . - - - name: Start mongo and minio - run: | - docker compose up -d mongo minio - - - name: Start FEDn controller - run: | - fedn controller start & + - name: Run tests run: | python3 -m unittest fedn.network.api.tests \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index bc1262fc7..a3b8bfe5e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ "type": "python", "request": "launch", "module": "pytest", - "justMyCode": true + "justMyCode": false }, { "args": [ @@ -17,7 +17,7 @@ "type": "python", "request": "launch", "module": "pytest", - "justMyCode": true + "justMyCode": false }, { "name": "Run current file", diff --git a/Dockerfile b/Dockerfile index 348beb747..7f031cad3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,20 +5,22 @@ FROM $BASE_IMG AS builder ARG GRPC_HEALTH_PROBE_VERSION="" ARG REQUIREMENTS="" +ARG INSTALL_TORCH=0 + WORKDIR /build # Temporarily add the Debian Testing repository to install zlib1g 1:1.3.dfsg+really1.3.1-1+b1 (fixed CVE-2023-45853) # Both zlib1g and zlib1g-dev are installed in the builder stage. RUN echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list.d/testing.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends -t testing zlib1g=1:1.3.dfsg+really1.3.1-1+b1 zlib1g-dev=1:1.3.dfsg+really1.3.1-1+b1 \ - && rm -rf /etc/apt/sources.list.d/testing.list \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* + && apt-get update \ + && apt-get install -y --no-install-recommends -t testing zlib1g=1:1.3.dfsg+really1.3.1-1+b1 zlib1g-dev=1:1.3.dfsg+really1.3.1-1+b1 \ + && rm -rf /etc/apt/sources.list.d/testing.list \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* # Install build dependencies RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends python3-dev gcc wget \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* # Add FEDn and default configs COPY . /build @@ -26,20 +28,23 @@ COPY $REQUIREMENTS /build/requirements.txt # Install dependencies RUN python -m venv /venv \ - && /venv/bin/pip install --upgrade pip \ - && /venv/bin/pip install --no-cache-dir 'setuptools>=65' \ - && /venv/bin/pip install --no-cache-dir . \ - && if [[ ! -z "$REQUIREMENTS" ]]; then \ - /venv/bin/pip install --no-cache-dir -r /build/requirements.txt; \ - fi \ - && rm -rf /build/requirements.txt + && /venv/bin/pip install --upgrade pip \ + && /venv/bin/pip install --no-cache-dir 'setuptools>=65' \ + && /venv/bin/pip install --no-cache-dir . \ + && if [[ ! -z "$REQUIREMENTS" ]]; then \ + /venv/bin/pip install --no-cache-dir -r /build/requirements.txt; \ + fi \ + && rm -rf /build/requirements.txt + +# only install torch when asked +RUN if [ "$INSTALL_TORCH" = "1" ]; then /venv/bin/pip install torch; fi # Install grpc health probe RUN if [ ! -z "$GRPC_HEALTH_PROBE_VERSION" ]; then \ - wget -qO /build/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \ - chmod +x /build/grpc_health_probe; \ - fi + wget -qO /build/grpc_health_probe https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 && \ + chmod +x /build/grpc_health_probe; \ + fi # Stage 2: Runtime FROM $BASE_IMG @@ -52,24 +57,24 @@ COPY --from=builder /build /app # Use a non-root user RUN set -ex \ - # Create a non-root user - && addgroup --system --gid 1001 appgroup \ - && adduser --system --uid 1001 --gid 1001 --no-create-home appuser \ - # Creare application specific tmp directory, set ENV TMPDIR to /app/tmp - && mkdir -p /app/tmp \ - && chown -R appuser:appgroup /venv /app \ - # Temporarily add the Debian Testing repository to install zlib1g 1:1.3.dfsg+really1.3.1-1+b1 (fixed CVE-2023-45853) - && echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list.d/testing.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends -t testing zlib1g=1:1.3.dfsg+really1.3.1-1+b1 \ - && rm -rf /etc/apt/sources.list.d/testing.list \ - # Update package index and upgrade all installed packages - && apt-get update \ - && apt-get upgrade -y \ - # Clean up - && apt-get autoremove -y \ - && apt-get clean -y \ - && rm -rf /var/lib/apt/lists/* + # Create a non-root user + && addgroup --system --gid 1001 appgroup \ + && adduser --system --uid 1001 --gid 1001 --no-create-home appuser \ + # Creare application specific tmp directory, set ENV TMPDIR to /app/tmp + && mkdir -p /app/tmp \ + && chown -R appuser:appgroup /venv /app \ + # Temporarily add the Debian Testing repository to install zlib1g 1:1.3.dfsg+really1.3.1-1+b1 (fixed CVE-2023-45853) + && echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list.d/testing.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends -t testing zlib1g=1:1.3.dfsg+really1.3.1-1+b1 \ + && rm -rf /etc/apt/sources.list.d/testing.list \ + # Update package index and upgrade all installed packages + && apt-get update \ + && apt-get upgrade -y \ + # Clean up + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* USER appuser diff --git a/async/bin/Activate.ps1 b/async/bin/Activate.ps1 new file mode 100644 index 000000000..2fb3852c3 --- /dev/null +++ b/async/bin/Activate.ps1 @@ -0,0 +1,241 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virutal environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/async/bin/activate b/async/bin/activate new file mode 100644 index 000000000..f1f08aa73 --- /dev/null +++ b/async/bin/activate @@ -0,0 +1,66 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV="/Users/sigvard/Desktop/fedn/async" +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/bin:$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1="(async) ${PS1:-}" + export PS1 +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/async/bin/activate.csh b/async/bin/activate.csh new file mode 100644 index 000000000..dd712b3ba --- /dev/null +++ b/async/bin/activate.csh @@ -0,0 +1,25 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV "/Users/sigvard/Desktop/fedn/async" + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/bin:$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = "(async) $prompt" +endif + +alias pydoc python -m pydoc + +rehash diff --git a/async/bin/activate.fish b/async/bin/activate.fish new file mode 100644 index 000000000..dd6918c01 --- /dev/null +++ b/async/bin/activate.fish @@ -0,0 +1,64 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/); you cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + functions -e fish_prompt + set -e _OLD_FISH_PROMPT_OVERRIDE + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + + set -e VIRTUAL_ENV + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV "/Users/sigvard/Desktop/fedn/async" + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/bin" $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) "(async) " (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" +end diff --git a/async/bin/f2py b/async/bin/f2py new file mode 100755 index 000000000..44e1c3ce9 --- /dev/null +++ b/async/bin/f2py @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from numpy.f2py.f2py2e import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async/bin/numpy-config b/async/bin/numpy-config new file mode 100755 index 000000000..e5a7737e3 --- /dev/null +++ b/async/bin/numpy-config @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from numpy._configtool import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async/bin/pip b/async/bin/pip new file mode 100755 index 000000000..473a0baa2 --- /dev/null +++ b/async/bin/pip @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async/bin/pip3 b/async/bin/pip3 new file mode 100755 index 000000000..473a0baa2 --- /dev/null +++ b/async/bin/pip3 @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async/bin/pip3.9 b/async/bin/pip3.9 new file mode 100755 index 000000000..473a0baa2 --- /dev/null +++ b/async/bin/pip3.9 @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async/bin/python b/async/bin/python new file mode 120000 index 000000000..b8a0adbbb --- /dev/null +++ b/async/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/async/bin/python3 b/async/bin/python3 new file mode 120000 index 000000000..f25545fee --- /dev/null +++ b/async/bin/python3 @@ -0,0 +1 @@ +/Library/Developer/CommandLineTools/usr/bin/python3 \ No newline at end of file diff --git a/async/bin/python3.9 b/async/bin/python3.9 new file mode 120000 index 000000000..b8a0adbbb --- /dev/null +++ b/async/bin/python3.9 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/async/pyvenv.cfg b/async/pyvenv.cfg new file mode 100644 index 000000000..4760c1ffc --- /dev/null +++ b/async/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /Library/Developer/CommandLineTools/usr/bin +include-system-site-packages = false +version = 3.9.6 diff --git a/async_env/bin/Activate.ps1 b/async_env/bin/Activate.ps1 new file mode 100644 index 000000000..b49d77ba4 --- /dev/null +++ b/async_env/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/async_env/bin/activate b/async_env/bin/activate new file mode 100644 index 000000000..89ea1caed --- /dev/null +++ b/async_env/bin/activate @@ -0,0 +1,76 @@ +# This file must be used with "source bin/activate" *from bash* +# You cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # Call hash to forget past locations. Without forgetting + # past locations the $PATH changes we made may not be respected. + # See "man bash" for more details. hash is usually a builtin of your shell + hash -r 2> /dev/null + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +# on Windows, a path can contain colons and backslashes and has to be converted: +case "$(uname)" in + CYGWIN*|MSYS*|MINGW*) + # transform D:\path\to\venv to /d/path/to/venv on MSYS and MINGW + # and to /cygdrive/d/path/to/venv on Cygwin + VIRTUAL_ENV=$(cygpath /Users/sigvard/Desktop/fedn/async_env) + export VIRTUAL_ENV + ;; + *) + # use the path as-is + export VIRTUAL_ENV=/Users/sigvard/Desktop/fedn/async_env + ;; +esac + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/"bin":$PATH" +export PATH + +VIRTUAL_ENV_PROMPT='(async_env) ' +export VIRTUAL_ENV_PROMPT + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1="("'(async_env) '") ${PS1:-}" + export PS1 +fi + +# Call hash to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +hash -r 2> /dev/null diff --git a/async_env/bin/activate.csh b/async_env/bin/activate.csh new file mode 100644 index 000000000..d6fafdb65 --- /dev/null +++ b/async_env/bin/activate.csh @@ -0,0 +1,27 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. + +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV /Users/sigvard/Desktop/fedn/async_env + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(async_env) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(async_env) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/async_env/bin/activate.fish b/async_env/bin/activate.fish new file mode 100644 index 000000000..cf06671b0 --- /dev/null +++ b/async_env/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/). You cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV /Users/sigvard/Desktop/fedn/async_env + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/"bin $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) '(async_env) ' (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT '(async_env) ' +end diff --git a/async_env/bin/debugpy b/async_env/bin/debugpy new file mode 100755 index 000000000..df6775a76 --- /dev/null +++ b/async_env/bin/debugpy @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from debugpy.server.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/f2py b/async_env/bin/f2py new file mode 100755 index 000000000..f30dacce0 --- /dev/null +++ b/async_env/bin/f2py @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from numpy.f2py.f2py2e import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/fedn b/async_env/bin/fedn new file mode 100755 index 000000000..1687cb47e --- /dev/null +++ b/async_env/bin/fedn @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from fedn.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/flask b/async_env/bin/flask new file mode 100755 index 000000000..89c524bbe --- /dev/null +++ b/async_env/bin/flask @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from flask.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/fonttools b/async_env/bin/fonttools new file mode 100755 index 000000000..f4a536168 --- /dev/null +++ b/async_env/bin/fonttools @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from fontTools.__main__ import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/gunicorn b/async_env/bin/gunicorn new file mode 100755 index 000000000..1254ac428 --- /dev/null +++ b/async_env/bin/gunicorn @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from gunicorn.app.wsgiapp import run +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(run()) diff --git a/async_env/bin/ipython b/async_env/bin/ipython new file mode 100755 index 000000000..2b15b2f91 --- /dev/null +++ b/async_env/bin/ipython @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from IPython import start_ipython +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(start_ipython()) diff --git a/async_env/bin/ipython3 b/async_env/bin/ipython3 new file mode 100755 index 000000000..2b15b2f91 --- /dev/null +++ b/async_env/bin/ipython3 @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from IPython import start_ipython +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(start_ipython()) diff --git a/async_env/bin/jupyter b/async_env/bin/jupyter new file mode 100755 index 000000000..ffed37cc2 --- /dev/null +++ b/async_env/bin/jupyter @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from jupyter_core.command import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/jupyter-kernel b/async_env/bin/jupyter-kernel new file mode 100755 index 000000000..f636826f4 --- /dev/null +++ b/async_env/bin/jupyter-kernel @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from jupyter_client.kernelapp import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/jupyter-kernelspec b/async_env/bin/jupyter-kernelspec new file mode 100755 index 000000000..0e36bc3be --- /dev/null +++ b/async_env/bin/jupyter-kernelspec @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from jupyter_client.kernelspecapp import KernelSpecApp +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(KernelSpecApp.launch_instance()) diff --git a/async_env/bin/jupyter-migrate b/async_env/bin/jupyter-migrate new file mode 100755 index 000000000..1f8b4be19 --- /dev/null +++ b/async_env/bin/jupyter-migrate @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from jupyter_core.migrate import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/jupyter-run b/async_env/bin/jupyter-run new file mode 100755 index 000000000..3ee009272 --- /dev/null +++ b/async_env/bin/jupyter-run @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from jupyter_client.runapp import RunApp +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(RunApp.launch_instance()) diff --git a/async_env/bin/jupyter-troubleshoot b/async_env/bin/jupyter-troubleshoot new file mode 100755 index 000000000..7b3e7abc3 --- /dev/null +++ b/async_env/bin/jupyter-troubleshoot @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from jupyter_core.troubleshoot import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/normalizer b/async_env/bin/normalizer new file mode 100755 index 000000000..b6d332a32 --- /dev/null +++ b/async_env/bin/normalizer @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from charset_normalizer import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli.cli_detect()) diff --git a/async_env/bin/numpy-config b/async_env/bin/numpy-config new file mode 100755 index 000000000..9d34dde26 --- /dev/null +++ b/async_env/bin/numpy-config @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from numpy._configtool import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/pip b/async_env/bin/pip new file mode 100755 index 000000000..17a2683bc --- /dev/null +++ b/async_env/bin/pip @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/pip3 b/async_env/bin/pip3 new file mode 100755 index 000000000..17a2683bc --- /dev/null +++ b/async_env/bin/pip3 @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/pip3.12 b/async_env/bin/pip3.12 new file mode 100755 index 000000000..17a2683bc --- /dev/null +++ b/async_env/bin/pip3.12 @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/pyftmerge b/async_env/bin/pyftmerge new file mode 100755 index 000000000..8d9879971 --- /dev/null +++ b/async_env/bin/pyftmerge @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from fontTools.merge import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/pyftsubset b/async_env/bin/pyftsubset new file mode 100755 index 000000000..6efe279e8 --- /dev/null +++ b/async_env/bin/pyftsubset @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from fontTools.subset import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/pygmentize b/async_env/bin/pygmentize new file mode 100755 index 000000000..bf4e63e03 --- /dev/null +++ b/async_env/bin/pygmentize @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python +# -*- coding: utf-8 -*- +import re +import sys +from pygments.cmdline import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/python b/async_env/bin/python new file mode 120000 index 000000000..11b9d8853 --- /dev/null +++ b/async_env/bin/python @@ -0,0 +1 @@ +python3.12 \ No newline at end of file diff --git a/async_env/bin/python3 b/async_env/bin/python3 new file mode 120000 index 000000000..11b9d8853 --- /dev/null +++ b/async_env/bin/python3 @@ -0,0 +1 @@ +python3.12 \ No newline at end of file diff --git a/async_env/bin/python3.12 b/async_env/bin/python3.12 new file mode 120000 index 000000000..a3f050844 --- /dev/null +++ b/async_env/bin/python3.12 @@ -0,0 +1 @@ +/opt/homebrew/opt/python@3.12/bin/python3.12 \ No newline at end of file diff --git a/async_env/bin/ttx b/async_env/bin/ttx new file mode 100755 index 000000000..4eecf6574 --- /dev/null +++ b/async_env/bin/ttx @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from fontTools.ttx import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/async_env/bin/virtualenv b/async_env/bin/virtualenv new file mode 100755 index 000000000..11557e237 --- /dev/null +++ b/async_env/bin/virtualenv @@ -0,0 +1,8 @@ +#!/Users/sigvard/Desktop/fedn/async_env/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from virtualenv.__main__ import run_with_catch +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(run_with_catch()) diff --git a/async_env/pyvenv.cfg b/async_env/pyvenv.cfg new file mode 100644 index 000000000..cac2f4ba6 --- /dev/null +++ b/async_env/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /opt/homebrew/opt/python@3.12/bin +include-system-site-packages = false +version = 3.12.10 +executable = /opt/homebrew/Cellar/python@3.12/3.12.10/Frameworks/Python.framework/Versions/3.12/bin/python3.12 +command = /opt/homebrew/opt/python@3.12/bin/python3.12 -m venv /Users/sigvard/Desktop/fedn/async_env diff --git a/async_env/share/jupyter/kernels/python3/kernel.json b/async_env/share/jupyter/kernels/python3/kernel.json new file mode 100644 index 000000000..cca38a42a --- /dev/null +++ b/async_env/share/jupyter/kernels/python3/kernel.json @@ -0,0 +1,14 @@ +{ + "argv": [ + "python", + "-m", + "ipykernel_launcher", + "-f", + "{connection_file}" + ], + "display_name": "Python 3 (ipykernel)", + "language": "python", + "metadata": { + "debugger": true + } +} \ No newline at end of file diff --git a/async_env/share/jupyter/kernels/python3/logo-32x32.png b/async_env/share/jupyter/kernels/python3/logo-32x32.png new file mode 100644 index 000000000..be8133076 Binary files /dev/null and b/async_env/share/jupyter/kernels/python3/logo-32x32.png differ diff --git a/async_env/share/jupyter/kernels/python3/logo-64x64.png b/async_env/share/jupyter/kernels/python3/logo-64x64.png new file mode 100644 index 000000000..eebbff638 Binary files /dev/null and b/async_env/share/jupyter/kernels/python3/logo-64x64.png differ diff --git a/async_env/share/jupyter/kernels/python3/logo-svg.svg b/async_env/share/jupyter/kernels/python3/logo-svg.svg new file mode 100644 index 000000000..467b07b26 --- /dev/null +++ b/async_env/share/jupyter/kernels/python3/logo-svg.svg @@ -0,0 +1,265 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/async_env/share/man/man1/ipython.1 b/async_env/share/man/man1/ipython.1 new file mode 100644 index 000000000..0f4a191f3 --- /dev/null +++ b/async_env/share/man/man1/ipython.1 @@ -0,0 +1,60 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" First parameter, NAME, should be all caps +.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection +.\" other parameters are allowed: see man(7), man(1) +.TH IPYTHON 1 "July 15, 2011" +.\" Please adjust this date whenever revising the manpage. +.\" +.\" Some roff macros, for reference: +.\" .nh disable hyphenation +.\" .hy enable hyphenation +.\" .ad l left justify +.\" .ad b justify to both left and right margins +.\" .nf disable filling +.\" .fi enable filling +.\" .br insert line break +.\" .sp insert n+1 empty lines +.\" for manpage-specific macros, see man(7) and groff_man(7) +.\" .SH section heading +.\" .SS secondary section heading +.\" +.\" +.\" To preview this page as plain text: nroff -man ipython.1 +.\" +.SH NAME +ipython \- Tools for Interactive Computing in Python. +.SH SYNOPSIS +.B ipython +.RI [ options ] " files" ... + +.B ipython subcommand +.RI [ options ] ... + +.SH DESCRIPTION +An interactive Python shell with automatic history (input and output), dynamic +object introspection, easier configuration, command completion, access to the +system shell, integration with numerical and scientific computing tools, +web notebook, Qt console, and more. + +For more information on how to use IPython, see 'ipython \-\-help', +or 'ipython \-\-help\-all' for all available command\(hyline options. + +.SH "ENVIRONMENT VARIABLES" +.sp +.PP +\fIIPYTHONDIR\fR +.RS 4 +This is the location where IPython stores all its configuration files. The default +is $HOME/.ipython if IPYTHONDIR is not defined. + +You can see the computed value of IPYTHONDIR with `ipython locate`. + +.SH FILES + +IPython uses various configuration files stored in profiles within IPYTHONDIR. +To generate the default configuration files and start configuring IPython, +do 'ipython profile create', and edit '*_config.py' files located in +IPYTHONDIR/profile_default. + +.SH AUTHORS +IPython is written by the IPython Development Team . diff --git a/async_env/share/man/man1/ttx.1 b/async_env/share/man/man1/ttx.1 new file mode 100644 index 000000000..bba23b5e5 --- /dev/null +++ b/async_env/share/man/man1/ttx.1 @@ -0,0 +1,225 @@ +.Dd May 18, 2004 +.\" ttx is not specific to any OS, but contrary to what groff_mdoc(7) +.\" seems to imply, entirely omitting the .Os macro causes 'BSD' to +.\" be used, so I give a zero-width space as its argument. +.Os \& +.\" The "FontTools Manual" argument apparently has no effect in +.\" groff 1.18.1. I think it is a bug in the -mdoc groff package. +.Dt TTX 1 "FontTools Manual" +.Sh NAME +.Nm ttx +.Nd tool for manipulating TrueType and OpenType fonts +.Sh SYNOPSIS +.Nm +.Bk +.Op Ar option ... +.Ek +.Bk +.Ar file ... +.Ek +.Sh DESCRIPTION +.Nm +is a tool for manipulating TrueType and OpenType fonts. It can convert +TrueType and OpenType fonts to and from an +.Tn XML Ns -based format called +.Tn TTX . +.Tn TTX +files have a +.Ql .ttx +extension. +.Pp +For each +.Ar file +argument it is given, +.Nm +detects whether it is a +.Ql .ttf , +.Ql .otf +or +.Ql .ttx +file and acts accordingly: if it is a +.Ql .ttf +or +.Ql .otf +file, it generates a +.Ql .ttx +file; if it is a +.Ql .ttx +file, it generates a +.Ql .ttf +or +.Ql .otf +file. +.Pp +By default, every output file is created in the same directory as the +corresponding input file and with the same name except for the +extension, which is substituted appropriately. +.Nm +never overwrites existing files; if necessary, it appends a suffix to +the output file name before the extension, as in +.Pa Arial#1.ttf . +.Ss "General options" +.Bl -tag -width ".Fl t Ar table" +.It Fl h +Display usage information. +.It Fl d Ar dir +Write the output files to directory +.Ar dir +instead of writing every output file to the same directory as the +corresponding input file. +.It Fl o Ar file +Write the output to +.Ar file +instead of writing it to the same directory as the +corresponding input file. +.It Fl v +Be verbose. Write more messages to the standard output describing what +is being done. +.It Fl a +Allow virtual glyphs ID's on compile or decompile. +.El +.Ss "Dump options" +The following options control the process of dumping font files +(TrueType or OpenType) to +.Tn TTX +files. +.Bl -tag -width ".Fl t Ar table" +.It Fl l +List table information. Instead of dumping the font to a +.Tn TTX +file, display minimal information about each table. +.It Fl t Ar table +Dump table +.Ar table . +This option may be given multiple times to dump several tables at +once. When not specified, all tables are dumped. +.It Fl x Ar table +Exclude table +.Ar table +from the list of tables to dump. This option may be given multiple +times to exclude several tables from the dump. The +.Fl t +and +.Fl x +options are mutually exclusive. +.It Fl s +Split tables. Dump each table to a separate +.Tn TTX +file and write (under the name that would have been used for the output +file if the +.Fl s +option had not been given) one small +.Tn TTX +file containing references to the individual table dump files. This +file can be used as input to +.Nm +as long as the referenced files can be found in the same directory. +.It Fl i +.\" XXX: I suppose OpenType programs (exist and) are also affected. +Don't disassemble TrueType instructions. When this option is specified, +all TrueType programs (glyph programs, the font program and the +pre-program) are written to the +.Tn TTX +file as hexadecimal data instead of +assembly. This saves some time and results in smaller +.Tn TTX +files. +.It Fl y Ar n +When decompiling a TrueType Collection (TTC) file, +decompile font number +.Ar n , +starting from 0. +.El +.Ss "Compilation options" +The following options control the process of compiling +.Tn TTX +files into font files (TrueType or OpenType): +.Bl -tag -width ".Fl t Ar table" +.It Fl m Ar fontfile +Merge the input +.Tn TTX +file +.Ar file +with +.Ar fontfile . +No more than one +.Ar file +argument can be specified when this option is used. +.It Fl b +Don't recalculate glyph bounding boxes. Use the values in the +.Tn TTX +file as is. +.El +.Sh "THE TTX FILE FORMAT" +You can find some information about the +.Tn TTX +file format in +.Pa documentation.html . +In particular, you will find in that file the list of tables understood by +.Nm +and the relations between TrueType GlyphIDs and the glyph names used in +.Tn TTX +files. +.Sh EXAMPLES +In the following examples, all files are read from and written to the +current directory. Additionally, the name given for the output file +assumes in every case that it did not exist before +.Nm +was invoked. +.Pp +Dump the TrueType font contained in +.Pa FreeSans.ttf +to +.Pa FreeSans.ttx : +.Pp +.Dl ttx FreeSans.ttf +.Pp +Compile +.Pa MyFont.ttx +into a TrueType or OpenType font file: +.Pp +.Dl ttx MyFont.ttx +.Pp +List the tables in +.Pa FreeSans.ttf +along with some information: +.Pp +.Dl ttx -l FreeSans.ttf +.Pp +Dump the +.Sq cmap +table from +.Pa FreeSans.ttf +to +.Pa FreeSans.ttx : +.Pp +.Dl ttx -t cmap FreeSans.ttf +.Sh NOTES +On MS\-Windows and MacOS, +.Nm +is available as a graphical application to which files can be dropped. +.Sh SEE ALSO +.Pa documentation.html +.Pp +.Xr fontforge 1 , +.Xr ftinfo 1 , +.Xr gfontview 1 , +.Xr xmbdfed 1 , +.Xr Font::TTF 3pm +.Sh AUTHORS +.Nm +was written by +.An -nosplit +.An "Just van Rossum" Aq just@letterror.com . +.Pp +This manual page was written by +.An "Florent Rougon" Aq f.rougon@free.fr +for the Debian GNU/Linux system based on the existing FontTools +documentation. It may be freely used, modified and distributed without +restrictions. +.\" For Emacs: +.\" Local Variables: +.\" fill-column: 72 +.\" sentence-end: "[.?!][]\"')}]*\\($\\| $\\| \\| \\)[ \n]*" +.\" sentence-end-double-space: t +.\" End: \ No newline at end of file diff --git a/config/settings-combiner.yaml.local.template b/config/settings-combiner.yaml.local.template index 9fbc2282c..df1ad4536 100644 --- a/config/settings-combiner.yaml.local.template +++ b/config/settings-combiner.yaml.local.template @@ -4,7 +4,7 @@ name: combiner host: localhost address: localhost port: 12080 -max_clients: 30 +max_clients: 10000 cert_path: tmp/server.crt key_path: tmp/server.key @@ -19,14 +19,14 @@ statestore: port: 6534 storage: - storage_type: S3 + storage_type: BOTO3 storage_config: - storage_hostname: localhost - storage_port: 9000 + storage_endpoint_url: http://localhost:9000 storage_access_key: fedn_admin storage_secret_key: password storage_bucket: fedn-models context_bucket: fedn-context - storage_secure_mode: False + storage_secure_mode: False + storage_verify_ssl: False diff --git a/config/settings-combiner.yaml.template b/config/settings-combiner.yaml.template index 73e93cf7f..7d69a5721 100644 --- a/config/settings-combiner.yaml.template +++ b/config/settings-combiner.yaml.template @@ -5,7 +5,7 @@ discover_port: 8092 name: combiner host: combiner port: 12080 -max_clients: 30 +max_clients: 10000 statestore: # Available DB types are MongoDB, PostgreSQL, SQLite @@ -22,14 +22,14 @@ statestore: port: 5432 storage: - storage_type: S3 + storage_type: BOTO3 storage_config: - storage_hostname: minio - storage_port: 9000 + storage_endpoint_url: http://minio:9000 storage_access_key: fedn_admin storage_secret_key: password storage_bucket: fedn-models context_bucket: fedn-context storage_secure_mode: False + storage_verify_ssl: False diff --git a/config/settings-controller.yaml.local.template b/config/settings-controller.yaml.local.template index 96e6e07d3..6ed47c01b 100644 --- a/config/settings-controller.yaml.local.template +++ b/config/settings-controller.yaml.local.template @@ -14,12 +14,12 @@ statestore: port: 6534 storage: - storage_type: S3 + storage_type: BOTO3 storage_config: - storage_hostname: localhost - storage_port: 9000 + storage_endpoint_url: http://minio:9000 storage_access_key: fedn_admin storage_secret_key: password storage_bucket: fedn-models context_bucket: fedn-context - storage_secure_mode: False + storage_secure_mode: False + storage_verify_ssl: False diff --git a/config/settings-hooks.yaml.template b/config/settings-hooks.yaml.template index e395b20ce..eb3725d23 100644 --- a/config/settings-hooks.yaml.template +++ b/config/settings-hooks.yaml.template @@ -5,4 +5,4 @@ discover_port: 8092 name: hooks host: hooks port: 12081 -max_clients: 30 \ No newline at end of file +max_clients: 10000 diff --git a/config/settings-reducer.yaml.template b/config/settings-reducer.yaml.template index eeab66ff8..62adb5aa0 100644 --- a/config/settings-reducer.yaml.template +++ b/config/settings-reducer.yaml.template @@ -19,12 +19,12 @@ statestore: port: 5432 storage: - storage_type: S3 + storage_type: BOTO3 storage_config: - storage_hostname: minio - storage_port: 9000 + storage_endpoint_url: http://minio:9000 storage_access_key: fedn_admin storage_secret_key: password storage_bucket: fedn-models context_bucket: fedn-context - storage_secure_mode: False + storage_secure_mode: False + storage_verify_ssl: False diff --git a/docs/conf.py b/docs/conf.py index cf51eba14..1e3415faf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,7 @@ author = "Scaleout Systems AB" # The full version, including alpha/beta/rc tags -release = "0.26.0" +release = "0.28.1" # Add any Sphinx extension module names here, as strings extensions = [ diff --git a/examples/async-clients/.gitignore b/examples/async-clients/.gitignore index a3e7562db..012b7bcf4 100644 --- a/examples/async-clients/.gitignore +++ b/examples/async-clients/.gitignore @@ -4,4 +4,9 @@ data *.tar.gz *.log .async-simulation -client.yaml \ No newline at end of file +client.yaml +config.py + +tokens.json +logs/* +config.py diff --git a/examples/async-clients/Experiment.ipynb b/examples/async-clients/Experiment.ipynb index 1035eb3e4..88c20bfe0 100644 --- a/examples/async-clients/Experiment.ipynb +++ b/examples/async-clients/Experiment.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 4, "id": "1a2686dd", "metadata": {}, "outputs": [], @@ -24,14 +24,13 @@ "from sklearn.neural_network import MLPClassifier\n", "from sklearn.metrics import accuracy_score\n", "from sklearn.model_selection import train_test_split\n", - "from client.entrypoint import compile_model, load_parameters, make_data \n", - "\n", + "#from client.entrypoint import compile_model, load_parameters, make_data \n", + "from init_seed import make_data, compile_model, load_parameters\n", "\n", "from fedn import APIClient\n", "import uuid\n", "import json\n", "import matplotlib.pyplot as plt\n", - "import numpy as np\n", "import copy\n", "\n", "import warnings\n", @@ -50,7 +49,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 5, "id": "70c5f5c9", "metadata": {}, "outputs": [], @@ -69,7 +68,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 6, "id": "a985c6b3", "metadata": {}, "outputs": [ @@ -77,8 +76,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "Training accuracy: 0.9208875\n", - "Test accuracy: 0.9193\n" + "Training accuracy: 0.9211625\n", + "Test accuracy: 0.9195\n" ] } ], @@ -101,23 +100,23 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 7, "id": "34ce6b7d", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[]" + "[]" ] }, - "execution_count": 4, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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BwfrztXevXuRk5OjO1c//fST7gtgzZo1iIyMdPtiaYnsdjvKy8t5rlyMGDECP/30E7Zv3+68DRkyBBMnTnTe5/nyrLS0FAcPHkRiYiL/rlwMGzbMbeiFffv2oXPnzgAC8Pu9QUt4W5hly5YpNptNefXVV5Vdu3Yp06ZNU6Kjo3XV461BSUmJsm3bNmXbtm0KAGXBggXKtm3blCNHjiiKIrq9RUdHKx999JGyY8cO5frrr/fY7W3QoEHKpk2blPXr1ys9evRocd2a77rrLiUqKkpZu3atrkvl2bNnndtMnz5d6dSpk/L1118rW7ZsUdLS0pS0tDTn87JL5ciRI5Xt27crq1evVmJjY1tkl8qHH35YWbdunXL48GFlx44dysMPP6yYTCblyy+/VBSF58obbS8hReH5ku6//35l7dq1yuHDh5XvvvtOSU9PV2JiYpSCggJFUXietDZv3qwEBQUpTz/9tLJ//37lrbfeUsLCwpQ333zTuU0gfb8zYPHiX//6l9KpUyfFarUqQ4cOVTZu3Gj0ITW5b775RgHgdps8ebKiKKLr29y5c5X4+HjFZrMpI0aMUPbu3avbx6lTp5Tx48cr4eHhSmRkpDJlyhSlpKTEgE/TeDydIwDKK6+84tzm3Llzyt133620bdtWCQsLU2644QblxIkTuv388ssvyqhRo5TQ0FAlJiZGuf/++5XKysom/jSN7w9/+IPSuXNnxWq1KrGxscqIESOcwYqi8Fx54xqw8HwJ48aNUxITExWr1ap06NBBGTdunG5cEZ4nvU8++US58MILFZvNpvTu3Vt56aWXdM8H0ve7SVEUpWFzNkREREQNizUsREREFPAYsBAREVHAY8BCREREAY8BCxEREQU8BixEREQU8BiwEBERUcBjwEJEREQBjwELERERBTwGLERERBTwGLAQERFRwGPAQkRERAGPAQsREREFvP8HF9TES/3jU/cAAAAASUVORK5CYII=\n", + "image/png": "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KHgELAAAoOARsAAAgIJHwAIAAGreas2FyKzfqIsoAQCA4mDO25msw1wjApZNmzY5t506dYp6VwAAQA7ncV21OZWSWCZhTYErLy+X5cuXyx577CElJSV5j/40EFqyZEnapa9rO45VdjhemeNYZY5jlTmOVfTHS0MQDVbat28vpaWlNT/Dor9kx44dq/Rn6H8OL+jMcKyyw/HKHMcqcxyrzHGsoj1e6TIrBk23AACg4BGwAACAgkfAkkb9+vVl3Lhxzi1S41hlh+OVOY5V5jhWmeNYFdfxqhFNtwAAoGYjwwIAAAoeAQsAACh4BCwAAKDgEbAAAICCR8CSxsSJE6Vr167SoEEDGTx4sMyePVtqmzfeeENOPvlkZyZCnUn42Wef9X1d+7bHjh0r7dq1k4YNG8rQoUNl/vz5vuesW7dOzjnnHGeyoebNm8tFF10kmzdvlppk/PjxctBBBzkzLu+1115y6qmnyrx583zP2bZtm1x++eXSsmVLadKkiZx++umycuVK33MWL14sJ554ojRq1MjZzjXXXCO7du2Smua+++6Tfv36JSahGjJkiPz3v/9NfJ1jldxvfvMb52/x5z//eeJzHC/XLbfc4hwb+6N3796Jr3Oc/JYtWybnnnuuczz0/btv377y3nvvFeb7u44SQrgnnngiVq9evdhDDz0U+/TTT2OjR4+ONW/ePLZy5cpYbfLiiy/GbrzxxtjkyZN1RFnsX//6l+/rv/nNb2LNmjWLPfvss7H//e9/sVNOOSXWrVu32HfffZd4znHHHRfr379/7J133om9+eabsR49esTOOuusWE0ybNiw2F//+tfYJ598Evvwww9jJ5xwQqxz586xzZs3J55z6aWXxjp16hSbNm1a7L333osdcsghsUMPPTTx9V27dsX233//2NChQ2MffPCBc+xbtWoVGzNmTKymee6552IvvPBC7IsvvojNmzcvdsMNN8Tq1q3rHD/FsQo3e/bsWNeuXWP9+vWLXXnllYnPc7xc48aNi+23336xb775JvGxevXqxNc5Tp5169bFunTpErvgggtis2bNii1cuDD20ksvxRYsWFCQ7+8ELCkcfPDBscsvvzzxePfu3bH27dvHxo8fH6utggFLeXl5rG3btrHf//73ic+tX78+Vr9+/dg//vEP5/Fnn33mfN+7776beM5///vfWElJSWzZsmWxmmrVqlXO7/36668njouekJ966qnEc+bOnes8Z+bMmc5jfXMsLS2NrVixIvGc++67L9a0adPY9u3bYzXdnnvuGfvLX/7CsUpi06ZNsZ49e8amTp0aO/LIIxMBC8fLH7DoyTMMx8nvuuuuix1++OGxZArt/Z2SUBI7duyQOXPmOOkve80ifTxz5sxI962QLFq0SFasWOE7TrouhJbPzHHSW00TDho0KPEcfb4ez1mzZklNtWHDBue2RYsWzq2+nnbu3Ok7Vpqq7ty5s+9YaUq2TZs2iecMGzbMWXTs008/lZpq9+7d8sQTT8iWLVuc0hDHKpyWMrRUYR8XxfHy05KFlrC7d+/ulCq0xKM4Tn7PPfec8758xhlnOKWvAw44QB544IGCfX8nYElizZo1zpuo/aJV+lj/A+EyxyLVcdJb/WOw1alTxzmR19RjqSuIa3/BYYcdJvvvv7/zOf1d69Wr5/xxpzpWYcfSfK2m+fjjj50+Ap0589JLL5V//etf0qdPH45VCA3o3n//fadXKojj5dGT6cMPPyxTpkxx+qT0pHvEEUc4KwJznPwWLlzoHKOePXvKSy+9JJdddpn87Gc/k0ceeaQg399rxGrNQCFeCX/yyScyY8aMqHeloPXq1Us+/PBDJxv19NNPy/nnny+vv/561LtVcJYsWSJXXnmlTJ061RkAgOSOP/74xH1t6tYApkuXLvLPf/7TaRqF/8JKMyN33HGH81gzLPq+NWnSJOdvsdCQYUmiVatWUlZWVqF7XB+3bds2sv0qNOZYpDpOertq1Srf17XjXjvLa+KxvOKKK+Q///mPvPbaa9KxY8fE5/V31VLj+vXrUx6rsGNpvlbT6NVujx49ZODAgU7moH///nLXXXdxrAK0lKF/QwceeKBz9aofGtjdfffdzn294uV4hdNsyj777CMLFizgdRWgI380o2nbd999EyW0Qnt/J2BJ8Uaqb6LTpk3zRaP6WGvscHXr1s15UdrHSWu9Wrs0x0lv9Q1C33SNV1991TmeevVTU2hPsgYrWtbQ30+PjU1fT3Xr1vUdKx32rG8O9rHSMon9BqBX1TpcMPjGUhPpa2L79u0cq4BjjjnG+V01G2U+9MpY+zPMfY5XOB1e++WXXzonZ15XflqyDk698MUXXzgZqYJ8f89rC28NHNas3dAPP/yw0wl9ySWXOMOa7e7x2kBHJujwPv3Ql8yECROc+19//XVi2Jsel3//+9+xjz76KDZ8+PDQYW8HHHCAM3RuxowZzkiHmjas+bLLLnOG/02fPt03pHLr1q2+IZU61PnVV191hlQOGTLE+QgOqTz22GOdodFTpkyJtW7dukYOqbz++uudEVSLFi1yXjf6WEcWvPzyy87XOVap2aOEFMfLdfXVVzt/g/q6euutt5zhyTosWUftKY6Tf4h8nTp1Yrfffnts/vz5sb///e+xRo0axR577LHEcwrp/Z2AJY177rnHeXHrfCw6zFnHmdc2r732mhOoBD/OP//8xNC3m2++OdamTRsnwDvmmGOceTVsa9eudV7ATZo0cYYHjho1ygmEapKwY6QfOjeLoX/kP/nJT5zhu/rGcNpppzlBje2rr76KHX/88bGGDRs6b7T6Brxz585YTXPhhRc6c0Do35aeEPR1Y4IVxbHKLmDheLlGjBgRa9eunfO66tChg/PYnleE4+T3/PPPOwGavnf37t07dv/99/u+Xkjv7yX6T35zNgAAAPlFDwsAACh4BCwAAKDgEbAAAICCR8ACAAAKHgELAAAoeAQsAACg4BGwAACAgkfAAgAACh4BCwAAKHgELAAAoOARsAAAgIJHwAIAAKTQ/X9fqOgqWLGzzQAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -152,23 +151,23 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "id": "79f02df8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 5, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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\n", + "image/png": "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/oNl7dDGiYmBhyWsMk2cwaKcRbV9+MsLBGNYCIlJzj77bLntttvkv//9r1SsWFF14np1Yw1cHDfeeKNaLRgLsmFW+v3336vXPvroIylfvrxMnDhRrcqckpKiVmVOT09XqzXXqlVLSpUqJR06dPBaXVi/D/tp0qSJpKamyqWXXipHjx6VMWPGSL169dRaOTg2DNCaYPc7depUadasmZQuXVqtDozZNMB3w/6/++47JazMVY/vu+8+tToyjgWrWmOl4YyMjDy179y5c6VLly5SsmRJNavH9zly5Ij79U8++UStDI1VotH2V111lezevTuXm+nHH3+Utm3bqvbFPoM5b04ukG+//VatlIzviFWS58+f7/We9957Tx0nXu/Xr59aRRntGQo4Rzi2qlWrquulc+fO8ttvv+X6TlghHN8dn4VVutesWeO1H5wjrNiNfeB8PPbYY5KZmen3sz/44AM55ZRTVDvVqFFDrRzthJNL6K+//lIWKVwzuNavueYa2bt3r/v1QG2Oaxag3bBv/bjQcRUB0tLSsOK0us13Pu7nco0s63It/Tz/901IFHLs2DHXypUr1a2b7GyXK/1w4f/hc4Nk0KBBrr59+7ofd+3a1VW2bFnXo48+6lq7dq1rzJgxrri4ONdPP/2kXs/KynKdccYZrlNOOUU9t2HDBtekSZNckydPVq9/+OGHrqSkJNeZZ57pmjdvnmv16tWuI0eOuIYMGaKe++WXX1zr1693vfDCC66UlBT1Geb7zjvvPNeSJUtcs2fPdlWqVMl1/vnnuy6//HLXihUr1OckJye7xo4d6z7eYPfbvXt312+//eZavHixq1mzZq6rrrpKvX7o0CG1/549e7p27Nih/tLT09VrTzzxhPoOmzZtck2cONFVrVo113PPPef+7JEjR7pat27ts21nzpyp+th///1XPcbxlSpVyvXKK6+o48O+27Rp47ruuuvc73n//fdVW6Jd58+f7+rYsaOrV69eufbZqlUr1f7Y5759+wKeN4D3jR8/Xt3Hd8Ljpk2bur7//nvXmjVrXJdeeqmrbt26royMDLXN3LlzXfHx8apN8fqoUaNcFStWdJUrV84VCrfddpurZs2a6nvhPOKaq1Chgjpu8zt16NDBNWvWLLVNly5d1HnV4Pzi+3300UeqbfC96tWrp76vL9566y1XiRIlXK+++qo6/kWL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", "text/plain": [ "
" ] @@ -242,12 +241,12 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 9, "id": "1061722d", "metadata": {}, "outputs": [], "source": [ - "DISCOVER_HOST = '127.0.0.1'\n", + "DISCOVER_HOST = '100.84.229.36'\n", "DISCOVER_PORT = 8092\n", "client = APIClient(DISCOVER_HOST, DISCOVER_PORT)" ] @@ -262,19 +261,21 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 23, "id": "5fdabea3", "metadata": {}, "outputs": [], "source": [ - "def load_fedn_model(model_id):\n", - "\n", - " data = client.download_model(model_id, 'temp.npz')\n", - " parameters = load_parameters('temp.npz')\n", + "def load_fedn_model(model_id, nr):\n", + " data = client.download_model(model_id, f\"models/{nr}_{model_id}.npz\")\n", + " parameters = np.load(f\"models/{nr}_{model_id}.npz\")\n", " model = compile_model()\n", - " n = len(parameters)//2\n", - " model.coefs_ = parameters[:n]\n", - " model.intercepts_ = parameters[n:]\n", + " sorted_keys = sorted(parameters.files, key=lambda x: int(x))\n", + " weight_list = [parameters[k] for k in sorted_keys]\n", + "\n", + " n = len(weight_list)//2\n", + " model.coefs_ = weight_list[:n]\n", + " model.intercepts_ = weight_list[n:]\n", " return model" ] }, @@ -288,17 +289,1245 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 24, "id": "13b75b1c", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[{'committed_at': 'Tue, 22 Apr 2025 19:10:30 GMT', 'model': 'ff5678e8-d091-4b10-84aa-20f14176d9b3', 'model_id': 'ff5678e8-d091-4b10-84aa-20f14176d9b3', 'name': None, 'parent_model': '870406a8-2a4a-477f-85a2-195d445183bd', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:53:09 GMT', 'model': 'fe1b5457-23d6-481d-b021-35294ee4d664', 'model_id': 'fe1b5457-23d6-481d-b021-35294ee4d664', 'name': None, 'parent_model': '914f8068-0df6-4def-8d5d-30d5b72e1449', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:06:47 GMT', 'model': 'fd85fd68-8fae-4a90-a27c-aa23ab913c84', 'model_id': 'fd85fd68-8fae-4a90-a27c-aa23ab913c84', 'name': None, 'parent_model': '8cb36005-e8bd-4c76-885a-bfa834b1f1b3', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:48:15 GMT', 'model': 'fc026787-10ac-41e8-af8f-b08b2058628b', 'model_id': 'fc026787-10ac-41e8-af8f-b08b2058628b', 'name': None, 'parent_model': '04ce09d9-8e10-4512-b324-66075852dd62', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:56:35 GMT', 'model': 'fbfa18df-716a-482d-a264-fa8bdba5f63e', 'model_id': 'fbfa18df-716a-482d-a264-fa8bdba5f63e', 'name': None, 'parent_model': 'dde40753-37dc-4917-be82-b70b9e0203b0', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:04:00 GMT', 'model': 'fb875eae-c14a-4437-9cb2-46881756d527', 'model_id': 'fb875eae-c14a-4437-9cb2-46881756d527', 'name': None, 'parent_model': '942f6bb8-1e4a-4dc4-a2e2-c15629abc0cc', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:34:50 GMT', 'model': 'fa7b76dc-ce39-4e53-a80d-63eeaafd4589', 'model_id': 'fa7b76dc-ce39-4e53-a80d-63eeaafd4589', 'name': None, 'parent_model': '89287967-1617-40d5-adef-00dbdbe6743e', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 21:15:44 GMT', 'model': 'fa618464-4ea6-4a4f-a521-3ec8611f3179', 'model_id': 'fa618464-4ea6-4a4f-a521-3ec8611f3179', 'name': None, 'parent_model': '5d446bec-513e-4238-8ab2-ba8853459519', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:18:18 GMT', 'model': 'fa52fcd6-3799-422e-8281-46b1547040ed', 'model_id': 'fa52fcd6-3799-422e-8281-46b1547040ed', 'name': None, 'parent_model': '97686b04-003b-4112-89df-bafae9d14093', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:08:40 GMT', 'model': 'f82ed811-8879-4b49-9f0e-47ebda9a3902', 'model_id': 'f82ed811-8879-4b49-9f0e-47ebda9a3902', 'name': None, 'parent_model': '9091c458-cd5f-4a3e-805f-384772f8d611', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:55:20 GMT', 'model': 'f74785bd-ab87-4aaa-836f-6e93fc28245f', 'model_id': 'f74785bd-ab87-4aaa-836f-6e93fc28245f', 'name': None, 'parent_model': 'f2561655-de53-4ed6-9d6a-1a9614b8a721', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:16:35 GMT', 'model': 'f73998dc-dbd2-4907-9730-4c9b803c6b00', 'model_id': 'f73998dc-dbd2-4907-9730-4c9b803c6b00', 'name': None, 'parent_model': '26bdd07c-04da-4aae-91c3-8e7b74eba5cd', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:47:09 GMT', 'model': 'f72642e1-3169-4eca-943f-a5b6a8934f29', 'model_id': 'f72642e1-3169-4eca-943f-a5b6a8934f29', 'name': None, 'parent_model': '3793a688-89a2-4cf8-b863-2b63f3c8eb82', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:33:03 GMT', 'model': 'f6843d85-a65f-4119-9f9f-28ae34dbab44', 'model_id': 'f6843d85-a65f-4119-9f9f-28ae34dbab44', 'name': None, 'parent_model': '3e9197c4-a53b-4f12-af20-f25d3d351541', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:57:47 GMT', 'model': 'f601f7ae-34df-4e44-b38e-ce3c31ac1603', 'model_id': 'f601f7ae-34df-4e44-b38e-ce3c31ac1603', 'name': None, 'parent_model': '9e584841-255f-4103-ab0b-a53964e054e7', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:03:48 GMT', 'model': 'f4937451-6a01-453b-918d-f5c32c638d3d', 'model_id': 'f4937451-6a01-453b-918d-f5c32c638d3d', 'name': None, 'parent_model': '59e5d4b7-27c1-407f-af2d-6a29ccece154', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:59:42 GMT', 'model': 'f464429d-b8c4-4186-8886-ef99a9c14b0c', 'model_id': 'f464429d-b8c4-4186-8886-ef99a9c14b0c', 'name': None, 'parent_model': 'bf16d524-25d1-4835-ac7f-69c8811cfd28', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:49:29 GMT', 'model': 'f44d4811-b524-4d9c-8f2e-01b3e087dd50', 'model_id': 'f44d4811-b524-4d9c-8f2e-01b3e087dd50', 'name': None, 'parent_model': 'aab098f1-bda5-49e3-8787-5ef259aabf14', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:36:28 GMT', 'model': 'f3c56650-5d74-4bd4-92be-3b31ec1437fb', 'model_id': 'f3c56650-5d74-4bd4-92be-3b31ec1437fb', 'name': None, 'parent_model': 'f369ce9e-fb75-4fb7-892f-4983b827aaba', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:19:06 GMT', 'model': 'f3b456cd-b8f2-4f05-ab26-b153095c765a', 'model_id': 'f3b456cd-b8f2-4f05-ab26-b153095c765a', 'name': None, 'parent_model': 'a283e30d-8daa-41d3-9c28-2d6db5383fcf', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:31:34 GMT', 'model': 'f38c1231-1104-4258-90fc-96393ba79207', 'model_id': 'f38c1231-1104-4258-90fc-96393ba79207', 'name': None, 'parent_model': '8b002342-5460-4384-b194-9be724274744', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:03:44 GMT', 'model': 'f38a00ee-3ba0-4b1f-a0c0-d0bb1d54b91e', 'model_id': 'f38a00ee-3ba0-4b1f-a0c0-d0bb1d54b91e', 'name': None, 'parent_model': 'd47d06d0-b827-4f73-8a4d-6509cee4dfff', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:36:02 GMT', 'model': 'f369ce9e-fb75-4fb7-892f-4983b827aaba', 'model_id': 'f369ce9e-fb75-4fb7-892f-4983b827aaba', 'name': None, 'parent_model': 'd03fbd31-58ec-4e9f-9794-c8d89f812aa4', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:54:54 GMT', 'model': 'f2561655-de53-4ed6-9d6a-1a9614b8a721', 'model_id': 'f2561655-de53-4ed6-9d6a-1a9614b8a721', 'name': None, 'parent_model': 'e8161d73-02c6-4432-bb48-46ccd98e9b46', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:45:07 GMT', 'model': 'f23ef93e-0347-48f4-9cef-80d2076412d8', 'model_id': 'f23ef93e-0347-48f4-9cef-80d2076412d8', 'name': None, 'parent_model': '8d8a7bef-e6a5-4003-9f4e-44fd15d2a4d6', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:28:36 GMT', 'model': 'f23db284-9a07-4665-a9a3-dd69f2b6dc62', 'model_id': 'f23db284-9a07-4665-a9a3-dd69f2b6dc62', 'name': None, 'parent_model': 'da34a6e5-dc28-4b15-b423-f46c0da3d99c', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:49:14 GMT', 'model': 'f1ea5492-cca8-4bcb-a4fe-5f52d2592b62', 'model_id': 'f1ea5492-cca8-4bcb-a4fe-5f52d2592b62', 'name': None, 'parent_model': '8fa9728d-fc1f-47f3-adad-b05d7f7c7435', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:41:47 GMT', 'model': 'f1ab8576-570c-4f9a-a05a-ce4936330cc1', 'model_id': 'f1ab8576-570c-4f9a-a05a-ce4936330cc1', 'name': None, 'parent_model': '6e412898-bbb0-4eaf-a4fe-86fee3029297', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:20:58 GMT', 'model': 'f154b375-b563-4d7c-b73a-7b842d2846a6', 'model_id': 'f154b375-b563-4d7c-b73a-7b842d2846a6', 'name': None, 'parent_model': 'c4d15b8b-6b82-4def-a91b-0acc07d42a96', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:26:26 GMT', 'model': 'f064448a-c863-4c28-ae03-8472e8e96b5b', 'model_id': 'f064448a-c863-4c28-ae03-8472e8e96b5b', 'name': None, 'parent_model': 'c92a477f-f09b-425d-8a7d-5946b32166b1', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:37:43 GMT', 'model': 'f026363e-d0df-4274-af52-44e3f3adfab7', 'model_id': 'f026363e-d0df-4274-af52-44e3f3adfab7', 'name': None, 'parent_model': 'dadc9d4f-d244-4e3d-b606-665426415307', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:28:34 GMT', 'model': 'efa5418f-d670-495b-8bec-8a298069c200', 'model_id': 'efa5418f-d670-495b-8bec-8a298069c200', 'name': None, 'parent_model': '2d3378c4-16c3-4ec1-9ed4-e904a7791dce', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:27:19 GMT', 'model': 'ef4c64a7-41fb-4798-8662-852a99169968', 'model_id': 'ef4c64a7-41fb-4798-8662-852a99169968', 'name': None, 'parent_model': '17c3d174-fbe2-4314-a372-fd98abe5eef6', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:33:28 GMT', 'model': 'ef0627ab-e649-4fba-b2b7-62c64e916e0a', 'model_id': 'ef0627ab-e649-4fba-b2b7-62c64e916e0a', 'name': None, 'parent_model': 'f6843d85-a65f-4119-9f9f-28ae34dbab44', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:59:28 GMT', 'model': 'eed875ba-0c5e-4e72-84bf-2f7cd2818616', 'model_id': 'eed875ba-0c5e-4e72-84bf-2f7cd2818616', 'name': None, 'parent_model': '9e0a3195-0a68-45f8-9075-306adec03b00', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:09:17 GMT', 'model': 'ee11559f-a6aa-46cf-8c0e-ce6aadcd36b5', 'model_id': 'ee11559f-a6aa-46cf-8c0e-ce6aadcd36b5', 'name': None, 'parent_model': '06fd78cd-5d7a-4081-b97f-f10ed89b5fce', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:58:36 GMT', 'model': 'ee0780cd-6819-48a2-ab3f-2cfd536125b9', 'model_id': 'ee0780cd-6819-48a2-ab3f-2cfd536125b9', 'name': None, 'parent_model': '60557e06-391c-465e-a554-f0e32dc95c41', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:35:19 GMT', 'model': 'ed7d4446-4361-4923-9f7e-c662c07f32c5', 'model_id': 'ed7d4446-4361-4923-9f7e-c662c07f32c5', 'name': None, 'parent_model': 'd4e60743-f50a-4cc9-9a29-dfdbd242805c', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:35:34 GMT', 'model': 'ecea315a-c00e-47a0-897e-6bc6784438c4', 'model_id': 'ecea315a-c00e-47a0-897e-6bc6784438c4', 'name': None, 'parent_model': '5d1659d5-984e-4a56-a4e0-c2ac634424b9', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:08:42 GMT', 'model': 'ebc02fbb-23cb-47ab-99da-4cfc0c89d187', 'model_id': 'ebc02fbb-23cb-47ab-99da-4cfc0c89d187', 'name': None, 'parent_model': '95c8c8cd-156c-4879-8642-b3bf21efe052', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:39:44 GMT', 'model': 'eb4c4784-c1e1-4015-b943-3d720e30662b', 'model_id': 'eb4c4784-c1e1-4015-b943-3d720e30662b', 'name': None, 'parent_model': '40e401ff-b056-47e3-942f-d34789d491e9', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:29:33 GMT', 'model': 'e99b6e2e-7b9d-49ac-b40b-9920328028c1', 'model_id': 'e99b6e2e-7b9d-49ac-b40b-9920328028c1', 'name': None, 'parent_model': '8e64041d-e896-4ae8-9836-393f2940099b', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:46:38 GMT', 'model': 'e90e788a-927d-4683-a02d-83c9de1d8b80', 'model_id': 'e90e788a-927d-4683-a02d-83c9de1d8b80', 'name': None, 'parent_model': '5e898a76-71ac-4a87-8c6f-cd6415fbee72', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:38:33 GMT', 'model': 'e8b75195-f7ef-4ae2-bdae-e93591d48030', 'model_id': 'e8b75195-f7ef-4ae2-bdae-e93591d48030', 'name': None, 'parent_model': '6e990e93-8a32-4877-a998-c56b6d317024', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:17:41 GMT', 'model': 'e89be0b2-deeb-4079-b69f-223a9600e727', 'model_id': 'e89be0b2-deeb-4079-b69f-223a9600e727', 'name': None, 'parent_model': None, 'session_id': None}, {'committed_at': 'Tue, 22 Apr 2025 21:07:36 GMT', 'model': 'e8305bff-cb91-4e0a-92e9-9e112a32602e', 'model_id': 'e8305bff-cb91-4e0a-92e9-9e112a32602e', 'name': None, 'parent_model': '212d24e6-aa7e-471f-b759-782f1c011a04', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:54:29 GMT', 'model': 'e8161d73-02c6-4432-bb48-46ccd98e9b46', 'model_id': 'e8161d73-02c6-4432-bb48-46ccd98e9b46', 'name': None, 'parent_model': '226501b9-837a-403e-a4bc-998f01c9afc1', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:21:49 GMT', 'model': 'e76cc2d8-eda1-4e51-ab19-908073decaec', 'model_id': 'e76cc2d8-eda1-4e51-ab19-908073decaec', 'name': None, 'parent_model': '26960d33-5d8e-48b3-bdcb-703578bed79e', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:05:49 GMT', 'model': 'e6b7a0d8-4a35-46ba-a168-d07481768d42', 'model_id': 'e6b7a0d8-4a35-46ba-a168-d07481768d42', 'name': None, 'parent_model': '563c20aa-883c-40a6-87e9-46f706ede8c8', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:23:32 GMT', 'model': 'e6907a1b-358d-4c05-8d8e-570a55d54fb1', 'model_id': 'e6907a1b-358d-4c05-8d8e-570a55d54fb1', 'name': None, 'parent_model': '40219deb-9950-4415-8184-577d212095da', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 21:01:06 GMT', 'model': 'e677469f-a332-43c6-ae5e-28c86c83bd12', 'model_id': 'e677469f-a332-43c6-ae5e-28c86c83bd12', 'name': None, 'parent_model': '48f4cfc8-6f7f-49b6-9089-71aadbe27e45', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:43:39 GMT', 'model': 'e626adf3-78b6-4659-ab8d-58756a64492b', 'model_id': 'e626adf3-78b6-4659-ab8d-58756a64492b', 'name': None, 'parent_model': '1e5f82c4-72b9-4ad2-b23f-9478b828c436', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:15:13 GMT', 'model': 'e61b7d82-d940-488d-b538-2c22a3a6aa79', 'model_id': 'e61b7d82-d940-488d-b538-2c22a3a6aa79', 'name': None, 'parent_model': '032e49fa-fa0d-44fd-b0bf-107fcf69716c', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:37:41 GMT', 'model': 'e57d785e-3100-458f-a27e-c2623c403e7b', 'model_id': 'e57d785e-3100-458f-a27e-c2623c403e7b', 'name': None, 'parent_model': '40a961e4-db23-483f-9af7-202e9393e19f', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:30:56 GMT', 'model': 'e5562c5f-8f08-4e3a-a02b-231bfbc2d7d9', 'model_id': 'e5562c5f-8f08-4e3a-a02b-231bfbc2d7d9', 'name': None, 'parent_model': '1ef307dc-6956-45e5-a36d-3cd799bd9467', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:28:45 GMT', 'model': 'e5517996-7b1b-4785-be2c-269cfac390bf', 'model_id': 'e5517996-7b1b-4785-be2c-269cfac390bf', 'name': None, 'parent_model': '74cfd7c2-1a63-420c-be0e-f7609e8917fd', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:52:04 GMT', 'model': 'e476a094-0fbb-4b0c-8cef-a32f3ae07135', 'model_id': 'e476a094-0fbb-4b0c-8cef-a32f3ae07135', 'name': None, 'parent_model': '7b42305d-9ce6-42f7-b15c-4e1d9edc4ac7', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:10:02 GMT', 'model': 'e404e033-3855-4613-aae1-33635468e9d8', 'model_id': 'e404e033-3855-4613-aae1-33635468e9d8', 'name': None, 'parent_model': 'ba765d36-1375-4666-839e-f6403e192821', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:35:56 GMT', 'model': 'e3e8887d-38be-4320-b6b8-c883d45b1cd6', 'model_id': 'e3e8887d-38be-4320-b6b8-c883d45b1cd6', 'name': None, 'parent_model': '4617858c-c8b3-4bfc-8ba8-f4a4dda884f1', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:19:56 GMT', 'model': 'e2b615d9-5466-49e3-8dca-a4acb2658b3c', 'model_id': 'e2b615d9-5466-49e3-8dca-a4acb2658b3c', 'name': None, 'parent_model': '0068194e-63cd-4f17-b5fa-e7c26a783f04', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 21:17:21 GMT', 'model': 'e187f02b-091f-41e8-8403-e07a1b879d33', 'model_id': 'e187f02b-091f-41e8-8403-e07a1b879d33', 'name': None, 'parent_model': '6215e9c7-cd0c-46af-ab4f-28b2d22d9ffc', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:14:58 GMT', 'model': 'e1269ed8-2950-42d6-a4da-92adf9bc71f5', 'model_id': 'e1269ed8-2950-42d6-a4da-92adf9bc71f5', 'name': None, 'parent_model': '4c51b608-b4b0-4e92-ae7c-fb26679b8b72', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:24:26 GMT', 'model': 'e09fcfb1-b66a-438d-b139-a1441be05a23', 'model_id': 'e09fcfb1-b66a-438d-b139-a1441be05a23', 'name': None, 'parent_model': 'c30f3b50-112f-4b94-8f0c-ded494d3e6ed', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:50:28 GMT', 'model': 'e041fa07-c4c1-4f4e-b30a-2d0a5ad8d7d2', 'model_id': 'e041fa07-c4c1-4f4e-b30a-2d0a5ad8d7d2', 'name': None, 'parent_model': '51f097eb-b1a4-474a-806e-837ca810635a', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:44:15 GMT', 'model': 'e02b1786-0d08-4d89-9199-ec6dc4e1e151', 'model_id': 'e02b1786-0d08-4d89-9199-ec6dc4e1e151', 'name': None, 'parent_model': '72922f7f-8238-4a7b-97ec-2b66fb104bfd', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:52:20 GMT', 'model': 'dfbee681-261a-4d40-8589-8fe60f05d1eb', 'model_id': 'dfbee681-261a-4d40-8589-8fe60f05d1eb', 'name': None, 'parent_model': '275513e5-2d5f-40fd-a735-60114aecfcb0', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:15:36 GMT', 'model': 'df25ec02-f8ac-42a7-a14c-7cfa86ef3fc3', 'model_id': 'df25ec02-f8ac-42a7-a14c-7cfa86ef3fc3', 'name': None, 'parent_model': 'e61b7d82-d940-488d-b538-2c22a3a6aa79', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:01:29 GMT', 'model': 'de4b37dc-4eb6-4fe8-9e78-d3b95dd6b976', 'model_id': 'de4b37dc-4eb6-4fe8-9e78-d3b95dd6b976', 'name': None, 'parent_model': 'af38ca86-443e-4b90-9720-19dff28f6cee', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:56:11 GMT', 'model': 'dde40753-37dc-4917-be82-b70b9e0203b0', 'model_id': 'dde40753-37dc-4917-be82-b70b9e0203b0', 'name': None, 'parent_model': '3ed5545d-f3f7-4798-83ea-921a4546f3e0', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:21:58 GMT', 'model': 'ddc8d2ad-4113-4ab4-be8f-bacdcf910ef3', 'model_id': 'ddc8d2ad-4113-4ab4-be8f-bacdcf910ef3', 'name': None, 'parent_model': 'd12efb27-2c5e-49b3-b4f5-c2fb98581660', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:39:19 GMT', 'model': 'dd6bb260-b20d-4ee0-9903-f728de991671', 'model_id': 'dd6bb260-b20d-4ee0-9903-f728de991671', 'name': None, 'parent_model': 'cafc3f96-a7ff-4f32-b0a0-b8b201b112d9', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:11:09 GMT', 'model': 'dcad8dd7-4477-4773-81a9-91eed944c098', 'model_id': 'dcad8dd7-4477-4773-81a9-91eed944c098', 'name': None, 'parent_model': 'c1522204-4a16-4bc1-a7c0-dc44f188ab6a', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:24:47 GMT', 'model': 'dc383596-46a7-468a-b44e-3995ee5d9ad6', 'model_id': 'dc383596-46a7-468a-b44e-3995ee5d9ad6', 'name': None, 'parent_model': '01b9c6df-1c8c-4e6b-a56b-d8f8dd4c97b0', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:03:55 GMT', 'model': 'db9edf85-ed30-41e1-8ae8-81f60e472851', 'model_id': 'db9edf85-ed30-41e1-8ae8-81f60e472851', 'name': None, 'parent_model': '3546efe3-3659-4e1c-9d0b-d9297b850967', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:57:35 GMT', 'model': 'db3525c9-f410-4979-8eef-a514c50f8334', 'model_id': 'db3525c9-f410-4979-8eef-a514c50f8334', 'name': None, 'parent_model': '1fc2107d-a8d0-4871-9b0c-48aff13a4d26', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:37:19 GMT', 'model': 'dadc9d4f-d244-4e3d-b606-665426415307', 'model_id': 'dadc9d4f-d244-4e3d-b606-665426415307', 'name': None, 'parent_model': '6b718264-4d61-4faf-83c6-8cec696fd260', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:53:34 GMT', 'model': 'da5d1cb1-30a9-4b81-95e3-5c00e1589c22', 'model_id': 'da5d1cb1-30a9-4b81-95e3-5c00e1589c22', 'name': None, 'parent_model': 'fe1b5457-23d6-481d-b021-35294ee4d664', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:28:10 GMT', 'model': 'da34a6e5-dc28-4b15-b423-f46c0da3d99c', 'model_id': 'da34a6e5-dc28-4b15-b423-f46c0da3d99c', 'name': None, 'parent_model': '9d5e2dfb-1491-41d7-9bdb-4e7fcfbe4bda', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:42:36 GMT', 'model': 'd93f0553-a98c-4be5-be31-2720bec70a1e', 'model_id': 'd93f0553-a98c-4be5-be31-2720bec70a1e', 'name': None, 'parent_model': '8aff477e-1f21-4544-aaf0-99bb2c6631da', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:39:48 GMT', 'model': 'd934e8b2-4906-4300-b345-11e810c20391', 'model_id': 'd934e8b2-4906-4300-b345-11e810c20391', 'name': None, 'parent_model': 'ad3797b9-6a62-4fba-bed6-34e4f03bf2d8', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:42:12 GMT', 'model': 'd909f93e-495a-4d87-a088-db8eeab6e995', 'model_id': 'd909f93e-495a-4d87-a088-db8eeab6e995', 'name': None, 'parent_model': '0a29f813-ed43-45ab-ac9d-9aebc762ddfa', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 21:11:16 GMT', 'model': 'd8c8a6da-06f3-408c-8ece-567ac258c8b5', 'model_id': 'd8c8a6da-06f3-408c-8ece-567ac258c8b5', 'name': None, 'parent_model': '6384bf43-0c3a-43de-b865-c95f1ce46383', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:16:08 GMT', 'model': 'd89e7f71-9a6a-4b81-87c9-cf0ee5e4affa', 'model_id': 'd89e7f71-9a6a-4b81-87c9-cf0ee5e4affa', 'name': None, 'parent_model': 'fa618464-4ea6-4a4f-a521-3ec8611f3179', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:34:01 GMT', 'model': 'd75d631b-98ec-45a1-bef8-2e7f959cfff1', 'model_id': 'd75d631b-98ec-45a1-bef8-2e7f959cfff1', 'name': None, 'parent_model': '0ce1a286-8842-4c45-a421-1695debf83bf', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:27:38 GMT', 'model': 'd5c71e06-9a4e-48b4-a9dd-dca87097780f', 'model_id': 'd5c71e06-9a4e-48b4-a9dd-dca87097780f', 'name': None, 'parent_model': 'caa820c7-8787-480a-b0f7-81fbe6c6d9b7', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:29:49 GMT', 'model': 'd5095189-5d45-48f1-b5b0-c887c9422759', 'model_id': 'd5095189-5d45-48f1-b5b0-c887c9422759', 'name': None, 'parent_model': 'cc5abf0f-9c3a-48f6-a74a-463a99f6249a', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:07:53 GMT', 'model': 'd501414c-d868-44d2-a949-592d9eabd909', 'model_id': 'd501414c-d868-44d2-a949-592d9eabd909', 'name': None, 'parent_model': '5b94d073-6dc8-4af5-a7ac-9f122d9db6e6', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:34:54 GMT', 'model': 'd4e60743-f50a-4cc9-9a29-dfdbd242805c', 'model_id': 'd4e60743-f50a-4cc9-9a29-dfdbd242805c', 'name': None, 'parent_model': '84e5445e-d023-41a9-b1a0-b08c050cfdb4', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:23:37 GMT', 'model': 'd4d705f7-63da-4155-93bf-42ab84bd2db9', 'model_id': 'd4d705f7-63da-4155-93bf-42ab84bd2db9', 'name': None, 'parent_model': 'be8891ea-41ae-4f42-ab6b-b224b2dbaa91', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:03:20 GMT', 'model': 'd47d06d0-b827-4f73-8a4d-6509cee4dfff', 'model_id': 'd47d06d0-b827-4f73-8a4d-6509cee4dfff', 'name': None, 'parent_model': '15e0cd65-af74-4fa7-a478-08d46a30ed40', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:33:02 GMT', 'model': 'd45582d3-681a-40c8-ac15-e2db3f3c1d0e', 'model_id': 'd45582d3-681a-40c8-ac15-e2db3f3c1d0e', 'name': None, 'parent_model': 'c1dcd79c-e72e-4579-ae1d-7c1aaa5ac4a6', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:52:02 GMT', 'model': 'd414ecb5-a304-46be-8d8e-a59587d23160', 'model_id': 'd414ecb5-a304-46be-8d8e-a59587d23160', 'name': None, 'parent_model': '5bb2c3ec-3dbe-41a1-94b2-c65a5160acb0', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:17:46 GMT', 'model': 'd3fe9f7e-39f9-4ab3-ad26-4af509dc0b8e', 'model_id': 'd3fe9f7e-39f9-4ab3-ad26-4af509dc0b8e', 'name': None, 'parent_model': 'e187f02b-091f-41e8-8403-e07a1b879d33', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:55:44 GMT', 'model': 'd2628817-4a8b-4cb1-9044-51a3106aa792', 'model_id': 'd2628817-4a8b-4cb1-9044-51a3106aa792', 'name': None, 'parent_model': 'f74785bd-ab87-4aaa-836f-6e93fc28245f', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:01:56 GMT', 'model': 'd13a4d8d-d87d-49da-b03d-a08c975b58f4', 'model_id': 'd13a4d8d-d87d-49da-b03d-a08c975b58f4', 'name': None, 'parent_model': '5e46ecd3-ef5c-4f1e-ab06-4a484eae6edb', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:21:33 GMT', 'model': 'd12efb27-2c5e-49b3-b4f5-c2fb98581660', 'model_id': 'd12efb27-2c5e-49b3-b4f5-c2fb98581660', 'name': None, 'parent_model': 'cb237bd2-66ec-42f9-b934-f50b87ff89f4', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:31:25 GMT', 'model': 'd0b3fbd1-f648-4aac-a17a-4e24437c35bb', 'model_id': 'd0b3fbd1-f648-4aac-a17a-4e24437c35bb', 'name': None, 'parent_model': '9deceb63-de19-4602-9593-eeed272cfe15', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:38:58 GMT', 'model': 'd05c2640-2360-4601-9503-a846444306b5', 'model_id': 'd05c2640-2360-4601-9503-a846444306b5', 'name': None, 'parent_model': 'e8b75195-f7ef-4ae2-bdae-e93591d48030', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:35:38 GMT', 'model': 'd03fbd31-58ec-4e9f-9794-c8d89f812aa4', 'model_id': 'd03fbd31-58ec-4e9f-9794-c8d89f812aa4', 'name': None, 'parent_model': '749729a6-77f2-4d6a-8d47-bc9f21f09651', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:07:53 GMT', 'model': 'd024c6a5-1587-4821-ba51-af19f529301f', 'model_id': 'd024c6a5-1587-4821-ba51-af19f529301f', 'name': None, 'parent_model': '093091a5-8918-4b73-9a4f-7d3bcb48a418', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:20:44 GMT', 'model': 'cff77a36-05e1-4654-b64d-73337d1179ef', 'model_id': 'cff77a36-05e1-4654-b64d-73337d1179ef', 'name': None, 'parent_model': '050e8b49-957a-43b9-8116-2914fb3556f9', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:26:56 GMT', 'model': 'cf707d6a-13b3-4677-b8eb-71aceb9ab3a3', 'model_id': 'cf707d6a-13b3-4677-b8eb-71aceb9ab3a3', 'name': None, 'parent_model': '3bd0fc40-a93f-4fac-84ac-bc24287c0419', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:36:23 GMT', 'model': 'cf0cc961-7aec-41bc-aa11-f258c62d6db9', 'model_id': 'cf0cc961-7aec-41bc-aa11-f258c62d6db9', 'name': None, 'parent_model': 'e3e8887d-38be-4320-b6b8-c883d45b1cd6', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:33:42 GMT', 'model': 'ce8e3a95-f6d5-4fae-9e94-8ef6ca003e19', 'model_id': 'ce8e3a95-f6d5-4fae-9e94-8ef6ca003e19', 'name': None, 'parent_model': '3cf0d5aa-8f1f-489c-ad9a-abdfa963bc69', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:00:07 GMT', 'model': 'cdd359ae-ed92-44e7-a93a-4b2aac5165bc', 'model_id': 'cdd359ae-ed92-44e7-a93a-4b2aac5165bc', 'name': None, 'parent_model': 'f464429d-b8c4-4186-8886-ef99a9c14b0c', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:00:14 GMT', 'model': 'ccb51938-06c1-405d-ac00-6c08a34a8845', 'model_id': 'ccb51938-06c1-405d-ac00-6c08a34a8845', 'name': None, 'parent_model': '704395e9-7482-4026-bf2b-01efe9c440aa', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:07:11 GMT', 'model': 'cca1105a-e979-4aae-b1de-abf0d6ce45c0', 'model_id': 'cca1105a-e979-4aae-b1de-abf0d6ce45c0', 'name': None, 'parent_model': 'fd85fd68-8fae-4a90-a27c-aa23ab913c84', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:29:25 GMT', 'model': 'cc5abf0f-9c3a-48f6-a74a-463a99f6249a', 'model_id': 'cc5abf0f-9c3a-48f6-a74a-463a99f6249a', 'name': None, 'parent_model': '0976bfed-bd0d-4833-a60f-d594fb59cd42', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:20:44 GMT', 'model': 'cc200b7d-cf83-4167-9a27-e46b11b2948c', 'model_id': 'cc200b7d-cf83-4167-9a27-e46b11b2948c', 'name': None, 'parent_model': 'ab82c74d-e8f2-45ad-8a0b-ca6e3388ce4a', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:12:47 GMT', 'model': 'cbe0ac6e-2058-4a8f-be1d-61da590f5a1b', 'model_id': 'cbe0ac6e-2058-4a8f-be1d-61da590f5a1b', 'name': None, 'parent_model': '940624ec-412f-4a10-acd3-d38a6aebed2b', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:58:28 GMT', 'model': 'cb8cf515-15be-4457-a15e-a1b04fde924e', 'model_id': 'cb8cf515-15be-4457-a15e-a1b04fde924e', 'name': None, 'parent_model': '40e4974e-cf5a-4430-a946-06229865b9fe', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:21:09 GMT', 'model': 'cb237bd2-66ec-42f9-b934-f50b87ff89f4', 'model_id': 'cb237bd2-66ec-42f9-b934-f50b87ff89f4', 'name': None, 'parent_model': 'cc200b7d-cf83-4167-9a27-e46b11b2948c', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:27:06 GMT', 'model': 'cb142ecb-905d-4d5f-b708-e29bd9e37341', 'model_id': 'cb142ecb-905d-4d5f-b708-e29bd9e37341', 'name': None, 'parent_model': '5d46651a-0648-47b6-a8c6-03e6c4a99a2e', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:47:58 GMT', 'model': 'cb106dbc-2406-43eb-9c50-3e175f1ebb0f', 'model_id': 'cb106dbc-2406-43eb-9c50-3e175f1ebb0f', 'name': None, 'parent_model': '191238d9-0087-4528-b38b-7dc1e66c46a8', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:38:55 GMT', 'model': 'cafc3f96-a7ff-4f32-b0a0-b8b201b112d9', 'model_id': 'cafc3f96-a7ff-4f32-b0a0-b8b201b112d9', 'name': None, 'parent_model': 'b2f62c2e-b5a9-45ce-9ea5-14976248fefd', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:27:14 GMT', 'model': 'caa820c7-8787-480a-b0f7-81fbe6c6d9b7', 'model_id': 'caa820c7-8787-480a-b0f7-81fbe6c6d9b7', 'name': None, 'parent_model': '5069832c-361c-4076-8179-97b0d990441f', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:12:26 GMT', 'model': 'ca494a0b-e774-4d57-a79c-ce6e3cb7a04b', 'model_id': 'ca494a0b-e774-4d57-a79c-ce6e3cb7a04b', 'name': None, 'parent_model': '2e2889d9-8853-4f33-853a-a17ea992423e', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:02:58 GMT', 'model': 'c949b10a-2baa-4d20-afc6-a99a3b5af1f3', 'model_id': 'c949b10a-2baa-4d20-afc6-a99a3b5af1f3', 'name': None, 'parent_model': '988bb218-028c-4f2e-b31f-881168dca63a', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:26:01 GMT', 'model': 'c92a477f-f09b-425d-8a7d-5946b32166b1', 'model_id': 'c92a477f-f09b-425d-8a7d-5946b32166b1', 'name': None, 'parent_model': '875f5a50-7653-48d5-9a43-8615f6ae7f47', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:04:13 GMT', 'model': 'c91a1859-6640-4f33-9856-13a421b77774', 'model_id': 'c91a1859-6640-4f33-9856-13a421b77774', 'name': None, 'parent_model': 'f4937451-6a01-453b-918d-f5c32c638d3d', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:56:46 GMT', 'model': 'c89524a2-4e1e-4938-9d6f-18548adec203', 'model_id': 'c89524a2-4e1e-4938-9d6f-18548adec203', 'name': None, 'parent_model': '35e05901-4a82-473d-8bef-834d691d016e', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:42:12 GMT', 'model': 'c80317f3-fc8c-49c7-a76c-a6390fcd1e5c', 'model_id': 'c80317f3-fc8c-49c7-a76c-a6390fcd1e5c', 'name': None, 'parent_model': 'f1ab8576-570c-4f9a-a05a-ce4936330cc1', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:52:29 GMT', 'model': 'c79ec909-1e84-4f31-8125-ed9d78e2b930', 'model_id': 'c79ec909-1e84-4f31-8125-ed9d78e2b930', 'name': None, 'parent_model': 'e476a094-0fbb-4b0c-8cef-a32f3ae07135', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:45:53 GMT', 'model': 'c719be14-1696-4b47-9966-15cff5b3ac80', 'model_id': 'c719be14-1696-4b47-9966-15cff5b3ac80', 'name': None, 'parent_model': '2d9e5723-ff17-4b71-9f60-736eabb0e31b', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:54:32 GMT', 'model': 'c661d4bc-7785-4293-bfa4-99f03920b6cd', 'model_id': 'c661d4bc-7785-4293-bfa4-99f03920b6cd', 'name': None, 'parent_model': '9e5c722c-191d-4816-8458-b7df80e64a8c', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:46:41 GMT', 'model': 'c65a3db1-8441-43b8-9750-d78924989746', 'model_id': 'c65a3db1-8441-43b8-9750-d78924989746', 'name': None, 'parent_model': '2a347eb8-7855-4c07-be51-6f395a29bde4', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:20:33 GMT', 'model': 'c4d15b8b-6b82-4def-a91b-0acc07d42a96', 'model_id': 'c4d15b8b-6b82-4def-a91b-0acc07d42a96', 'name': None, 'parent_model': '10106182-31be-4f56-be11-19fe8188622e', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:20:08 GMT', 'model': 'c497c0ea-6cbf-48d5-8ac4-22a784aaa59c', 'model_id': 'c497c0ea-6cbf-48d5-8ac4-22a784aaa59c', 'name': None, 'parent_model': '38cacea3-7e16-4355-ae27-625720dfa34b', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 21:14:58 GMT', 'model': 'c404f913-0fb4-4ebd-ad68-f797fe49b7eb', 'model_id': 'c404f913-0fb4-4ebd-ad68-f797fe49b7eb', 'name': None, 'parent_model': '3718c3b8-c62e-4939-b18f-c571d738623e', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:23:59 GMT', 'model': 'c3363d47-936f-44e7-9a05-6b345bb179da', 'model_id': 'c3363d47-936f-44e7-9a05-6b345bb179da', 'name': None, 'parent_model': 'e6907a1b-358d-4c05-8d8e-570a55d54fb1', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:24:25 GMT', 'model': 'c329368e-4124-4e4b-9749-2deec9b522a7', 'model_id': 'c329368e-4124-4e4b-9749-2deec9b522a7', 'name': None, 'parent_model': 'a8e862a0-e7b1-4383-a29a-98ac5fa6f952', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:24:01 GMT', 'model': 'c30f3b50-112f-4b94-8f0c-ded494d3e6ed', 'model_id': 'c30f3b50-112f-4b94-8f0c-ded494d3e6ed', 'name': None, 'parent_model': 'd4d705f7-63da-4155-93bf-42ab84bd2db9', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:49:26 GMT', 'model': 'c2f217d4-7133-46e6-aabe-fac076e0bd80', 'model_id': 'c2f217d4-7133-46e6-aabe-fac076e0bd80', 'name': None, 'parent_model': '6118f91d-098f-4e55-ae65-58c9e36317c0', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:32:36 GMT', 'model': 'c1dcd79c-e72e-4579-ae1d-7c1aaa5ac4a6', 'model_id': 'c1dcd79c-e72e-4579-ae1d-7c1aaa5ac4a6', 'name': None, 'parent_model': '0aab2669-a442-40d2-abbc-deaa31cd98a8', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:13:17 GMT', 'model': 'c1964f4d-c1c2-431e-9f41-f36c79fb8950', 'model_id': 'c1964f4d-c1c2-431e-9f41-f36c79fb8950', 'name': None, 'parent_model': '906ee073-ae00-44aa-a9f4-0e2ecadfb18e', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:11:37 GMT', 'model': 'c15f13e0-7295-4a6f-86dd-ab1a74416ecc', 'model_id': 'c15f13e0-7295-4a6f-86dd-ab1a74416ecc', 'name': None, 'parent_model': '997981d2-7e9e-46fb-a3dd-1ab7fa80ce96', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:10:44 GMT', 'model': 'c1522204-4a16-4bc1-a7c0-dc44f188ab6a', 'model_id': 'c1522204-4a16-4bc1-a7c0-dc44f188ab6a', 'name': None, 'parent_model': '2c27cbbd-d1d3-4ae7-969e-4b031a235a5a', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:40:57 GMT', 'model': 'bfe56534-d58e-46a2-8857-0417159420b0', 'model_id': 'bfe56534-d58e-46a2-8857-0417159420b0', 'name': None, 'parent_model': '05a2d395-3411-4da3-8b32-9fd14b25886e', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:39:47 GMT', 'model': 'bf76514a-5f48-47f7-85be-b0fe6d1de8ec', 'model_id': 'bf76514a-5f48-47f7-85be-b0fe6d1de8ec', 'name': None, 'parent_model': 'bbb407dd-3075-462b-b866-ff85ba76ad37', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:59:17 GMT', 'model': 'bf16d524-25d1-4835-ac7f-69c8811cfd28', 'model_id': 'bf16d524-25d1-4835-ac7f-69c8811cfd28', 'name': None, 'parent_model': '0b086464-c3f2-4981-9dec-430544a08409', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:38:02 GMT', 'model': 'be8cf200-4aa0-4f63-9ce6-6ba377f04b80', 'model_id': 'be8cf200-4aa0-4f63-9ce6-6ba377f04b80', 'name': None, 'parent_model': '82589fd5-52ea-4d13-a84d-216c5637fc00', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:23:12 GMT', 'model': 'be8891ea-41ae-4f42-ab6b-b224b2dbaa91', 'model_id': 'be8891ea-41ae-4f42-ab6b-b224b2dbaa91', 'name': None, 'parent_model': '187d786b-1a8f-4473-9321-dbcb0a44d892', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:21:59 GMT', 'model': 'be6e1d85-1738-4281-9997-35e97e6ca7cb', 'model_id': 'be6e1d85-1738-4281-9997-35e97e6ca7cb', 'name': None, 'parent_model': '374b5775-28f5-40a7-9b51-b6677158d0aa', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:04:37 GMT', 'model': 'bd67f2c7-a640-4194-8820-f3257327ca48', 'model_id': 'bd67f2c7-a640-4194-8820-f3257327ca48', 'name': None, 'parent_model': 'c91a1859-6640-4f33-9856-13a421b77774', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:51:56 GMT', 'model': 'bd04d093-bfb8-4589-9a3e-e784e439523b', 'model_id': 'bd04d093-bfb8-4589-9a3e-e784e439523b', 'name': None, 'parent_model': '6c8b6c84-884f-40d8-ac50-3f16039f1e9e', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:31:49 GMT', 'model': 'bc1d2ac9-6b62-46d8-a8e2-ab99b440ee5d', 'model_id': 'bc1d2ac9-6b62-46d8-a8e2-ab99b440ee5d', 'name': None, 'parent_model': 'd0b3fbd1-f648-4aac-a17a-4e24437c35bb', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:39:22 GMT', 'model': 'bbb407dd-3075-462b-b866-ff85ba76ad37', 'model_id': 'bbb407dd-3075-462b-b866-ff85ba76ad37', 'name': None, 'parent_model': 'd05c2640-2360-4601-9503-a846444306b5', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:42:37 GMT', 'model': 'bafbd518-5189-4dc5-bdf0-6c4d4ff39d95', 'model_id': 'bafbd518-5189-4dc5-bdf0-6c4d4ff39d95', 'name': None, 'parent_model': 'd909f93e-495a-4d87-a088-db8eeab6e995', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:41:47 GMT', 'model': 'bac98b07-6b49-4f4f-9725-a59edf6dc9b2', 'model_id': 'bac98b07-6b49-4f4f-9725-a59edf6dc9b2', 'name': None, 'parent_model': '0dc635c2-21cb-4486-8753-05bb6c0b2fef', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 21:09:38 GMT', 'model': 'ba765d36-1375-4666-839e-f6403e192821', 'model_id': 'ba765d36-1375-4666-839e-f6403e192821', 'name': None, 'parent_model': '318e2ea5-30ed-4299-89e8-653aa0cecac0', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:40:09 GMT', 'model': 'ba361877-a75e-4319-af75-dd983bc1d8fa', 'model_id': 'ba361877-a75e-4319-af75-dd983bc1d8fa', 'name': None, 'parent_model': '1d60094f-631c-4a7f-8145-e4386398bff8', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:00:51 GMT', 'model': 'ba33aadc-54f7-4c3c-a2d6-3541403dda41', 'model_id': 'ba33aadc-54f7-4c3c-a2d6-3541403dda41', 'name': None, 'parent_model': '9da045b7-0124-4453-9693-3e9cfcff4081', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:19:51 GMT', 'model': 'b993160d-d164-4dcc-a28c-2debc506515f', 'model_id': 'b993160d-d164-4dcc-a28c-2debc506515f', 'name': None, 'parent_model': '3bd2b407-297c-40d4-bace-69a2ab74d8ee', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:56:58 GMT', 'model': 'b8d6b599-f30c-45e7-ac15-e383a95b565e', 'model_id': 'b8d6b599-f30c-45e7-ac15-e383a95b565e', 'name': None, 'parent_model': '34a8396c-7895-4702-801c-e01a366cbdf1', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:04:34 GMT', 'model': 'b865f83b-6c51-4da1-a8ee-21d4b9cd4683', 'model_id': 'b865f83b-6c51-4da1-a8ee-21d4b9cd4683', 'name': None, 'parent_model': '01043492-0171-46e3-afab-4d33a76c152e', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:29:47 GMT', 'model': 'b66247ea-304f-4059-b594-ec7f638972af', 'model_id': 'b66247ea-304f-4059-b594-ec7f638972af', 'name': None, 'parent_model': '9f5b13e7-adb9-4dce-8b8c-243e2b5e09de', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:33:12 GMT', 'model': 'b653f40b-8346-40e2-bf0f-52e52b00711c', 'model_id': 'b653f40b-8346-40e2-bf0f-52e52b00711c', 'name': None, 'parent_model': 'a7ef9152-70d3-4df8-bae0-7c3cc2bdf5e5', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:38:27 GMT', 'model': 'b519702f-72d1-429f-bdd2-d33e8b44dbe1', 'model_id': 'b519702f-72d1-429f-bdd2-d33e8b44dbe1', 'name': None, 'parent_model': '556c0bb9-b538-4af4-b41e-05cf00cb910d', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:19:56 GMT', 'model': 'b42115c1-c9d7-446a-bfea-dc17623dcb90', 'model_id': 'b42115c1-c9d7-446a-bfea-dc17623dcb90', 'name': None, 'parent_model': 'aaeecdd5-3bfb-4e19-9167-a4a5f9bffa10', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:50:48 GMT', 'model': 'b41cc615-7aa3-4a06-8bed-378bf071f194', 'model_id': 'b41cc615-7aa3-4a06-8bed-378bf071f194', 'name': None, 'parent_model': '15954bcf-e018-4d51-ad59-4779c39addf4', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:48:12 GMT', 'model': 'b31ff2a6-88af-4667-ba73-5c53213edba9', 'model_id': 'b31ff2a6-88af-4667-ba73-5c53213edba9', 'name': None, 'parent_model': '41b07e79-dbbf-4abf-9d83-8fcde4affd2e', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:38:30 GMT', 'model': 'b2f62c2e-b5a9-45ce-9ea5-14976248fefd', 'model_id': 'b2f62c2e-b5a9-45ce-9ea5-14976248fefd', 'name': None, 'parent_model': '97aba36f-3c07-4ceb-af7c-f5e893959e38', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:06:14 GMT', 'model': 'b17e6767-a701-4fc6-b9d8-19bff83d7025', 'model_id': 'b17e6767-a701-4fc6-b9d8-19bff83d7025', 'name': None, 'parent_model': 'e6b7a0d8-4a35-46ba-a168-d07481768d42', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:00:38 GMT', 'model': 'b12871ec-b2a2-4207-8150-790d7b28e71a', 'model_id': 'b12871ec-b2a2-4207-8150-790d7b28e71a', 'name': None, 'parent_model': 'ccb51938-06c1-405d-ac00-6c08a34a8845', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:48:35 GMT', 'model': 'b03e0721-b683-4c58-be3d-e79aa3c48ea4', 'model_id': 'b03e0721-b683-4c58-be3d-e79aa3c48ea4', 'name': None, 'parent_model': 'b31ff2a6-88af-4667-ba73-5c53213edba9', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 21:11:42 GMT', 'model': 'afcbb07c-efad-49b7-8e24-92793659b92e', 'model_id': 'afcbb07c-efad-49b7-8e24-92793659b92e', 'name': None, 'parent_model': 'd8c8a6da-06f3-408c-8ece-567ac258c8b5', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:21:48 GMT', 'model': 'afacb56f-f001-4967-9b5e-19fe0b576e79', 'model_id': 'afacb56f-f001-4967-9b5e-19fe0b576e79', 'name': None, 'parent_model': '55a19400-37dd-4370-ab10-7d724d8700bd', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:01:02 GMT', 'model': 'af38ca86-443e-4b90-9720-19dff28f6cee', 'model_id': 'af38ca86-443e-4b90-9720-19dff28f6cee', 'name': None, 'parent_model': 'b12871ec-b2a2-4207-8150-790d7b28e71a', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:30:06 GMT', 'model': 'adec5bda-29db-4018-ba44-ec8321dbe595', 'model_id': 'adec5bda-29db-4018-ba44-ec8321dbe595', 'name': None, 'parent_model': '469c5be5-c900-4e1e-a5ae-d1c87f4d864f', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:39:21 GMT', 'model': 'ad3797b9-6a62-4fba-bed6-34e4f03bf2d8', 'model_id': 'ad3797b9-6a62-4fba-bed6-34e4f03bf2d8', 'name': None, 'parent_model': '12661475-074b-499a-b25f-c3a074aece58', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:36:32 GMT', 'model': 'acff391a-fa15-4dc1-9d78-7666117b5f0d', 'model_id': 'acff391a-fa15-4dc1-9d78-7666117b5f0d', 'name': None, 'parent_model': '21c5de81-ff85-46da-ab85-5ed2f1264db4', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:04:59 GMT', 'model': 'acfe1d3d-de19-4c88-abb2-442841add432', 'model_id': 'acfe1d3d-de19-4c88-abb2-442841add432', 'name': None, 'parent_model': 'b865f83b-6c51-4da1-a8ee-21d4b9cd4683', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:43:50 GMT', 'model': 'aceb722a-2fdf-4982-9ae3-9843027b79de', 'model_id': 'aceb722a-2fdf-4982-9ae3-9843027b79de', 'name': None, 'parent_model': '6bc5a56d-b68f-4fdc-8aa2-bf7e29aace96', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:45:05 GMT', 'model': 'ace3aaff-fe20-42f0-9b33-07d4e95da9c6', 'model_id': 'ace3aaff-fe20-42f0-9b33-07d4e95da9c6', 'name': None, 'parent_model': '85e44978-ec1d-4e8a-bc58-37b0dd364f44', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:18:31 GMT', 'model': 'acbe88e3-e16b-483f-b2ae-dfa3092faaa6', 'model_id': 'acbe88e3-e16b-483f-b2ae-dfa3092faaa6', 'name': None, 'parent_model': '8935d372-7f24-4eb8-9fff-ad2492379d40', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:20:17 GMT', 'model': 'ab82c74d-e8f2-45ad-8a0b-ca6e3388ce4a', 'model_id': 'ab82c74d-e8f2-45ad-8a0b-ca6e3388ce4a', 'name': None, 'parent_model': 'b993160d-d164-4dcc-a28c-2debc506515f', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:19:31 GMT', 'model': 'aaeecdd5-3bfb-4e19-9167-a4a5f9bffa10', 'model_id': 'aaeecdd5-3bfb-4e19-9167-a4a5f9bffa10', 'name': None, 'parent_model': 'f3b456cd-b8f2-4f05-ab26-b153095c765a', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:07:02 GMT', 'model': 'aaeafbfd-fb4f-4ad3-b003-90c96c180419', 'model_id': 'aaeafbfd-fb4f-4ad3-b003-90c96c180419', 'name': None, 'parent_model': '990d8d8f-fae0-48f8-8ff2-a67589a5c439', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:21:07 GMT', 'model': 'aacb592f-3d84-417f-bfe7-d832ecf31899', 'model_id': 'aacb592f-3d84-417f-bfe7-d832ecf31899', 'name': None, 'parent_model': 'cff77a36-05e1-4654-b64d-73337d1179ef', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:49:04 GMT', 'model': 'aab098f1-bda5-49e3-8787-5ef259aabf14', 'model_id': 'aab098f1-bda5-49e3-8787-5ef259aabf14', 'name': None, 'parent_model': '90e834e8-9b17-4c4b-b83b-95824f0a8d8c', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:13:41 GMT', 'model': 'aa5b654d-9655-4f5c-92c2-973b04f6499f', 'model_id': 'aa5b654d-9655-4f5c-92c2-973b04f6499f', 'name': None, 'parent_model': 'c1964f4d-c1c2-431e-9f41-f36c79fb8950', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:19:20 GMT', 'model': 'aa43e35c-a5e1-446e-9971-119f716e5097', 'model_id': 'aa43e35c-a5e1-446e-9971-119f716e5097', 'name': None, 'parent_model': '10542aff-b66b-492c-9581-0cef4a8ac376', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:01:40 GMT', 'model': 'a989e55e-18d1-4226-8010-ccee096be054', 'model_id': 'a989e55e-18d1-4226-8010-ccee096be054', 'name': None, 'parent_model': '14ded340-ddc6-49ae-82fc-b93095483059', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:16:01 GMT', 'model': 'a917860b-bcbe-4108-b888-5b9ac268149e', 'model_id': 'a917860b-bcbe-4108-b888-5b9ac268149e', 'name': None, 'parent_model': 'df25ec02-f8ac-42a7-a14c-7cfa86ef3fc3', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:24:00 GMT', 'model': 'a8e862a0-e7b1-4383-a29a-98ac5fa6f952', 'model_id': 'a8e862a0-e7b1-4383-a29a-98ac5fa6f952', 'name': None, 'parent_model': '04b76483-1eb1-4c4d-b275-2bbb56e94ae8', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:26:04 GMT', 'model': 'a825f62e-16d2-4e3a-aacc-b9278aebb858', 'model_id': 'a825f62e-16d2-4e3a-aacc-b9278aebb858', 'name': None, 'parent_model': '58d5f64b-12f2-42f3-af00-b1653c1a9b2e', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:32:48 GMT', 'model': 'a7ef9152-70d3-4df8-bae0-7c3cc2bdf5e5', 'model_id': 'a7ef9152-70d3-4df8-bae0-7c3cc2bdf5e5', 'name': None, 'parent_model': '80f4b305-b987-490c-836c-f72e8ee28c08', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:08:02 GMT', 'model': 'a7d74f0d-1864-4f19-a26c-034ac06fee24', 'model_id': 'a7d74f0d-1864-4f19-a26c-034ac06fee24', 'name': None, 'parent_model': '5685ed0f-b2af-4b0e-81a3-a4240942576a', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:51:12 GMT', 'model': 'a79209fe-1675-4772-b1bf-52f656227d3c', 'model_id': 'a79209fe-1675-4772-b1bf-52f656227d3c', 'name': None, 'parent_model': 'b41cc615-7aa3-4a06-8bed-378bf071f194', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:42:24 GMT', 'model': 'a7029d70-c444-42d3-90e2-709f67cd7dd3', 'model_id': 'a7029d70-c444-42d3-90e2-709f67cd7dd3', 'name': None, 'parent_model': 'a48596e8-84dd-487b-a087-4fa844596331', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:26:18 GMT', 'model': 'a6e7102a-a581-40eb-b963-b1b1d63fdc9d', 'model_id': 'a6e7102a-a581-40eb-b963-b1b1d63fdc9d', 'name': None, 'parent_model': '17bc8207-1e77-44ad-89e8-70fecd2e924b', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:40:14 GMT', 'model': 'a6930266-f3ab-45c9-acd3-36b9321adbd1', 'model_id': 'a6930266-f3ab-45c9-acd3-36b9321adbd1', 'name': None, 'parent_model': 'd934e8b2-4906-4300-b345-11e810c20391', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:50:17 GMT', 'model': 'a639ab0a-eec1-48fc-85b2-03d1d06e0e9f', 'model_id': 'a639ab0a-eec1-48fc-85b2-03d1d06e0e9f', 'name': None, 'parent_model': '0b85f303-511b-4d8b-9372-274553607ef8', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:05:35 GMT', 'model': 'a4cb2d36-ba87-4d77-bf05-0c3ab580e2c4', 'model_id': 'a4cb2d36-ba87-4d77-bf05-0c3ab580e2c4', 'name': None, 'parent_model': '5b9d10d0-6408-4dd9-8e0e-5d0bc3cad593', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:41:58 GMT', 'model': 'a48596e8-84dd-487b-a087-4fa844596331', 'model_id': 'a48596e8-84dd-487b-a087-4fa844596331', 'name': None, 'parent_model': '5ab95eea-381b-4b4c-9965-d8d1f78f6862', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:31:48 GMT', 'model': 'a41506c9-49c7-4173-a6e4-8412d0dbe6bb', 'model_id': 'a41506c9-49c7-4173-a6e4-8412d0dbe6bb', 'name': None, 'parent_model': '2db3e2d3-08ca-4a8f-9b73-26e3e06c9d37', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 21:12:54 GMT', 'model': 'a413f162-ce2c-4b42-b635-3bc38a40842a', 'model_id': 'a413f162-ce2c-4b42-b635-3bc38a40842a', 'name': None, 'parent_model': '533cfc99-1a62-439e-a613-93c0063ababd', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:55:21 GMT', 'model': 'a29b6232-55a3-492d-8a7c-91de7bc167da', 'model_id': 'a29b6232-55a3-492d-8a7c-91de7bc167da', 'name': None, 'parent_model': '53bcaebb-5bc3-4ed7-8dda-67782983f022', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:22:38 GMT', 'model': 'a29abcb6-4316-4cd2-96ca-53ebaa4030bf', 'model_id': 'a29abcb6-4316-4cd2-96ca-53ebaa4030bf', 'name': None, 'parent_model': '3c4cbd82-168e-4d38-af73-f6f9f93e85d4', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:18:41 GMT', 'model': 'a283e30d-8daa-41d3-9c28-2d6db5383fcf', 'model_id': 'a283e30d-8daa-41d3-9c28-2d6db5383fcf', 'name': None, 'parent_model': 'fa52fcd6-3799-422e-8281-46b1547040ed', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:18:53 GMT', 'model': 'a27babc5-907d-4a6d-9a08-555c1389a39a', 'model_id': 'a27babc5-907d-4a6d-9a08-555c1389a39a', 'name': None, 'parent_model': '6b2c86e8-176b-417d-9c5e-88baafff4298', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:43:27 GMT', 'model': 'a1fed732-be5c-4916-815f-687183079e24', 'model_id': 'a1fed732-be5c-4916-815f-687183079e24', 'name': None, 'parent_model': '6cf95fe6-0f72-4b42-9dc5-be038e939ae5', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:25:06 GMT', 'model': 'a1fd8545-98af-4696-850d-777c866b3ffd', 'model_id': 'a1fd8545-98af-4696-850d-777c866b3ffd', 'name': None, 'parent_model': '8a9601b6-2a4f-4c25-a0cd-a72b6f191871', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:29:23 GMT', 'model': '9f5b13e7-adb9-4dce-8b8c-243e2b5e09de', 'model_id': '9f5b13e7-adb9-4dce-8b8c-243e2b5e09de', 'name': None, 'parent_model': '04c21707-bb5b-4ad1-aaf3-117ccdb6c422', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:38:52 GMT', 'model': '9f2b2440-0916-4e56-879a-e21f107cbbee', 'model_id': '9f2b2440-0916-4e56-879a-e21f107cbbee', 'name': None, 'parent_model': 'b519702f-72d1-429f-bdd2-d33e8b44dbe1', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:54:08 GMT', 'model': '9e5c722c-191d-4816-8458-b7df80e64a8c', 'model_id': '9e5c722c-191d-4816-8458-b7df80e64a8c', 'name': None, 'parent_model': '034dbac4-5c1c-4c08-ad0c-6f4b9b11bf4e', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:57:22 GMT', 'model': '9e584841-255f-4103-ab0b-a53964e054e7', 'model_id': '9e584841-255f-4103-ab0b-a53964e054e7', 'name': None, 'parent_model': '9dc9005c-d247-4b50-a23e-6ea3cf7b1ddc', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:20:45 GMT', 'model': '9e21e9d1-bd09-4de8-9de1-5b1763037424', 'model_id': '9e21e9d1-bd09-4de8-9de1-5b1763037424', 'name': None, 'parent_model': '7013ab7b-9f7a-45ed-a14e-0cfb10dd1bcb', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:13:59 GMT', 'model': '9e116ea6-8a55-45b4-80c2-ca86ff6bc0d5', 'model_id': '9e116ea6-8a55-45b4-80c2-ca86ff6bc0d5', 'name': None, 'parent_model': '888ed478-ce90-4cab-ae30-59e200cd0dd8', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:59:02 GMT', 'model': '9e0a3195-0a68-45f8-9075-306adec03b00', 'model_id': '9e0a3195-0a68-45f8-9075-306adec03b00', 'name': None, 'parent_model': '47950500-d05b-4f86-8fd0-8aea34fd92b4', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:09:57 GMT', 'model': '9dfdf095-5e33-4e9a-8c99-75771050d711', 'model_id': '9dfdf095-5e33-4e9a-8c99-75771050d711', 'name': None, 'parent_model': '1e3d3735-8709-4c19-9d69-0f4ebe4c90de', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:31:00 GMT', 'model': '9deceb63-de19-4602-9593-eeed272cfe15', 'model_id': '9deceb63-de19-4602-9593-eeed272cfe15', 'name': None, 'parent_model': '0d5f6c89-2fec-41f9-9314-4ee6801851ee', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:56:59 GMT', 'model': '9dc9005c-d247-4b50-a23e-6ea3cf7b1ddc', 'model_id': '9dc9005c-d247-4b50-a23e-6ea3cf7b1ddc', 'name': None, 'parent_model': 'fbfa18df-716a-482d-a264-fa8bdba5f63e', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:00:27 GMT', 'model': '9da045b7-0124-4453-9693-3e9cfcff4081', 'model_id': '9da045b7-0124-4453-9693-3e9cfcff4081', 'name': None, 'parent_model': '0494a168-30a4-4a5d-b819-96da1b313264', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:27:46 GMT', 'model': '9d5e2dfb-1491-41d7-9bdb-4e7fcfbe4bda', 'model_id': '9d5e2dfb-1491-41d7-9bdb-4e7fcfbe4bda', 'name': None, 'parent_model': '82c38af2-d39d-43fb-b79e-a4fe05ccea06', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:28:04 GMT', 'model': '9c5ece57-0fab-499a-a36c-4996a1f520f7', 'model_id': '9c5ece57-0fab-499a-a36c-4996a1f520f7', 'name': None, 'parent_model': 'd5c71e06-9a4e-48b4-a9dd-dca87097780f', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 21:19:44 GMT', 'model': '9c14d812-f4c5-406e-970f-9ee120e0cfd5', 'model_id': '9c14d812-f4c5-406e-970f-9ee120e0cfd5', 'name': None, 'parent_model': 'aa43e35c-a5e1-446e-9971-119f716e5097', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:32:13 GMT', 'model': '9bf02e67-7362-40fb-b546-66c192cf1058', 'model_id': '9bf02e67-7362-40fb-b546-66c192cf1058', 'name': None, 'parent_model': 'bc1d2ac9-6b62-46d8-a8e2-ab99b440ee5d', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:58:00 GMT', 'model': '9a8488af-b74d-440f-bdab-5cf48435fd04', 'model_id': '9a8488af-b74d-440f-bdab-5cf48435fd04', 'name': None, 'parent_model': 'db3525c9-f410-4979-8eef-a514c50f8334', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 21:12:06 GMT', 'model': '9a67813a-1067-4645-9b8f-4eac84fb0dc7', 'model_id': '9a67813a-1067-4645-9b8f-4eac84fb0dc7', 'name': None, 'parent_model': 'afcbb07c-efad-49b7-8e24-92793659b92e', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:51:32 GMT', 'model': '99ac7fd1-c7a7-43d5-9b4e-9a548ae6d35d', 'model_id': '99ac7fd1-c7a7-43d5-9b4e-9a548ae6d35d', 'name': None, 'parent_model': '287c950d-7061-427e-af1c-ab5096c6cc4d', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:11:12 GMT', 'model': '997981d2-7e9e-46fb-a3dd-1ab7fa80ce96', 'model_id': '997981d2-7e9e-46fb-a3dd-1ab7fa80ce96', 'name': None, 'parent_model': '6b3a5a2f-75c7-4e66-ab8a-f310f67a60a2', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:06:37 GMT', 'model': '990d8d8f-fae0-48f8-8ff2-a67589a5c439', 'model_id': '990d8d8f-fae0-48f8-8ff2-a67589a5c439', 'name': None, 'parent_model': 'b17e6767-a701-4fc6-b9d8-19bff83d7025', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:37:37 GMT', 'model': '9902acd5-2a4b-42c5-96e8-4cc4799a2f7b', 'model_id': '9902acd5-2a4b-42c5-96e8-4cc4799a2f7b', 'name': None, 'parent_model': '1a4bcda4-c0f8-4181-a6ad-5b92e1abf80e', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:35:07 GMT', 'model': '98f07390-0fd4-4a40-8606-2a4b5e5d2273', 'model_id': '98f07390-0fd4-4a40-8606-2a4b5e5d2273', 'name': None, 'parent_model': '26dd83fc-9eb6-4cbb-8886-1c5e6aac7395', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:02:34 GMT', 'model': '988bb218-028c-4f2e-b31f-881168dca63a', 'model_id': '988bb218-028c-4f2e-b31f-881168dca63a', 'name': None, 'parent_model': '16a344fb-9e3a-4df5-9aea-c37251181c1c', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:52:50 GMT', 'model': '98384026-3764-4437-a98d-e48d06774ec9', 'model_id': '98384026-3764-4437-a98d-e48d06774ec9', 'name': None, 'parent_model': '3ffbd35e-825a-4eaf-a5c7-c0357674ddfc', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:38:06 GMT', 'model': '97aba36f-3c07-4ceb-af7c-f5e893959e38', 'model_id': '97aba36f-3c07-4ceb-af7c-f5e893959e38', 'name': None, 'parent_model': 'e57d785e-3100-458f-a27e-c2623c403e7b', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:17:52 GMT', 'model': '97686b04-003b-4112-89df-bafae9d14093', 'model_id': '97686b04-003b-4112-89df-bafae9d14093', 'name': None, 'parent_model': '08ad21f5-7aeb-4a1a-9080-eea1a6a97f93', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:12:07 GMT', 'model': '97082d29-49e3-4560-bcfd-5f0d89b510f1', 'model_id': '97082d29-49e3-4560-bcfd-5f0d89b510f1', 'name': None, 'parent_model': '6c0a5fa3-bf70-4b73-89d4-34d001d3ee53', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:40:36 GMT', 'model': '96129f90-6ef0-4bd7-a9eb-25e121dc2d21', 'model_id': '96129f90-6ef0-4bd7-a9eb-25e121dc2d21', 'name': None, 'parent_model': '1995c198-63aa-4281-827a-e9c07652223a', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:08:18 GMT', 'model': '95c8c8cd-156c-4879-8642-b3bf21efe052', 'model_id': '95c8c8cd-156c-4879-8642-b3bf21efe052', 'name': None, 'parent_model': 'd501414c-d868-44d2-a949-592d9eabd909', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:45:56 GMT', 'model': '952386fc-4163-4b32-a2b6-fc2378e4d044', 'model_id': '952386fc-4163-4b32-a2b6-fc2378e4d044', 'name': None, 'parent_model': '00a5c1f6-c938-41f0-8cd4-b204e928a80f', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:03:35 GMT', 'model': '942f6bb8-1e4a-4dc4-a2e2-c15629abc0cc', 'model_id': '942f6bb8-1e4a-4dc4-a2e2-c15629abc0cc', 'name': None, 'parent_model': '50c8abb9-6fc7-4f3f-a04f-335a8393789a', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:07:05 GMT', 'model': '940695d2-6e72-42ee-a11e-1718b5ed9199', 'model_id': '940695d2-6e72-42ee-a11e-1718b5ed9199', 'name': None, 'parent_model': '38a7d49f-a699-426e-9f8e-8e0c6a421508', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:12:21 GMT', 'model': '940624ec-412f-4a10-acd3-d38a6aebed2b', 'model_id': '940624ec-412f-4a10-acd3-d38a6aebed2b', 'name': None, 'parent_model': '2a051ad9-155e-4c9c-9596-d0db28731ab7', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:21:59 GMT', 'model': '93df3d4e-18ac-49c4-b512-3d8805d695fe', 'model_id': '93df3d4e-18ac-49c4-b512-3d8805d695fe', 'name': None, 'parent_model': '8cffa60c-36b2-4857-b079-f6e8de502334', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:14:08 GMT', 'model': '93cc1df9-a616-49be-9cd5-abbcdcb8575c', 'model_id': '93cc1df9-a616-49be-9cd5-abbcdcb8575c', 'name': None, 'parent_model': 'aa5b654d-9655-4f5c-92c2-973b04f6499f', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:47:06 GMT', 'model': '93aa1685-48cf-4cb6-81b2-2b401ff266a4', 'model_id': '93aa1685-48cf-4cb6-81b2-2b401ff266a4', 'name': None, 'parent_model': 'c65a3db1-8441-43b8-9750-d78924989746', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:16:50 GMT', 'model': '92b2c175-156d-40a0-9253-8448bfbca308', 'model_id': '92b2c175-156d-40a0-9253-8448bfbca308', 'name': None, 'parent_model': '0ed80466-7642-48ab-bbc1-258178eed980', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:27:44 GMT', 'model': '91d59105-06d8-4167-8d0b-402fc6beae75', 'model_id': '91d59105-06d8-4167-8d0b-402fc6beae75', 'name': None, 'parent_model': 'ef4c64a7-41fb-4798-8662-852a99169968', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:50:41 GMT', 'model': '917cbea1-cc02-4cd7-9ab2-ae5aa8eb2744', 'model_id': '917cbea1-cc02-4cd7-9ab2-ae5aa8eb2744', 'name': None, 'parent_model': '62d8f671-6b1d-4307-a37e-4f4107820d23', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 21:02:21 GMT', 'model': '9155dd7d-41be-4515-9b3d-1b65b1bddb2b', 'model_id': '9155dd7d-41be-4515-9b3d-1b65b1bddb2b', 'name': None, 'parent_model': 'd13a4d8d-d87d-49da-b03d-a08c975b58f4', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:52:45 GMT', 'model': '914f8068-0df6-4def-8d5d-30d5b72e1449', 'model_id': '914f8068-0df6-4def-8d5d-30d5b72e1449', 'name': None, 'parent_model': '69705d84-4ec7-40c6-a555-c7e22f76f471', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:48:40 GMT', 'model': '90e834e8-9b17-4c4b-b83b-95824f0a8d8c', 'model_id': '90e834e8-9b17-4c4b-b83b-95824f0a8d8c', 'name': None, 'parent_model': 'fc026787-10ac-41e8-af8f-b08b2058628b', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:08:18 GMT', 'model': '9091c458-cd5f-4a3e-805f-384772f8d611', 'model_id': '9091c458-cd5f-4a3e-805f-384772f8d611', 'name': None, 'parent_model': 'd024c6a5-1587-4821-ba51-af19f529301f', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:18:04 GMT', 'model': '9070e73d-7dab-4e9e-8bf0-796162cb1ac9', 'model_id': '9070e73d-7dab-4e9e-8bf0-796162cb1ac9', 'name': None, 'parent_model': '82e19754-f92d-4ed9-a3a1-fa869126f717', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:12:53 GMT', 'model': '906ee073-ae00-44aa-a9f4-0e2ecadfb18e', 'model_id': '906ee073-ae00-44aa-a9f4-0e2ecadfb18e', 'name': None, 'parent_model': '6466d2b8-a03e-43de-8940-73f446360c6e', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:18:14 GMT', 'model': '903f2f4c-d080-4a9e-aa3a-daa7f83eb903', 'model_id': '903f2f4c-d080-4a9e-aa3a-daa7f83eb903', 'name': None, 'parent_model': '62d1bf65-53e5-4674-9b51-dc249eab11e5', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:48:48 GMT', 'model': '8fa9728d-fc1f-47f3-adad-b05d7f7c7435', 'model_id': '8fa9728d-fc1f-47f3-adad-b05d7f7c7435', 'name': None, 'parent_model': '388274fd-b918-40b9-9baf-4d63b87e86a2', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:25:10 GMT', 'model': '8f80d11e-524c-44fd-8c66-46b506280df5', 'model_id': '8f80d11e-524c-44fd-8c66-46b506280df5', 'name': None, 'parent_model': 'dc383596-46a7-468a-b44e-3995ee5d9ad6', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:58:29 GMT', 'model': '8f690c79-6213-4ae3-83a9-a6dba1d786f1', 'model_id': '8f690c79-6213-4ae3-83a9-a6dba1d786f1', 'name': None, 'parent_model': '9a8488af-b74d-440f-bdab-5cf48435fd04', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:47:21 GMT', 'model': '8ecc86cf-19ed-49f4-bd02-76354c8850d2', 'model_id': '8ecc86cf-19ed-49f4-bd02-76354c8850d2', 'name': None, 'parent_model': '02709899-6add-464b-9527-4d6c2a94cd23', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:26:07 GMT', 'model': '8eb5797d-ac23-413b-a932-6fed86749e88', 'model_id': '8eb5797d-ac23-413b-a932-6fed86749e88', 'name': None, 'parent_model': '0219db36-520c-488e-a6ea-a3f6f17a7ff3', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:29:08 GMT', 'model': '8e64041d-e896-4ae8-9836-393f2940099b', 'model_id': '8e64041d-e896-4ae8-9836-393f2940099b', 'name': None, 'parent_model': 'e5517996-7b1b-4785-be2c-269cfac390bf', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:44:43 GMT', 'model': '8d8a7bef-e6a5-4003-9f4e-44fd15d2a4d6', 'model_id': '8d8a7bef-e6a5-4003-9f4e-44fd15d2a4d6', 'name': None, 'parent_model': '263b2bfd-cf09-4397-a5b8-6b6d79b68cf8', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:21:34 GMT', 'model': '8cffa60c-36b2-4857-b079-f6e8de502334', 'model_id': '8cffa60c-36b2-4857-b079-f6e8de502334', 'name': None, 'parent_model': '4595b7f3-0d9b-4254-9759-94bd768438ad', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:06:21 GMT', 'model': '8cb36005-e8bd-4c76-885a-bfa834b1f1b3', 'model_id': '8cb36005-e8bd-4c76-885a-bfa834b1f1b3', 'name': None, 'parent_model': '0167d98c-2d49-4d15-9b86-e866b6c7b303', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:40:33 GMT', 'model': '8c6e3f38-04ef-44f1-b2cf-61c123b8fda2', 'model_id': '8c6e3f38-04ef-44f1-b2cf-61c123b8fda2', 'name': None, 'parent_model': '1b499746-1be3-4b64-b383-d6a81c60a2f3', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:03:07 GMT', 'model': '8c31c26c-80a3-48cb-9aa9-87ee58afe60b', 'model_id': '8c31c26c-80a3-48cb-9aa9-87ee58afe60b', 'name': None, 'parent_model': '4a45b724-3490-4a50-8be7-5df69e8e7345', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:15:47 GMT', 'model': '8b88d2a9-847f-4e52-b1a6-e62c63b91984', 'model_id': '8b88d2a9-847f-4e52-b1a6-e62c63b91984', 'name': None, 'parent_model': '1dc1e1a9-5573-4403-877f-928d09be7237', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:31:09 GMT', 'model': '8b002342-5460-4384-b194-9be724274744', 'model_id': '8b002342-5460-4384-b194-9be724274744', 'name': None, 'parent_model': '1946c0c4-11b8-41c2-bd32-6bb7da4cb681', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:42:12 GMT', 'model': '8aff477e-1f21-4544-aaf0-99bb2c6631da', 'model_id': '8aff477e-1f21-4544-aaf0-99bb2c6631da', 'name': None, 'parent_model': 'bac98b07-6b49-4f4f-9725-a59edf6dc9b2', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:24:41 GMT', 'model': '8a9601b6-2a4f-4c25-a0cd-a72b6f191871', 'model_id': '8a9601b6-2a4f-4c25-a0cd-a72b6f191871', 'name': None, 'parent_model': '4b3875f5-0625-4ae6-8714-08dfa8a6ad25', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:29:17 GMT', 'model': '8a83cc4c-7551-4261-ba55-e7b0201df89f', 'model_id': '8a83cc4c-7551-4261-ba55-e7b0201df89f', 'name': None, 'parent_model': '82e1a19f-7308-4364-89fe-b87cc7d85aa9', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:57:15 GMT', 'model': '8a06c8df-dc37-4777-b60f-802f0c7628cb', 'model_id': '8a06c8df-dc37-4777-b60f-802f0c7628cb', 'name': None, 'parent_model': '48ba8e1a-4ee0-4dd8-84dc-b22b548eb16e', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:59:52 GMT', 'model': '893627c6-cab5-4f27-98f6-709fee0aec22', 'model_id': '893627c6-cab5-4f27-98f6-709fee0aec22', 'name': None, 'parent_model': 'eed875ba-0c5e-4e72-84bf-2f7cd2818616', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:18:07 GMT', 'model': '8935d372-7f24-4eb8-9fff-ad2492379d40', 'model_id': '8935d372-7f24-4eb8-9fff-ad2492379d40', 'name': None, 'parent_model': 'd3fe9f7e-39f9-4ab3-ad26-4af509dc0b8e', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:34:26 GMT', 'model': '89287967-1617-40d5-adef-00dbdbe6743e', 'model_id': '89287967-1617-40d5-adef-00dbdbe6743e', 'name': None, 'parent_model': 'd75d631b-98ec-45a1-bef8-2e7f959cfff1', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:13:35 GMT', 'model': '888ed478-ce90-4cab-ae30-59e200cd0dd8', 'model_id': '888ed478-ce90-4cab-ae30-59e200cd0dd8', 'name': None, 'parent_model': '4853bd50-016f-4e7a-a955-b44ef30237c0', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:53:08 GMT', 'model': '88536d97-62d1-4833-8e55-b6a55c87cbb5', 'model_id': '88536d97-62d1-4833-8e55-b6a55c87cbb5', 'name': None, 'parent_model': '3dc7deb3-a85d-4155-b390-bcf2cc890c80', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:44:39 GMT', 'model': '879ddd7a-34cc-4a86-b717-0e4445b9d708', 'model_id': '879ddd7a-34cc-4a86-b717-0e4445b9d708', 'name': None, 'parent_model': 'e02b1786-0d08-4d89-9199-ec6dc4e1e151', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:25:35 GMT', 'model': '875f5a50-7653-48d5-9a43-8615f6ae7f47', 'model_id': '875f5a50-7653-48d5-9a43-8615f6ae7f47', 'name': None, 'parent_model': '8f80d11e-524c-44fd-8c66-46b506280df5', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:43:53 GMT', 'model': '874ae2d7-3fb6-4d37-8083-5b96a95ef016', 'model_id': '874ae2d7-3fb6-4d37-8083-5b96a95ef016', 'name': None, 'parent_model': '24fd39e4-c075-4075-866a-b5b0ef937fb4', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:45:17 GMT', 'model': '873334c0-390a-4d76-81c0-c87a7c21d244', 'model_id': '873334c0-390a-4d76-81c0-c87a7c21d244', 'name': None, 'parent_model': '00ab838f-86f7-4880-9883-1ffd55d8f35c', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:10:06 GMT', 'model': '870406a8-2a4a-477f-85a2-195d445183bd', 'model_id': '870406a8-2a4a-477f-85a2-195d445183bd', 'name': None, 'parent_model': '540ed0e2-c498-4934-8faa-590d281b9d7e', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:35:58 GMT', 'model': '86dbb66d-af42-4cf2-b07c-ea6cb158f1e0', 'model_id': '86dbb66d-af42-4cf2-b07c-ea6cb158f1e0', 'name': None, 'parent_model': 'ecea315a-c00e-47a0-897e-6bc6784438c4', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:44:40 GMT', 'model': '85e44978-ec1d-4e8a-bc58-37b0dd364f44', 'model_id': '85e44978-ec1d-4e8a-bc58-37b0dd364f44', 'name': None, 'parent_model': '37e48780-41be-4827-b03c-444bcc485958', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:15:22 GMT', 'model': '85cf4f2b-652b-42e2-a970-897facf189f8', 'model_id': '85cf4f2b-652b-42e2-a970-897facf189f8', 'name': None, 'parent_model': 'e1269ed8-2950-42d6-a4da-92adf9bc71f5', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:44:29 GMT', 'model': '85a18809-19ab-487a-90df-5b35f33f9f83', 'model_id': '85a18809-19ab-487a-90df-5b35f33f9f83', 'name': None, 'parent_model': '2c2a5062-adc4-4803-894d-133a8c8de9bf', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:14:57 GMT', 'model': '85690a5d-6617-4551-9cda-b82aab6a3d96', 'model_id': '85690a5d-6617-4551-9cda-b82aab6a3d96', 'name': None, 'parent_model': '5fedebaa-278f-4eba-b93f-6bad4c1a0ee0', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:47:31 GMT', 'model': '84ee590f-dcda-4e27-9e42-9f11cfc03a78', 'model_id': '84ee590f-dcda-4e27-9e42-9f11cfc03a78', 'name': None, 'parent_model': '93aa1685-48cf-4cb6-81b2-2b401ff266a4', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:34:28 GMT', 'model': '84e5445e-d023-41a9-b1a0-b08c050cfdb4', 'model_id': '84e5445e-d023-41a9-b1a0-b08c050cfdb4', 'name': None, 'parent_model': '78d52b83-55c2-4751-a841-6835f0b2c1cd', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:01:54 GMT', 'model': '839c278c-506a-4bc2-9d32-ada1bc987c92', 'model_id': '839c278c-506a-4bc2-9d32-ada1bc987c92', 'name': None, 'parent_model': 'de4b37dc-4eb6-4fe8-9e78-d3b95dd6b976', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:59:40 GMT', 'model': '838f401c-494b-4f90-ac6b-b4fca5d2abb9', 'model_id': '838f401c-494b-4f90-ac6b-b4fca5d2abb9', 'name': None, 'parent_model': '226b6919-2e0d-4329-9276-1257aeebb5d8', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:33:27 GMT', 'model': '82fad5ba-6747-4acb-95dd-6ea9ddef58f8', 'model_id': '82fad5ba-6747-4acb-95dd-6ea9ddef58f8', 'name': None, 'parent_model': 'd45582d3-681a-40c8-ac15-e2db3f3c1d0e', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:28:53 GMT', 'model': '82e1a19f-7308-4364-89fe-b87cc7d85aa9', 'model_id': '82e1a19f-7308-4364-89fe-b87cc7d85aa9', 'name': None, 'parent_model': '66a78b86-0704-46e1-87b0-eabc0383ea46', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:17:39 GMT', 'model': '82e19754-f92d-4ed9-a3a1-fa869126f717', 'model_id': '82e19754-f92d-4ed9-a3a1-fa869126f717', 'name': None, 'parent_model': '35517fd4-5a9e-4be0-bca7-768c46a6f8b6', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:27:21 GMT', 'model': '82c38af2-d39d-43fb-b79e-a4fe05ccea06', 'model_id': '82c38af2-d39d-43fb-b79e-a4fe05ccea06', 'name': None, 'parent_model': 'cf707d6a-13b3-4677-b8eb-71aceb9ab3a3', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:37:38 GMT', 'model': '82589fd5-52ea-4d13-a84d-216c5637fc00', 'model_id': '82589fd5-52ea-4d13-a84d-216c5637fc00', 'name': None, 'parent_model': '2308127a-7dcb-479a-9d41-b5d09dee80f1', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:48:46 GMT', 'model': '81f75659-772b-4a5b-b00c-fcae674f4fbd', 'model_id': '81f75659-772b-4a5b-b00c-fcae674f4fbd', 'name': None, 'parent_model': '4c800850-badf-4bb5-a661-0804b0de16b7', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:54:45 GMT', 'model': '810d1050-6a01-48eb-b5ec-4f411713e721', 'model_id': '810d1050-6a01-48eb-b5ec-4f411713e721', 'name': None, 'parent_model': '0a0a61cf-7cfb-4935-9f55-a418d1afafba', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:32:23 GMT', 'model': '80f4b305-b987-490c-836c-f72e8ee28c08', 'model_id': '80f4b305-b987-490c-836c-f72e8ee28c08', 'name': None, 'parent_model': '078e0682-6d2d-462c-8588-152e0f98c8dc', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:22:47 GMT', 'model': '80d7a802-3c2c-43e8-af73-2cac9ba9adf0', 'model_id': '80d7a802-3c2c-43e8-af73-2cac9ba9adf0', 'name': None, 'parent_model': '43853a66-244a-45be-aa00-1e447689b979', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:34:18 GMT', 'model': '801e14be-e743-4871-9158-d85b13793333', 'model_id': '801e14be-e743-4871-9158-d85b13793333', 'name': None, 'parent_model': '635a8fb2-2147-4feb-9538-52ab176cd8b4', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:22:48 GMT', 'model': '7ebfd2cc-5ee5-485b-bd3e-798a0db83d8d', 'model_id': '7ebfd2cc-5ee5-485b-bd3e-798a0db83d8d', 'name': None, 'parent_model': '591c9abb-adcc-4fa4-9bbb-36d938bedab8', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:04:44 GMT', 'model': '7d38faf0-c0f9-4041-928c-4d22fc8cb15e', 'model_id': '7d38faf0-c0f9-4041-928c-4d22fc8cb15e', 'name': None, 'parent_model': '608b24c1-d7f0-4f25-a4f4-6f0487ffe67c', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:49:35 GMT', 'model': '7c598d23-a398-4310-987d-a8de819b181f', 'model_id': '7c598d23-a398-4310-987d-a8de819b181f', 'name': None, 'parent_model': '6cbf7e71-a0ce-48c7-9dc0-38c70ac0f813', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:06:48 GMT', 'model': '7c4ac221-f6d1-4038-a5a5-d573d6485e3c', 'model_id': '7c4ac221-f6d1-4038-a5a5-d573d6485e3c', 'name': None, 'parent_model': '4022ec90-3eb6-400d-a575-9c62c5d0089e', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:01:42 GMT', 'model': '7ba76861-2cb5-456a-bc47-0b6efe0c5fd2', 'model_id': '7ba76861-2cb5-456a-bc47-0b6efe0c5fd2', 'name': None, 'parent_model': '01123a52-71bd-4d2d-bced-cd5e2bf714f6', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:24:51 GMT', 'model': '7b5bb36c-2343-49d3-842f-458e0f6f5b0d', 'model_id': '7b5bb36c-2343-49d3-842f-458e0f6f5b0d', 'name': None, 'parent_model': 'e09fcfb1-b66a-438d-b139-a1441be05a23', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:51:41 GMT', 'model': '7b42305d-9ce6-42f7-b15c-4e1d9edc4ac7', 'model_id': '7b42305d-9ce6-42f7-b15c-4e1d9edc4ac7', 'name': None, 'parent_model': '4fcc56af-259b-4a52-b702-d83144678490', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:13:18 GMT', 'model': '7b171a08-75bb-4bdf-a82e-3896cda2171c', 'model_id': '7b171a08-75bb-4bdf-a82e-3896cda2171c', 'name': None, 'parent_model': 'a413f162-ce2c-4b42-b635-3bc38a40842a', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:34:05 GMT', 'model': '78d52b83-55c2-4751-a841-6835f0b2c1cd', 'model_id': '78d52b83-55c2-4751-a841-6835f0b2c1cd', 'name': None, 'parent_model': 'ce8e3a95-f6d5-4fae-9e94-8ef6ca003e19', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 21:04:48 GMT', 'model': '783f15c1-8b99-4a07-a353-9cf324cc8fc0', 'model_id': '783f15c1-8b99-4a07-a353-9cf324cc8fc0', 'name': None, 'parent_model': '5958aed2-a398-45b2-8701-8dfeb05ef5ff', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:35:43 GMT', 'model': '77b959ef-6c15-4b87-b9c4-5f229f96ed9e', 'model_id': '77b959ef-6c15-4b87-b9c4-5f229f96ed9e', 'name': None, 'parent_model': 'ed7d4446-4361-4923-9f7e-c662c07f32c5', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:19:18 GMT', 'model': '779ca47d-8269-45ee-903e-aef988a9d28b', 'model_id': '779ca47d-8269-45ee-903e-aef988a9d28b', 'name': None, 'parent_model': 'a27babc5-907d-4a6d-9a08-555c1389a39a', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:18:58 GMT', 'model': '775a5e34-e4cd-429e-a001-8c2050818966', 'model_id': '775a5e34-e4cd-429e-a001-8c2050818966', 'name': None, 'parent_model': 'e89be0b2-deeb-4079-b69f-223a9600e727', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 21:02:46 GMT', 'model': '7706c971-8265-4bfa-9878-b27667029a74', 'model_id': '7706c971-8265-4bfa-9878-b27667029a74', 'name': None, 'parent_model': '9155dd7d-41be-4515-9b3d-1b65b1bddb2b', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:06:00 GMT', 'model': '767d3df1-b3f2-453f-aa12-26b7f7728216', 'model_id': '767d3df1-b3f2-453f-aa12-26b7f7728216', 'name': None, 'parent_model': '0cfdf850-c1bc-40a0-9f1e-6bb3984d8523', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:10:27 GMT', 'model': '76185288-e40f-41a8-8470-4ca34caf2c33', 'model_id': '76185288-e40f-41a8-8470-4ca34caf2c33', 'name': None, 'parent_model': 'e404e033-3855-4613-aae1-33635468e9d8', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:17:00 GMT', 'model': '75b5c7d5-bf82-4ec9-b149-f95eafcee9eb', 'model_id': '75b5c7d5-bf82-4ec9-b149-f95eafcee9eb', 'name': None, 'parent_model': 'f73998dc-dbd2-4907-9730-4c9b803c6b00', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:30:32 GMT', 'model': '7529ea28-024e-419e-a1d9-76c546f5f76c', 'model_id': '7529ea28-024e-419e-a1d9-76c546f5f76c', 'name': None, 'parent_model': '6a019676-ba35-4caa-833b-2080ed2b18ad', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:28:20 GMT', 'model': '74cfd7c2-1a63-420c-be0e-f7609e8917fd', 'model_id': '74cfd7c2-1a63-420c-be0e-f7609e8917fd', 'name': None, 'parent_model': '07395c38-6a31-43a8-9c24-994a2ac356ed', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:35:14 GMT', 'model': '749729a6-77f2-4d6a-8d47-bc9f21f09651', 'model_id': '749729a6-77f2-4d6a-8d47-bc9f21f09651', 'name': None, 'parent_model': 'fa7b76dc-ce39-4e53-a80d-63eeaafd4589', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:25:31 GMT', 'model': '73fd821b-80d6-4892-b870-c6e291d1892b', 'model_id': '73fd821b-80d6-4892-b870-c6e291d1892b', 'name': None, 'parent_model': 'a1fd8545-98af-4696-850d-777c866b3ffd', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:00:32 GMT', 'model': '73500632-669b-4d17-adc7-5e38493e7f20', 'model_id': '73500632-669b-4d17-adc7-5e38493e7f20', 'name': None, 'parent_model': 'cdd359ae-ed92-44e7-a93a-4b2aac5165bc', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:41:00 GMT', 'model': '731e41d1-a917-426d-8c76-a303ca18498c', 'model_id': '731e41d1-a917-426d-8c76-a303ca18498c', 'name': None, 'parent_model': '96129f90-6ef0-4bd7-a9eb-25e121dc2d21', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:20:32 GMT', 'model': '72f3746e-04bc-41f7-b47e-a660b80f92e5', 'model_id': '72f3746e-04bc-41f7-b47e-a660b80f92e5', 'name': None, 'parent_model': 'c497c0ea-6cbf-48d5-8ac4-22a784aaa59c', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:43:51 GMT', 'model': '72922f7f-8238-4a7b-97ec-2b66fb104bfd', 'model_id': '72922f7f-8238-4a7b-97ec-2b66fb104bfd', 'name': None, 'parent_model': 'a1fed732-be5c-4916-815f-687183079e24', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:55:09 GMT', 'model': '72392897-ac2b-443e-8244-bb2a829e78aa', 'model_id': '72392897-ac2b-443e-8244-bb2a829e78aa', 'name': None, 'parent_model': '810d1050-6a01-48eb-b5ec-4f411713e721', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:19:03 GMT', 'model': '7130c73d-ecd8-4e1c-9583-15fd69430b5b', 'model_id': '7130c73d-ecd8-4e1c-9583-15fd69430b5b', 'name': None, 'parent_model': '2c9746bc-6834-4b8a-85b6-e4e6fc1fe8cf', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:16:12 GMT', 'model': '70fce658-518e-4b65-9707-7929fd321e2d', 'model_id': '70fce658-518e-4b65-9707-7929fd321e2d', 'name': None, 'parent_model': '5f2b67a3-6a70-4539-9d75-99efadc7a61a', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:59:50 GMT', 'model': '704395e9-7482-4026-bf2b-01efe9c440aa', 'model_id': '704395e9-7482-4026-bf2b-01efe9c440aa', 'name': None, 'parent_model': '492abd7a-87b3-42de-96a3-a7b4bf7b796d', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:20:21 GMT', 'model': '7013ab7b-9f7a-45ed-a14e-0cfb10dd1bcb', 'model_id': '7013ab7b-9f7a-45ed-a14e-0cfb10dd1bcb', 'name': None, 'parent_model': 'b42115c1-c9d7-446a-bfea-dc17623dcb90', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:47:03 GMT', 'model': '6f0002c3-0c30-4229-acb1-46726e1d5928', 'model_id': '6f0002c3-0c30-4229-acb1-46726e1d5928', 'name': None, 'parent_model': 'e90e788a-927d-4683-a02d-83c9de1d8b80', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:31:00 GMT', 'model': '6ef23645-13d1-45e3-8c31-56a8aa3769af', 'model_id': '6ef23645-13d1-45e3-8c31-56a8aa3769af', 'name': None, 'parent_model': '7529ea28-024e-419e-a1d9-76c546f5f76c', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:56:09 GMT', 'model': '6ee6c5d2-2ff3-4ccf-a184-01b08267c4fd', 'model_id': '6ee6c5d2-2ff3-4ccf-a184-01b08267c4fd', 'name': None, 'parent_model': 'd2628817-4a8b-4cb1-9044-51a3106aa792', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:55:58 GMT', 'model': '6ec95010-aa3a-4eee-b3c8-c57574f27839', 'model_id': '6ec95010-aa3a-4eee-b3c8-c57574f27839', 'name': None, 'parent_model': '0120b2d1-cf8f-4965-9116-dcd191b1849a', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:38:08 GMT', 'model': '6e990e93-8a32-4877-a998-c56b6d317024', 'model_id': '6e990e93-8a32-4877-a998-c56b6d317024', 'name': None, 'parent_model': 'f026363e-d0df-4274-af52-44e3f3adfab7', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:41:21 GMT', 'model': '6e412898-bbb0-4eaf-a4fe-86fee3029297', 'model_id': '6e412898-bbb0-4eaf-a4fe-86fee3029297', 'name': None, 'parent_model': '2e09c36e-2ec1-4e49-aab4-bd141b9305f0', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:08:01 GMT', 'model': '6e1733f4-7658-41a9-8746-1ef74e4383cf', 'model_id': '6e1733f4-7658-41a9-8746-1ef74e4383cf', 'name': None, 'parent_model': 'e8305bff-cb91-4e0a-92e9-9e112a32602e', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:19:13 GMT', 'model': '6cffdeb6-7e22-404f-a233-d1420ecdbb5c', 'model_id': '6cffdeb6-7e22-404f-a233-d1420ecdbb5c', 'name': None, 'parent_model': '775a5e34-e4cd-429e-a001-8c2050818966', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:43:02 GMT', 'model': '6cf95fe6-0f72-4b42-9dc5-be038e939ae5', 'model_id': '6cf95fe6-0f72-4b42-9dc5-be038e939ae5', 'name': None, 'parent_model': 'bafbd518-5189-4dc5-bdf0-6c4d4ff39d95', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:49:10 GMT', 'model': '6cbf7e71-a0ce-48c7-9dc0-38c70ac0f813', 'model_id': '6cbf7e71-a0ce-48c7-9dc0-38c70ac0f813', 'name': None, 'parent_model': '81f75659-772b-4a5b-b00c-fcae674f4fbd', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:51:31 GMT', 'model': '6c8b6c84-884f-40d8-ac50-3f16039f1e9e', 'model_id': '6c8b6c84-884f-40d8-ac50-3f16039f1e9e', 'name': None, 'parent_model': '31d0d4f0-0c24-4893-b4a2-4b5d5dc63cba', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:11:43 GMT', 'model': '6c0a5fa3-bf70-4b73-89d4-34d001d3ee53', 'model_id': '6c0a5fa3-bf70-4b73-89d4-34d001d3ee53', 'name': None, 'parent_model': '0fbd0d5b-8c51-4e8c-9857-363e5bc8e2f0', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:43:26 GMT', 'model': '6bc5a56d-b68f-4fdc-8aa2-bf7e29aace96', 'model_id': '6bc5a56d-b68f-4fdc-8aa2-bf7e29aace96', 'name': None, 'parent_model': '2a93cd3f-1ed2-4afc-af88-febf02f575e4', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:36:56 GMT', 'model': '6b718264-4d61-4faf-83c6-8cec696fd260', 'model_id': '6b718264-4d61-4faf-83c6-8cec696fd260', 'name': None, 'parent_model': 'acff391a-fa15-4dc1-9d78-7666117b5f0d', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:10:47 GMT', 'model': '6b3a5a2f-75c7-4e66-ab8a-f310f67a60a2', 'model_id': '6b3a5a2f-75c7-4e66-ab8a-f310f67a60a2', 'name': None, 'parent_model': '3c26e627-a191-4e27-a23f-272be9e9dac6', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:18:29 GMT', 'model': '6b2c86e8-176b-417d-9c5e-88baafff4298', 'model_id': '6b2c86e8-176b-417d-9c5e-88baafff4298', 'name': None, 'parent_model': '9070e73d-7dab-4e9e-8bf0-796162cb1ac9', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:25:16 GMT', 'model': '6b11c40f-bc2d-4ce0-941c-3f2fc6f6ff70', 'model_id': '6b11c40f-bc2d-4ce0-941c-3f2fc6f6ff70', 'name': None, 'parent_model': '7b5bb36c-2343-49d3-842f-458e0f6f5b0d', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:30:11 GMT', 'model': '6a019676-ba35-4caa-833b-2080ed2b18ad', 'model_id': '6a019676-ba35-4caa-833b-2080ed2b18ad', 'name': None, 'parent_model': 'd5095189-5d45-48f1-b5b0-c887c9422759', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:52:20 GMT', 'model': '69705d84-4ec7-40c6-a555-c7e22f76f471', 'model_id': '69705d84-4ec7-40c6-a555-c7e22f76f471', 'name': None, 'parent_model': 'bd04d093-bfb8-4589-9a3e-e784e439523b', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 21:08:25 GMT', 'model': '695aac77-5579-4359-a6e0-0263d4969af1', 'model_id': '695aac77-5579-4359-a6e0-0263d4969af1', 'name': None, 'parent_model': '6e1733f4-7658-41a9-8746-1ef74e4383cf', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:28:28 GMT', 'model': '66a78b86-0704-46e1-87b0-eabc0383ea46', 'model_id': '66a78b86-0704-46e1-87b0-eabc0383ea46', 'name': None, 'parent_model': '9c5ece57-0fab-499a-a36c-4996a1f520f7', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:02:05 GMT', 'model': '65a7dac3-63f3-4502-a49c-63f6b9d3a83e', 'model_id': '65a7dac3-63f3-4502-a49c-63f6b9d3a83e', 'name': None, 'parent_model': 'a989e55e-18d1-4226-8010-ccee096be054', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:13:41 GMT', 'model': '6557e769-7188-4448-9d87-e83bbb990c7b', 'model_id': '6557e769-7188-4448-9d87-e83bbb990c7b', 'name': None, 'parent_model': '21532c21-6aff-492b-9b13-97d316be7f7b', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:38:30 GMT', 'model': '649306e9-9466-4818-a93d-6af6d2c88eb8', 'model_id': '649306e9-9466-4818-a93d-6af6d2c88eb8', 'name': None, 'parent_model': 'be8cf200-4aa0-4f63-9ce6-6ba377f04b80', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:12:29 GMT', 'model': '6466d2b8-a03e-43de-8940-73f446360c6e', 'model_id': '6466d2b8-a03e-43de-8940-73f446360c6e', 'name': None, 'parent_model': '97082d29-49e3-4560-bcfd-5f0d89b510f1', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:42:37 GMT', 'model': '63f8ab71-574b-45cf-880b-dd3edce96854', 'model_id': '63f8ab71-574b-45cf-880b-dd3edce96854', 'name': None, 'parent_model': 'c80317f3-fc8c-49c7-a76c-a6390fcd1e5c', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:10:51 GMT', 'model': '6384bf43-0c3a-43de-b865-c95f1ce46383', 'model_id': '6384bf43-0c3a-43de-b865-c95f1ce46383', 'name': None, 'parent_model': '76185288-e40f-41a8-8470-4ca34caf2c33', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:59:00 GMT', 'model': '6379939e-f9b2-4910-ab64-967ad4028511', 'model_id': '6379939e-f9b2-4910-ab64-967ad4028511', 'name': None, 'parent_model': 'ee0780cd-6819-48a2-ab3f-2cfd536125b9', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:33:52 GMT', 'model': '635a8fb2-2147-4feb-9538-52ab176cd8b4', 'model_id': '635a8fb2-2147-4feb-9538-52ab176cd8b4', 'name': None, 'parent_model': '82fad5ba-6747-4acb-95dd-6ea9ddef58f8', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:23:12 GMT', 'model': '633b17bb-6fee-4cbd-92a7-ee112cb1a837', 'model_id': '633b17bb-6fee-4cbd-92a7-ee112cb1a837', 'name': None, 'parent_model': '7ebfd2cc-5ee5-485b-bd3e-798a0db83d8d', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:50:17 GMT', 'model': '62d8f671-6b1d-4307-a37e-4f4107820d23', 'model_id': '62d8f671-6b1d-4307-a37e-4f4107820d23', 'name': None, 'parent_model': '3c6e51e4-98af-4430-8ec5-9a16852a529e', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:17:49 GMT', 'model': '62d1bf65-53e5-4674-9b51-dc249eab11e5', 'model_id': '62d1bf65-53e5-4674-9b51-dc249eab11e5', 'name': None, 'parent_model': '4166a7c3-f43c-4ed5-81f8-21d217573d78', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:16:56 GMT', 'model': '6215e9c7-cd0c-46af-ab4f-28b2d22d9ffc', 'model_id': '6215e9c7-cd0c-46af-ab4f-28b2d22d9ffc', 'name': None, 'parent_model': '30872c0e-d3ae-4785-8cd6-71d8fe5732d1', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:23:02 GMT', 'model': '61494b3c-8fe0-4fe6-9059-f5f37412d189', 'model_id': '61494b3c-8fe0-4fe6-9059-f5f37412d189', 'name': None, 'parent_model': 'a29abcb6-4316-4cd2-96ca-53ebaa4030bf', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:49:01 GMT', 'model': '6118f91d-098f-4e55-ae65-58c9e36317c0', 'model_id': '6118f91d-098f-4e55-ae65-58c9e36317c0', 'name': None, 'parent_model': 'b03e0721-b683-4c58-be3d-e79aa3c48ea4', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:04:20 GMT', 'model': '608b24c1-d7f0-4f25-a4f4-6f0487ffe67c', 'model_id': '608b24c1-d7f0-4f25-a4f4-6f0487ffe67c', 'name': None, 'parent_model': 'db9edf85-ed30-41e1-8ae8-81f60e472851', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:58:12 GMT', 'model': '60557e06-391c-465e-a554-f0e32dc95c41', 'model_id': '60557e06-391c-465e-a554-f0e32dc95c41', 'name': None, 'parent_model': '32d4eeac-69ee-4067-aa0e-534a2004e55f', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:14:32 GMT', 'model': '5fedebaa-278f-4eba-b93f-6bad4c1a0ee0', 'model_id': '5fedebaa-278f-4eba-b93f-6bad4c1a0ee0', 'name': None, 'parent_model': '014a5d60-1159-4f85-8f12-e5b76bd111fa', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 21:22:13 GMT', 'model': '5f389fdd-561f-49ee-9a03-3e1be9c219e6', 'model_id': '5f389fdd-561f-49ee-9a03-3e1be9c219e6', 'name': None, 'parent_model': 'afacb56f-f001-4967-9b5e-19fe0b576e79', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:15:48 GMT', 'model': '5f2b67a3-6a70-4539-9d75-99efadc7a61a', 'model_id': '5f2b67a3-6a70-4539-9d75-99efadc7a61a', 'name': None, 'parent_model': '85cf4f2b-652b-42e2-a970-897facf189f8', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:46:15 GMT', 'model': '5e898a76-71ac-4a87-8c6f-cd6415fbee72', 'model_id': '5e898a76-71ac-4a87-8c6f-cd6415fbee72', 'name': None, 'parent_model': '1285b541-4234-4ca0-9e75-983a2a13272a', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 21:01:31 GMT', 'model': '5e46ecd3-ef5c-4f1e-ab06-4a484eae6edb', 'model_id': '5e46ecd3-ef5c-4f1e-ab06-4a484eae6edb', 'name': None, 'parent_model': 'e677469f-a332-43c6-ae5e-28c86c83bd12', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:08:49 GMT', 'model': '5d62bd3f-c674-4361-baf7-8ff3a147d029', 'model_id': '5d62bd3f-c674-4361-baf7-8ff3a147d029', 'name': None, 'parent_model': '695aac77-5579-4359-a6e0-0263d4969af1', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:26:42 GMT', 'model': '5d46651a-0648-47b6-a8c6-03e6c4a99a2e', 'model_id': '5d46651a-0648-47b6-a8c6-03e6c4a99a2e', 'name': None, 'parent_model': 'a6e7102a-a581-40eb-b963-b1b1d63fdc9d', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 21:15:20 GMT', 'model': '5d446bec-513e-4238-8ab2-ba8853459519', 'model_id': '5d446bec-513e-4238-8ab2-ba8853459519', 'name': None, 'parent_model': 'c404f913-0fb4-4ebd-ad68-f797fe49b7eb', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:35:10 GMT', 'model': '5d1659d5-984e-4a56-a4e0-c2ac634424b9', 'model_id': '5d1659d5-984e-4a56-a4e0-c2ac634424b9', 'name': None, 'parent_model': '56cea560-25b7-4eb8-9781-be3a3e3481b8', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:54:47 GMT', 'model': '5bc906ca-1b99-4486-9a9a-578a2138431d', 'model_id': '5bc906ca-1b99-4486-9a9a-578a2138431d', 'name': None, 'parent_model': '1101ca7d-3e9c-49f7-a013-2a02b4dea481', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:51:37 GMT', 'model': '5bb2c3ec-3dbe-41a1-94b2-c65a5160acb0', 'model_id': '5bb2c3ec-3dbe-41a1-94b2-c65a5160acb0', 'name': None, 'parent_model': 'a79209fe-1675-4772-b1bf-52f656227d3c', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:05:08 GMT', 'model': '5b9d10d0-6408-4dd9-8e0e-5d0bc3cad593', 'model_id': '5b9d10d0-6408-4dd9-8e0e-5d0bc3cad593', 'name': None, 'parent_model': '7d38faf0-c0f9-4041-928c-4d22fc8cb15e', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:07:29 GMT', 'model': '5b94d073-6dc8-4af5-a7ac-9f122d9db6e6', 'model_id': '5b94d073-6dc8-4af5-a7ac-9f122d9db6e6', 'name': None, 'parent_model': 'aaeafbfd-fb4f-4ad3-b003-90c96c180419', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:50:42 GMT', 'model': '5b7f6e1a-83d8-4089-9377-03976ca14bb8', 'model_id': '5b7f6e1a-83d8-4089-9377-03976ca14bb8', 'name': None, 'parent_model': 'a639ab0a-eec1-48fc-85b2-03d1d06e0e9f', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:41:31 GMT', 'model': '5ab95eea-381b-4b4c-9965-d8d1f78f6862', 'model_id': '5ab95eea-381b-4b4c-9965-d8d1f78f6862', 'name': None, 'parent_model': '5884b08c-c31e-49cf-9b9c-5a96ec93da3e', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:56:26 GMT', 'model': '5a6daf6d-b909-4d65-8566-afe0ed59594b', 'model_id': '5a6daf6d-b909-4d65-8566-afe0ed59594b', 'name': None, 'parent_model': '32d3ba09-6507-44de-89c3-2cacd9800108', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:03:23 GMT', 'model': '59e5d4b7-27c1-407f-af2d-6a29ccece154', 'model_id': '59e5d4b7-27c1-407f-af2d-6a29ccece154', 'name': None, 'parent_model': 'c949b10a-2baa-4d20-afc6-a99a3b5af1f3', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 21:04:24 GMT', 'model': '5958aed2-a398-45b2-8701-8dfeb05ef5ff', 'model_id': '5958aed2-a398-45b2-8701-8dfeb05ef5ff', 'name': None, 'parent_model': 'fb875eae-c14a-4437-9cb2-46881756d527', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:22:23 GMT', 'model': '591c9abb-adcc-4fa4-9bbb-36d938bedab8', 'model_id': '591c9abb-adcc-4fa4-9bbb-36d938bedab8', 'name': None, 'parent_model': '93df3d4e-18ac-49c4-b512-3d8805d695fe', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:25:38 GMT', 'model': '58d5f64b-12f2-42f3-af00-b1653c1a9b2e', 'model_id': '58d5f64b-12f2-42f3-af00-b1653c1a9b2e', 'name': None, 'parent_model': '369f491b-d118-45e1-8c6e-3e3540c14162', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:41:07 GMT', 'model': '5884b08c-c31e-49cf-9b9c-5a96ec93da3e', 'model_id': '5884b08c-c31e-49cf-9b9c-5a96ec93da3e', 'name': None, 'parent_model': '26444efa-d831-4072-9e91-0179da3b7946', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:34:43 GMT', 'model': '56cea560-25b7-4eb8-9781-be3a3e3481b8', 'model_id': '56cea560-25b7-4eb8-9781-be3a3e3481b8', 'name': None, 'parent_model': '4ab48fb0-09e1-4470-aae3-05c1b14d13b5', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:07:37 GMT', 'model': '5685ed0f-b2af-4b0e-81a3-a4240942576a', 'model_id': '5685ed0f-b2af-4b0e-81a3-a4240942576a', 'name': None, 'parent_model': 'cca1105a-e979-4aae-b1de-abf0d6ce45c0', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:05:24 GMT', 'model': '563c20aa-883c-40a6-87e9-46f706ede8c8', 'model_id': '563c20aa-883c-40a6-87e9-46f706ede8c8', 'name': None, 'parent_model': 'acfe1d3d-de19-4c88-abb2-442841add432', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 21:21:22 GMT', 'model': '55a19400-37dd-4370-ab10-7d724d8700bd', 'model_id': '55a19400-37dd-4370-ab10-7d724d8700bd', 'name': None, 'parent_model': 'f154b375-b563-4d7c-b73a-7b842d2846a6', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:38:02 GMT', 'model': '556c0bb9-b538-4af4-b41e-05cf00cb910d', 'model_id': '556c0bb9-b538-4af4-b41e-05cf00cb910d', 'name': None, 'parent_model': '9902acd5-2a4b-42c5-96e8-4cc4799a2f7b', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:00:55 GMT', 'model': '551c860b-e146-4c55-820c-18f9bfe0e671', 'model_id': '551c860b-e146-4c55-820c-18f9bfe0e671', 'name': None, 'parent_model': '73500632-669b-4d17-adc7-5e38493e7f20', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:36:48 GMT', 'model': '54a1a509-d9e9-4d40-81d6-25857f2c60fe', 'model_id': '54a1a509-d9e9-4d40-81d6-25857f2c60fe', 'name': None, 'parent_model': '082fdf1e-b70d-438e-89d8-aff3c9b5f2bf', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:42:49 GMT', 'model': '54848fcc-2c4b-4417-bf0a-4bb7cf7944f3', 'model_id': '54848fcc-2c4b-4417-bf0a-4bb7cf7944f3', 'name': None, 'parent_model': 'a7029d70-c444-42d3-90e2-709f67cd7dd3', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:09:42 GMT', 'model': '540ed0e2-c498-4934-8faa-590d281b9d7e', 'model_id': '540ed0e2-c498-4934-8faa-590d281b9d7e', 'name': None, 'parent_model': 'ee11559f-a6aa-46cf-8c0e-ce6aadcd36b5', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:54:57 GMT', 'model': '53bcaebb-5bc3-4ed7-8dda-67782983f022', 'model_id': '53bcaebb-5bc3-4ed7-8dda-67782983f022', 'name': None, 'parent_model': 'c661d4bc-7785-4293-bfa4-99f03920b6cd', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:33:53 GMT', 'model': '53a7cffc-ce7f-4b3c-ad28-8239425fdf5a', 'model_id': '53a7cffc-ce7f-4b3c-ad28-8239425fdf5a', 'name': None, 'parent_model': 'ef0627ab-e649-4fba-b2b7-62c64e916e0a', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 21:12:31 GMT', 'model': '533cfc99-1a62-439e-a613-93c0063ababd', 'model_id': '533cfc99-1a62-439e-a613-93c0063ababd', 'name': None, 'parent_model': '9a67813a-1067-4645-9b8f-4eac84fb0dc7', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:22:38 GMT', 'model': '5223c505-cf13-45fe-9d11-81d1e604aed8', 'model_id': '5223c505-cf13-45fe-9d11-81d1e604aed8', 'name': None, 'parent_model': '5f389fdd-561f-49ee-9a03-3e1be9c219e6', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:50:03 GMT', 'model': '51f097eb-b1a4-474a-806e-837ca810635a', 'model_id': '51f097eb-b1a4-474a-806e-837ca810635a', 'name': None, 'parent_model': '4a1f79a2-cb6b-484c-b5bc-98f95b2b659c', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:03:10 GMT', 'model': '50c8abb9-6fc7-4f3f-a04f-335a8393789a', 'model_id': '50c8abb9-6fc7-4f3f-a04f-335a8393789a', 'name': None, 'parent_model': '7706c971-8265-4bfa-9878-b27667029a74', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:16:35 GMT', 'model': '50b29a25-12ba-4671-9c4d-9af087be892f', 'model_id': '50b29a25-12ba-4671-9c4d-9af087be892f', 'name': None, 'parent_model': '70fce658-518e-4b65-9707-7929fd321e2d', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:11:34 GMT', 'model': '50b02046-2752-4e3d-bf6a-95b095156875', 'model_id': '50b02046-2752-4e3d-bf6a-95b095156875', 'name': None, 'parent_model': 'dcad8dd7-4477-4773-81a9-91eed944c098', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:26:50 GMT', 'model': '5069832c-361c-4076-8179-97b0d990441f', 'model_id': '5069832c-361c-4076-8179-97b0d990441f', 'name': None, 'parent_model': 'f064448a-c863-4c28-ae03-8472e8e96b5b', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:57:39 GMT', 'model': '50136f30-5e8c-47a5-bca7-87ed9a8f8407', 'model_id': '50136f30-5e8c-47a5-bca7-87ed9a8f8407', 'name': None, 'parent_model': '8a06c8df-dc37-4777-b60f-802f0c7628cb', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:51:16 GMT', 'model': '4fcc56af-259b-4a52-b702-d83144678490', 'model_id': '4fcc56af-259b-4a52-b702-d83144678490', 'name': None, 'parent_model': '118d0262-3b42-4275-878b-e25a8a8669fc', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:46:31 GMT', 'model': '4fc42295-001d-4fa2-81eb-bcae1702417c', 'model_id': '4fc42295-001d-4fa2-81eb-bcae1702417c', 'name': None, 'parent_model': '29b80f3f-b5b0-4dfd-b700-6cc322628132', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:23:28 GMT', 'model': '4ecbd3d6-0397-44bd-b9f3-3939a328600a', 'model_id': '4ecbd3d6-0397-44bd-b9f3-3939a328600a', 'name': None, 'parent_model': '61494b3c-8fe0-4fe6-9059-f5f37412d189', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:09:56 GMT', 'model': '4ddbf542-c15e-484b-be3e-bcb4128934e8', 'model_id': '4ddbf542-c15e-484b-be3e-bcb4128934e8', 'name': None, 'parent_model': '32b970c8-3246-487e-aa39-c75c1ee73c34', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:48:21 GMT', 'model': '4c800850-badf-4bb5-a661-0804b0de16b7', 'model_id': '4c800850-badf-4bb5-a661-0804b0de16b7', 'name': None, 'parent_model': '0bbb3d24-1d52-42f4-a51b-c383d14a2bd5', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:55:37 GMT', 'model': '4c7d7b60-bdd1-44d0-beef-e98845d49229', 'model_id': '4c7d7b60-bdd1-44d0-beef-e98845d49229', 'name': None, 'parent_model': '25923536-80be-481f-b387-488360f792a9', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:14:32 GMT', 'model': '4c51b608-b4b0-4e92-ae7c-fb26679b8b72', 'model_id': '4c51b608-b4b0-4e92-ae7c-fb26679b8b72', 'name': None, 'parent_model': '93cc1df9-a616-49be-9cd5-abbcdcb8575c', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:05:01 GMT', 'model': '4bc36877-898d-4f53-871e-991983f20aca', 'model_id': '4bc36877-898d-4f53-871e-991983f20aca', 'name': None, 'parent_model': 'bd67f2c7-a640-4194-8820-f3257327ca48', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:27:32 GMT', 'model': '4b9ba400-5ec6-4731-b545-79025ad192d2', 'model_id': '4b9ba400-5ec6-4731-b545-79025ad192d2', 'name': None, 'parent_model': 'cb142ecb-905d-4d5f-b708-e29bd9e37341', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:24:16 GMT', 'model': '4b3875f5-0625-4ae6-8714-08dfa8a6ad25', 'model_id': '4b3875f5-0625-4ae6-8714-08dfa8a6ad25', 'name': None, 'parent_model': '40035636-b82e-4a6e-b798-58b4920e9d5b', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:34:18 GMT', 'model': '4ab48fb0-09e1-4470-aae3-05c1b14d13b5', 'model_id': '4ab48fb0-09e1-4470-aae3-05c1b14d13b5', 'name': None, 'parent_model': '53a7cffc-ce7f-4b3c-ad28-8239425fdf5a', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:02:17 GMT', 'model': '4a609ed9-6123-4a8c-9b89-84916563f119', 'model_id': '4a609ed9-6123-4a8c-9b89-84916563f119', 'name': None, 'parent_model': '839c278c-506a-4bc2-9d32-ada1bc987c92', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:02:41 GMT', 'model': '4a45b724-3490-4a50-8be7-5df69e8e7345', 'model_id': '4a45b724-3490-4a50-8be7-5df69e8e7345', 'name': None, 'parent_model': '4a609ed9-6123-4a8c-9b89-84916563f119', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:49:38 GMT', 'model': '4a1f79a2-cb6b-484c-b5bc-98f95b2b659c', 'model_id': '4a1f79a2-cb6b-484c-b5bc-98f95b2b659c', 'name': None, 'parent_model': 'f1ea5492-cca8-4bcb-a4fe-5f52d2592b62', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:45:03 GMT', 'model': '4a02be83-a571-4863-b9f5-e5e5bf7470e5', 'model_id': '4a02be83-a571-4863-b9f5-e5e5bf7470e5', 'name': None, 'parent_model': '879ddd7a-34cc-4a86-b717-0e4445b9d708', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:59:26 GMT', 'model': '492abd7a-87b3-42de-96a3-a7b4bf7b796d', 'model_id': '492abd7a-87b3-42de-96a3-a7b4bf7b796d', 'name': None, 'parent_model': '6379939e-f9b2-4910-ab64-967ad4028511', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:00:41 GMT', 'model': '48f4cfc8-6f7f-49b6-9089-71aadbe27e45', 'model_id': '48f4cfc8-6f7f-49b6-9089-71aadbe27e45', 'name': None, 'parent_model': '2d8e431e-e98c-4469-a610-9895098a788c', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:56:51 GMT', 'model': '48ba8e1a-4ee0-4dd8-84dc-b22b548eb16e', 'model_id': '48ba8e1a-4ee0-4dd8-84dc-b22b548eb16e', 'name': None, 'parent_model': '5a6daf6d-b909-4d65-8566-afe0ed59594b', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:13:11 GMT', 'model': '4853bd50-016f-4e7a-a955-b44ef30237c0', 'model_id': '4853bd50-016f-4e7a-a955-b44ef30237c0', 'name': None, 'parent_model': 'cbe0ac6e-2058-4a8f-be1d-61da590f5a1b', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:58:37 GMT', 'model': '47950500-d05b-4f86-8fd0-8aea34fd92b4', 'model_id': '47950500-d05b-4f86-8fd0-8aea34fd92b4', 'name': None, 'parent_model': '0bec3be3-cf9a-45e9-8957-15c7f8ab10a8', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:53:58 GMT', 'model': '4787bcf9-1e01-433f-bfd4-0db7f1e0bd72', 'model_id': '4787bcf9-1e01-433f-bfd4-0db7f1e0bd72', 'name': None, 'parent_model': 'da5d1cb1-30a9-4b81-95e3-5c00e1589c22', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:08:28 GMT', 'model': '475656f4-4b02-4f68-a373-42dd8eccba80', 'model_id': '475656f4-4b02-4f68-a373-42dd8eccba80', 'name': None, 'parent_model': 'a7d74f0d-1864-4f19-a26c-034ac06fee24', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:29:42 GMT', 'model': '469c5be5-c900-4e1e-a5ae-d1c87f4d864f', 'model_id': '469c5be5-c900-4e1e-a5ae-d1c87f4d864f', 'name': None, 'parent_model': '8a83cc4c-7551-4261-ba55-e7b0201df89f', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:35:31 GMT', 'model': '4617858c-c8b3-4bfc-8ba8-f4a4dda884f1', 'model_id': '4617858c-c8b3-4bfc-8ba8-f4a4dda884f1', 'name': None, 'parent_model': '98f07390-0fd4-4a40-8606-2a4b5e5d2273', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:21:09 GMT', 'model': '4595b7f3-0d9b-4254-9759-94bd768438ad', 'model_id': '4595b7f3-0d9b-4254-9759-94bd768438ad', 'name': None, 'parent_model': '9e21e9d1-bd09-4de8-9de1-5b1763037424', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:53:15 GMT', 'model': '43ef44d9-056c-48b8-857c-291ad5f00f84', 'model_id': '43ef44d9-056c-48b8-857c-291ad5f00f84', 'name': None, 'parent_model': '98384026-3764-4437-a98d-e48d06774ec9', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:22:24 GMT', 'model': '43853a66-244a-45be-aa00-1e447689b979', 'model_id': '43853a66-244a-45be-aa00-1e447689b979', 'name': None, 'parent_model': 'be6e1d85-1738-4281-9997-35e97e6ca7cb', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:47:47 GMT', 'model': '41b07e79-dbbf-4abf-9d83-8fcde4affd2e', 'model_id': '41b07e79-dbbf-4abf-9d83-8fcde4affd2e', 'name': None, 'parent_model': '8ecc86cf-19ed-49f4-bd02-76354c8850d2', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:17:00 GMT', 'model': '41a8c79f-b2e6-4592-b7dc-ad3015a646f1', 'model_id': '41a8c79f-b2e6-4592-b7dc-ad3015a646f1', 'name': None, 'parent_model': '50b29a25-12ba-4671-9c4d-9af087be892f', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:26:30 GMT', 'model': '4174f10f-e5e4-49c7-a5a9-5d5bfec3036e', 'model_id': '4174f10f-e5e4-49c7-a5a9-5d5bfec3036e', 'name': None, 'parent_model': 'a825f62e-16d2-4e3a-aacc-b9278aebb858', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:17:24 GMT', 'model': '4166a7c3-f43c-4ed5-81f8-21d217573d78', 'model_id': '4166a7c3-f43c-4ed5-81f8-21d217573d78', 'name': None, 'parent_model': '41a8c79f-b2e6-4592-b7dc-ad3015a646f1', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:41:25 GMT', 'model': '413a6f0c-7a54-4aeb-9118-afd5a2f79c4d', 'model_id': '413a6f0c-7a54-4aeb-9118-afd5a2f79c4d', 'name': None, 'parent_model': '731e41d1-a917-426d-8c76-a303ca18498c', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:58:04 GMT', 'model': '40e4974e-cf5a-4430-a946-06229865b9fe', 'model_id': '40e4974e-cf5a-4430-a946-06229865b9fe', 'name': None, 'parent_model': '50136f30-5e8c-47a5-bca7-87ed9a8f8407', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:39:17 GMT', 'model': '40e401ff-b056-47e3-942f-d34789d491e9', 'model_id': '40e401ff-b056-47e3-942f-d34789d491e9', 'name': None, 'parent_model': '9f2b2440-0916-4e56-879a-e21f107cbbee', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:37:18 GMT', 'model': '40a961e4-db23-483f-9af7-202e9393e19f', 'model_id': '40a961e4-db23-483f-9af7-202e9393e19f', 'name': None, 'parent_model': '068bab00-492f-4fda-9a09-e26383a9de1c', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 21:06:24 GMT', 'model': '4022ec90-3eb6-400d-a575-9c62c5d0089e', 'model_id': '4022ec90-3eb6-400d-a575-9c62c5d0089e', 'name': None, 'parent_model': '767d3df1-b3f2-453f-aa12-26b7f7728216', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:23:09 GMT', 'model': '40219deb-9950-4415-8184-577d212095da', 'model_id': '40219deb-9950-4415-8184-577d212095da', 'name': None, 'parent_model': '80d7a802-3c2c-43e8-af73-2cac9ba9adf0', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:23:53 GMT', 'model': '40035636-b82e-4a6e-b798-58b4920e9d5b', 'model_id': '40035636-b82e-4a6e-b798-58b4920e9d5b', 'name': None, 'parent_model': '4ecbd3d6-0397-44bd-b9f3-3939a328600a', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:52:28 GMT', 'model': '3ffbd35e-825a-4eaf-a5c7-c0357674ddfc', 'model_id': '3ffbd35e-825a-4eaf-a5c7-c0357674ddfc', 'name': None, 'parent_model': 'd414ecb5-a304-46be-8d8e-a59587d23160', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:14:24 GMT', 'model': '3f8e1fef-c3b4-41c6-b569-9ee2ea54cb8d', 'model_id': '3f8e1fef-c3b4-41c6-b569-9ee2ea54cb8d', 'name': None, 'parent_model': '9e116ea6-8a55-45b4-80c2-ca86ff6bc0d5', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:55:47 GMT', 'model': '3ed5545d-f3f7-4798-83ea-921a4546f3e0', 'model_id': '3ed5545d-f3f7-4798-83ea-921a4546f3e0', 'name': None, 'parent_model': 'a29b6232-55a3-492d-8a7c-91de7bc167da', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:32:38 GMT', 'model': '3e9197c4-a53b-4f12-af20-f25d3d351541', 'model_id': '3e9197c4-a53b-4f12-af20-f25d3d351541', 'name': None, 'parent_model': '9bf02e67-7362-40fb-b546-66c192cf1058', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:53:55 GMT', 'model': '3dc9eef1-6906-4554-adae-87118ef38f31', 'model_id': '3dc9eef1-6906-4554-adae-87118ef38f31', 'name': None, 'parent_model': '33e7cd5c-c016-416e-8f72-08ed9bffe0c6', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:52:46 GMT', 'model': '3dc7deb3-a85d-4155-b390-bcf2cc890c80', 'model_id': '3dc7deb3-a85d-4155-b390-bcf2cc890c80', 'name': None, 'parent_model': 'dfbee681-261a-4d40-8589-8fe60f05d1eb', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:09:07 GMT', 'model': '3dc24c19-5af9-42c0-aa1e-c75ef735a653', 'model_id': '3dc24c19-5af9-42c0-aa1e-c75ef735a653', 'name': None, 'parent_model': 'f82ed811-8879-4b49-9f0e-47ebda9a3902', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:33:18 GMT', 'model': '3cf0d5aa-8f1f-489c-ad9a-abdfa963bc69', 'model_id': '3cf0d5aa-8f1f-489c-ad9a-abdfa963bc69', 'name': None, 'parent_model': '27457247-28fc-49ef-a76c-64a4db45a893', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:45:42 GMT', 'model': '3c94c8dd-ce4c-48ff-82eb-197abfb9ffd0', 'model_id': '3c94c8dd-ce4c-48ff-82eb-197abfb9ffd0', 'name': None, 'parent_model': '873334c0-390a-4d76-81c0-c87a7c21d244', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:49:52 GMT', 'model': '3c6e51e4-98af-4430-8ec5-9a16852a529e', 'model_id': '3c6e51e4-98af-4430-8ec5-9a16852a529e', 'name': None, 'parent_model': 'c2f217d4-7133-46e6-aabe-fac076e0bd80', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:22:13 GMT', 'model': '3c4cbd82-168e-4d38-af73-f6f9f93e85d4', 'model_id': '3c4cbd82-168e-4d38-af73-f6f9f93e85d4', 'name': None, 'parent_model': 'e76cc2d8-eda1-4e51-ab19-908073decaec', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:10:21 GMT', 'model': '3c26e627-a191-4e27-a23f-272be9e9dac6', 'model_id': '3c26e627-a191-4e27-a23f-272be9e9dac6', 'name': None, 'parent_model': '4ddbf542-c15e-484b-be3e-bcb4128934e8', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:19:27 GMT', 'model': '3bd2b407-297c-40d4-bace-69a2ab74d8ee', 'model_id': '3bd2b407-297c-40d4-bace-69a2ab74d8ee', 'name': None, 'parent_model': '7130c73d-ecd8-4e1c-9583-15fd69430b5b', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:26:31 GMT', 'model': '3bd0fc40-a93f-4fac-84ac-bc24287c0419', 'model_id': '3bd0fc40-a93f-4fac-84ac-bc24287c0419', 'name': None, 'parent_model': '8eb5797d-ac23-413b-a932-6fed86749e88', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 21:22:59 GMT', 'model': '3b2df363-57fd-463e-b49b-1256039aa5a5', 'model_id': '3b2df363-57fd-463e-b49b-1256039aa5a5', 'name': None, 'parent_model': '5223c505-cf13-45fe-9d11-81d1e604aed8', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:19:42 GMT', 'model': '38cacea3-7e16-4355-ae27-625720dfa34b', 'model_id': '38cacea3-7e16-4355-ae27-625720dfa34b', 'name': None, 'parent_model': '779ca47d-8269-45ee-903e-aef988a9d28b', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:06:40 GMT', 'model': '38a7d49f-a699-426e-9f8e-8e0c6a421508', 'model_id': '38a7d49f-a699-426e-9f8e-8e0c6a421508', 'name': None, 'parent_model': '056242e3-f7fa-4945-8a82-7b6a9b58c287', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:48:22 GMT', 'model': '388274fd-b918-40b9-9baf-4d63b87e86a2', 'model_id': '388274fd-b918-40b9-9baf-4d63b87e86a2', 'name': None, 'parent_model': 'cb106dbc-2406-43eb-9c50-3e175f1ebb0f', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:44:14 GMT', 'model': '37e48780-41be-4827-b03c-444bcc485958', 'model_id': '37e48780-41be-4827-b03c-444bcc485958', 'name': None, 'parent_model': 'aceb722a-2fdf-4982-9ae3-9843027b79de', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:30:11 GMT', 'model': '379d64b6-4a2b-485e-aed2-12716d3136cf', 'model_id': '379d64b6-4a2b-485e-aed2-12716d3136cf', 'name': None, 'parent_model': 'b66247ea-304f-4059-b594-ec7f638972af', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:46:45 GMT', 'model': '3793a688-89a2-4cf8-b863-2b63f3c8eb82', 'model_id': '3793a688-89a2-4cf8-b863-2b63f3c8eb82', 'name': None, 'parent_model': '1f1d9bc4-1e83-4afa-9c17-497a363a129d', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:13:43 GMT', 'model': '375f219a-e3ac-4ef8-90c3-b6b854ff611f', 'model_id': '375f219a-e3ac-4ef8-90c3-b6b854ff611f', 'name': None, 'parent_model': '7b171a08-75bb-4bdf-a82e-3896cda2171c', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:21:30 GMT', 'model': '374b5775-28f5-40a7-9b51-b6677158d0aa', 'model_id': '374b5775-28f5-40a7-9b51-b6677158d0aa', 'name': None, 'parent_model': 'aacb592f-3d84-417f-bfe7-d832ecf31899', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 21:14:34 GMT', 'model': '3718c3b8-c62e-4939-b18f-c571d738623e', 'model_id': '3718c3b8-c62e-4939-b18f-c571d738623e', 'name': None, 'parent_model': '30320bc1-6bd6-4748-8e70-1e58c019d2b1', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:25:14 GMT', 'model': '369f491b-d118-45e1-8c6e-3e3540c14162', 'model_id': '369f491b-d118-45e1-8c6e-3e3540c14162', 'name': None, 'parent_model': '10f9f46a-17a0-46f0-b503-26f5de16da17', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:05:51 GMT', 'model': '366ed4f8-6b79-43a3-b076-ae65ca2918ee', 'model_id': '366ed4f8-6b79-43a3-b076-ae65ca2918ee', 'name': None, 'parent_model': '1ffc021a-1498-41e1-b7cc-a29063e1eb25', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:56:22 GMT', 'model': '35e05901-4a82-473d-8bef-834d691d016e', 'model_id': '35e05901-4a82-473d-8bef-834d691d016e', 'name': None, 'parent_model': '6ec95010-aa3a-4eee-b3c8-c57574f27839', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:17:15 GMT', 'model': '35517fd4-5a9e-4be0-bca7-768c46a6f8b6', 'model_id': '35517fd4-5a9e-4be0-bca7-768c46a6f8b6', 'name': None, 'parent_model': '92b2c175-156d-40a0-9253-8448bfbca308', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:03:30 GMT', 'model': '3546efe3-3659-4e1c-9d0b-d9297b850967', 'model_id': '3546efe3-3659-4e1c-9d0b-d9297b850967', 'name': None, 'parent_model': '8c31c26c-80a3-48cb-9aa9-87ee58afe60b', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:56:33 GMT', 'model': '34a8396c-7895-4702-801c-e01a366cbdf1', 'model_id': '34a8396c-7895-4702-801c-e01a366cbdf1', 'name': None, 'parent_model': '6ee6c5d2-2ff3-4ccf-a184-01b08267c4fd', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:52:54 GMT', 'model': '34518709-7d79-452e-822a-b24ad08a2718', 'model_id': '34518709-7d79-452e-822a-b24ad08a2718', 'name': None, 'parent_model': 'c79ec909-1e84-4f31-8125-ed9d78e2b930', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:53:30 GMT', 'model': '33e7cd5c-c016-416e-8f72-08ed9bffe0c6', 'model_id': '33e7cd5c-c016-416e-8f72-08ed9bffe0c6', 'name': None, 'parent_model': '88536d97-62d1-4833-8e55-b6a55c87cbb5', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:58:54 GMT', 'model': '32fbb56c-5a34-4e23-be42-092549562bca', 'model_id': '32fbb56c-5a34-4e23-be42-092549562bca', 'name': None, 'parent_model': '8f690c79-6213-4ae3-83a9-a6dba1d786f1', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:57:47 GMT', 'model': '32d4eeac-69ee-4067-aa0e-534a2004e55f', 'model_id': '32d4eeac-69ee-4067-aa0e-534a2004e55f', 'name': None, 'parent_model': '028d0ce9-7e56-4c41-b652-8b031d18a1ad', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:56:01 GMT', 'model': '32d3ba09-6507-44de-89c3-2cacd9800108', 'model_id': '32d3ba09-6507-44de-89c3-2cacd9800108', 'name': None, 'parent_model': '4c7d7b60-bdd1-44d0-beef-e98845d49229', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:09:31 GMT', 'model': '32b970c8-3246-487e-aa39-c75c1ee73c34', 'model_id': '32b970c8-3246-487e-aa39-c75c1ee73c34', 'name': None, 'parent_model': '17e9cde4-9a4b-4bcd-ac89-674870f54d4e', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:51:07 GMT', 'model': '31d0d4f0-0c24-4893-b4a2-4b5d5dc63cba', 'model_id': '31d0d4f0-0c24-4893-b4a2-4b5d5dc63cba', 'name': None, 'parent_model': '917cbea1-cc02-4cd7-9ab2-ae5aa8eb2744', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 21:09:13 GMT', 'model': '318e2ea5-30ed-4299-89e8-653aa0cecac0', 'model_id': '318e2ea5-30ed-4299-89e8-653aa0cecac0', 'name': None, 'parent_model': '5d62bd3f-c674-4361-baf7-8ff3a147d029', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:16:32 GMT', 'model': '30872c0e-d3ae-4785-8cd6-71d8fe5732d1', 'model_id': '30872c0e-d3ae-4785-8cd6-71d8fe5732d1', 'name': None, 'parent_model': 'd89e7f71-9a6a-4b81-87c9-cf0ee5e4affa', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:14:09 GMT', 'model': '30320bc1-6bd6-4748-8e70-1e58c019d2b1', 'model_id': '30320bc1-6bd6-4748-8e70-1e58c019d2b1', 'name': None, 'parent_model': '375f219a-e3ac-4ef8-90c3-b6b854ff611f', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:49:59 GMT', 'model': '2f50e10e-b367-4e7f-a7e0-ba7a22a5af85', 'model_id': '2f50e10e-b367-4e7f-a7e0-ba7a22a5af85', 'name': None, 'parent_model': '7c598d23-a398-4310-987d-a8de819b181f', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:12:00 GMT', 'model': '2e2889d9-8853-4f33-853a-a17ea992423e', 'model_id': '2e2889d9-8853-4f33-853a-a17ea992423e', 'name': None, 'parent_model': 'c15f13e0-7295-4a6f-86dd-ab1a74416ecc', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:40:58 GMT', 'model': '2e09c36e-2ec1-4e49-aab4-bd141b9305f0', 'model_id': '2e09c36e-2ec1-4e49-aab4-bd141b9305f0', 'name': None, 'parent_model': '8c6e3f38-04ef-44f1-b2cf-61c123b8fda2', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:31:25 GMT', 'model': '2db3e2d3-08ca-4a8f-9b73-26e3e06c9d37', 'model_id': '2db3e2d3-08ca-4a8f-9b73-26e3e06c9d37', 'name': None, 'parent_model': '6ef23645-13d1-45e3-8c31-56a8aa3769af', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:45:29 GMT', 'model': '2d9e5723-ff17-4b71-9f60-736eabb0e31b', 'model_id': '2d9e5723-ff17-4b71-9f60-736eabb0e31b', 'name': None, 'parent_model': 'ace3aaff-fe20-42f0-9b33-07d4e95da9c6', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 21:00:17 GMT', 'model': '2d8e431e-e98c-4469-a610-9895098a788c', 'model_id': '2d8e431e-e98c-4469-a610-9895098a788c', 'name': None, 'parent_model': '893627c6-cab5-4f27-98f6-709fee0aec22', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:28:09 GMT', 'model': '2d3378c4-16c3-4ec1-9ed4-e904a7791dce', 'model_id': '2d3378c4-16c3-4ec1-9ed4-e904a7791dce', 'name': None, 'parent_model': '91d59105-06d8-4167-8d0b-402fc6beae75', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:20:58 GMT', 'model': '2cc33803-b45c-4e99-8d50-fc96f0a3a76a', 'model_id': '2cc33803-b45c-4e99-8d50-fc96f0a3a76a', 'name': None, 'parent_model': '72f3746e-04bc-41f7-b47e-a660b80f92e5', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:18:39 GMT', 'model': '2c9746bc-6834-4b8a-85b6-e4e6fc1fe8cf', 'model_id': '2c9746bc-6834-4b8a-85b6-e4e6fc1fe8cf', 'name': None, 'parent_model': '903f2f4c-d080-4a9e-aa3a-daa7f83eb903', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:44:04 GMT', 'model': '2c2a5062-adc4-4803-894d-133a8c8de9bf', 'model_id': '2c2a5062-adc4-4803-894d-133a8c8de9bf', 'name': None, 'parent_model': 'e626adf3-78b6-4659-ab8d-58756a64492b', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:10:19 GMT', 'model': '2c27cbbd-d1d3-4ae7-969e-4b031a235a5a', 'model_id': '2c27cbbd-d1d3-4ae7-969e-4b031a235a5a', 'name': None, 'parent_model': '9dfdf095-5e33-4e9a-8c99-75771050d711', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:43:01 GMT', 'model': '2a93cd3f-1ed2-4afc-af88-febf02f575e4', 'model_id': '2a93cd3f-1ed2-4afc-af88-febf02f575e4', 'name': None, 'parent_model': '63f8ab71-574b-45cf-880b-dd3edce96854', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:46:19 GMT', 'model': '2a347eb8-7855-4c07-be51-6f395a29bde4', 'model_id': '2a347eb8-7855-4c07-be51-6f395a29bde4', 'name': None, 'parent_model': 'c719be14-1696-4b47-9966-15cff5b3ac80', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:11:59 GMT', 'model': '2a051ad9-155e-4c9c-9596-d0db28731ab7', 'model_id': '2a051ad9-155e-4c9c-9596-d0db28731ab7', 'name': None, 'parent_model': '50b02046-2752-4e3d-bf6a-95b095156875', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:46:06 GMT', 'model': '29b80f3f-b5b0-4dfd-b700-6cc322628132', 'model_id': '29b80f3f-b5b0-4dfd-b700-6cc322628132', 'name': None, 'parent_model': '3c94c8dd-ce4c-48ff-82eb-197abfb9ffd0', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:51:07 GMT', 'model': '287c950d-7061-427e-af1c-ab5096c6cc4d', 'model_id': '287c950d-7061-427e-af1c-ab5096c6cc4d', 'name': None, 'parent_model': '5b7f6e1a-83d8-4089-9377-03976ca14bb8', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:51:56 GMT', 'model': '275513e5-2d5f-40fd-a735-60114aecfcb0', 'model_id': '275513e5-2d5f-40fd-a735-60114aecfcb0', 'name': None, 'parent_model': '99ac7fd1-c7a7-43d5-9b4e-9a548ae6d35d', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:32:51 GMT', 'model': '27457247-28fc-49ef-a76c-64a4db45a893', 'model_id': '27457247-28fc-49ef-a76c-64a4db45a893', 'name': None, 'parent_model': '0eb628f8-b9da-44a7-bbe7-4f8366f697c1', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:34:41 GMT', 'model': '26dd83fc-9eb6-4cbb-8886-1c5e6aac7395', 'model_id': '26dd83fc-9eb6-4cbb-8886-1c5e6aac7395', 'name': None, 'parent_model': '801e14be-e743-4871-9158-d85b13793333', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:16:11 GMT', 'model': '26bdd07c-04da-4aae-91c3-8e7b74eba5cd', 'model_id': '26bdd07c-04da-4aae-91c3-8e7b74eba5cd', 'name': None, 'parent_model': '8b88d2a9-847f-4e52-b1a6-e62c63b91984', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:21:23 GMT', 'model': '26960d33-5d8e-48b3-bdcb-703578bed79e', 'model_id': '26960d33-5d8e-48b3-bdcb-703578bed79e', 'name': None, 'parent_model': '2cc33803-b45c-4e99-8d50-fc96f0a3a76a', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:40:40 GMT', 'model': '26444efa-d831-4072-9e91-0179da3b7946', 'model_id': '26444efa-d831-4072-9e91-0179da3b7946', 'name': None, 'parent_model': 'a6930266-f3ab-45c9-acd3-36b9321adbd1', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:44:18 GMT', 'model': '263b2bfd-cf09-4397-a5b8-6b6d79b68cf8', 'model_id': '263b2bfd-cf09-4397-a5b8-6b6d79b68cf8', 'name': None, 'parent_model': '874ae2d7-3fb6-4d37-8083-5b96a95ef016', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:43:04 GMT', 'model': '25f416ab-6816-4168-a3e8-b01f8b451404', 'model_id': '25f416ab-6816-4168-a3e8-b01f8b451404', 'name': None, 'parent_model': 'd93f0553-a98c-4be5-be31-2720bec70a1e', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:55:10 GMT', 'model': '25923536-80be-481f-b387-488360f792a9', 'model_id': '25923536-80be-481f-b387-488360f792a9', 'name': None, 'parent_model': '5bc906ca-1b99-4486-9a9a-578a2138431d', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:43:28 GMT', 'model': '24fd39e4-c075-4075-866a-b5b0ef937fb4', 'model_id': '24fd39e4-c075-4075-866a-b5b0ef937fb4', 'name': None, 'parent_model': '25f416ab-6816-4168-a3e8-b01f8b451404', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:37:13 GMT', 'model': '2308127a-7dcb-479a-9d41-b5d09dee80f1', 'model_id': '2308127a-7dcb-479a-9d41-b5d09dee80f1', 'name': None, 'parent_model': '14eeaf3f-5818-4200-b6a6-a38da8660b17', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:59:16 GMT', 'model': '226b6919-2e0d-4329-9276-1257aeebb5d8', 'model_id': '226b6919-2e0d-4329-9276-1257aeebb5d8', 'name': None, 'parent_model': '32fbb56c-5a34-4e23-be42-092549562bca', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:54:03 GMT', 'model': '226501b9-837a-403e-a4bc-998f01c9afc1', 'model_id': '226501b9-837a-403e-a4bc-998f01c9afc1', 'name': None, 'parent_model': '21078d4f-3cad-4d95-bf91-363967878f41', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:36:08 GMT', 'model': '21c5de81-ff85-46da-ab85-5ed2f1264db4', 'model_id': '21c5de81-ff85-46da-ab85-5ed2f1264db4', 'name': None, 'parent_model': '77b959ef-6c15-4b87-b9c4-5f229f96ed9e', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:13:16 GMT', 'model': '21532c21-6aff-492b-9b13-97d316be7f7b', 'model_id': '21532c21-6aff-492b-9b13-97d316be7f7b', 'name': None, 'parent_model': '015c75c5-42a4-4153-88fd-6a594bed7beb', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 21:07:11 GMT', 'model': '212d24e6-aa7e-471f-b759-782f1c011a04', 'model_id': '212d24e6-aa7e-471f-b759-782f1c011a04', 'name': None, 'parent_model': '7c4ac221-f6d1-4038-a5a5-d573d6485e3c', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:53:39 GMT', 'model': '21078d4f-3cad-4d95-bf91-363967878f41', 'model_id': '21078d4f-3cad-4d95-bf91-363967878f41', 'name': None, 'parent_model': '43ef44d9-056c-48b8-857c-291ad5f00f84', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:05:27 GMT', 'model': '1ffc021a-1498-41e1-b7cc-a29063e1eb25', 'model_id': '1ffc021a-1498-41e1-b7cc-a29063e1eb25', 'name': None, 'parent_model': '4bc36877-898d-4f53-871e-991983f20aca', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:57:11 GMT', 'model': '1fc2107d-a8d0-4871-9b0c-48aff13a4d26', 'model_id': '1fc2107d-a8d0-4871-9b0c-48aff13a4d26', 'name': None, 'parent_model': 'c89524a2-4e1e-4938-9d6f-18548adec203', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:46:20 GMT', 'model': '1f1d9bc4-1e83-4afa-9c17-497a363a129d', 'model_id': '1f1d9bc4-1e83-4afa-9c17-497a363a129d', 'name': None, 'parent_model': '952386fc-4163-4b32-a2b6-fc2378e4d044', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:30:31 GMT', 'model': '1ef307dc-6956-45e5-a36d-3cd799bd9467', 'model_id': '1ef307dc-6956-45e5-a36d-3cd799bd9467', 'name': None, 'parent_model': 'adec5bda-29db-4018-ba44-ec8321dbe595', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:43:14 GMT', 'model': '1e5f82c4-72b9-4ad2-b23f-9478b828c436', 'model_id': '1e5f82c4-72b9-4ad2-b23f-9478b828c436', 'name': None, 'parent_model': '54848fcc-2c4b-4417-bf0a-4bb7cf7944f3', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:09:31 GMT', 'model': '1e3d3735-8709-4c19-9d69-0f4ebe4c90de', 'model_id': '1e3d3735-8709-4c19-9d69-0f4ebe4c90de', 'name': None, 'parent_model': '3dc24c19-5af9-42c0-aa1e-c75ef735a653', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:15:22 GMT', 'model': '1dc1e1a9-5573-4403-877f-928d09be7237', 'model_id': '1dc1e1a9-5573-4403-877f-928d09be7237', 'name': None, 'parent_model': '85690a5d-6617-4551-9cda-b82aab6a3d96', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:39:43 GMT', 'model': '1d60094f-631c-4a7f-8145-e4386398bff8', 'model_id': '1d60094f-631c-4a7f-8145-e4386398bff8', 'name': None, 'parent_model': 'dd6bb260-b20d-4ee0-9903-f728de991671', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:40:09 GMT', 'model': '1b499746-1be3-4b64-b383-d6a81c60a2f3', 'model_id': '1b499746-1be3-4b64-b383-d6a81c60a2f3', 'name': None, 'parent_model': 'eb4c4784-c1e1-4015-b943-3d720e30662b', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:37:12 GMT', 'model': '1a4bcda4-c0f8-4181-a6ad-5b92e1abf80e', 'model_id': '1a4bcda4-c0f8-4181-a6ad-5b92e1abf80e', 'name': None, 'parent_model': '54a1a509-d9e9-4d40-81d6-25857f2c60fe', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:40:11 GMT', 'model': '1995c198-63aa-4281-827a-e9c07652223a', 'model_id': '1995c198-63aa-4281-827a-e9c07652223a', 'name': None, 'parent_model': 'bf76514a-5f48-47f7-85be-b0fe6d1de8ec', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:30:43 GMT', 'model': '1946c0c4-11b8-41c2-bd32-6bb7da4cb681', 'model_id': '1946c0c4-11b8-41c2-bd32-6bb7da4cb681', 'name': None, 'parent_model': '185dcbae-cbc0-4568-9020-61613e946233', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:47:33 GMT', 'model': '191238d9-0087-4528-b38b-7dc1e66c46a8', 'model_id': '191238d9-0087-4528-b38b-7dc1e66c46a8', 'name': None, 'parent_model': 'f72642e1-3169-4eca-943f-a5b6a8934f29', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:22:48 GMT', 'model': '187d786b-1a8f-4473-9321-dbcb0a44d892', 'model_id': '187d786b-1a8f-4473-9321-dbcb0a44d892', 'name': None, 'parent_model': '07e2a2c4-45c7-4480-a32f-7f263f8f1461', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:30:20 GMT', 'model': '185dcbae-cbc0-4568-9020-61613e946233', 'model_id': '185dcbae-cbc0-4568-9020-61613e946233', 'name': None, 'parent_model': '0bfa74a4-d795-4753-aede-8c85da8020fe', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:09:08 GMT', 'model': '17e9cde4-9a4b-4bcd-ac89-674870f54d4e', 'model_id': '17e9cde4-9a4b-4bcd-ac89-674870f54d4e', 'name': None, 'parent_model': 'ebc02fbb-23cb-47ab-99da-4cfc0c89d187', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:26:54 GMT', 'model': '17c3d174-fbe2-4314-a372-fd98abe5eef6', 'model_id': '17c3d174-fbe2-4314-a372-fd98abe5eef6', 'name': None, 'parent_model': '4174f10f-e5e4-49c7-a5a9-5d5bfec3036e', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:25:55 GMT', 'model': '17bc8207-1e77-44ad-89e8-70fecd2e924b', 'model_id': '17bc8207-1e77-44ad-89e8-70fecd2e924b', 'name': None, 'parent_model': '73fd821b-80d6-4892-b870-c6e291d1892b', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:02:10 GMT', 'model': '16a344fb-9e3a-4df5-9aea-c37251181c1c', 'model_id': '16a344fb-9e3a-4df5-9aea-c37251181c1c', 'name': None, 'parent_model': '7ba76861-2cb5-456a-bc47-0b6efe0c5fd2', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:02:54 GMT', 'model': '15e0cd65-af74-4fa7-a478-08d46a30ed40', 'model_id': '15e0cd65-af74-4fa7-a478-08d46a30ed40', 'name': None, 'parent_model': '08a89783-6199-4431-891a-8f50cc04b8ef', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:50:23 GMT', 'model': '15954bcf-e018-4d51-ad59-4779c39addf4', 'model_id': '15954bcf-e018-4d51-ad59-4779c39addf4', 'name': None, 'parent_model': '2f50e10e-b367-4e7f-a7e0-ba7a22a5af85', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:47:27 GMT', 'model': '153c37e5-7fef-498b-849c-51bedc145a82', 'model_id': '153c37e5-7fef-498b-849c-51bedc145a82', 'name': None, 'parent_model': '6f0002c3-0c30-4229-acb1-46726e1d5928', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:36:48 GMT', 'model': '14eeaf3f-5818-4200-b6a6-a38da8660b17', 'model_id': '14eeaf3f-5818-4200-b6a6-a38da8660b17', 'name': None, 'parent_model': 'cf0cc961-7aec-41bc-aa11-f258c62d6db9', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:01:16 GMT', 'model': '14ded340-ddc6-49ae-82fc-b93095483059', 'model_id': '14ded340-ddc6-49ae-82fc-b93095483059', 'name': None, 'parent_model': 'ba33aadc-54f7-4c3c-a2d6-3541403dda41', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:45:51 GMT', 'model': '1285b541-4234-4ca0-9e75-983a2a13272a', 'model_id': '1285b541-4234-4ca0-9e75-983a2a13272a', 'name': None, 'parent_model': '080d1a28-7f9d-4c6d-8b8d-ba149db08604', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:38:56 GMT', 'model': '12661475-074b-499a-b25f-c3a074aece58', 'model_id': '12661475-074b-499a-b25f-c3a074aece58', 'name': None, 'parent_model': '649306e9-9466-4818-a93d-6af6d2c88eb8', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:50:52 GMT', 'model': '118d0262-3b42-4275-878b-e25a8a8669fc', 'model_id': '118d0262-3b42-4275-878b-e25a8a8669fc', 'name': None, 'parent_model': 'e041fa07-c4c1-4f4e-b30a-2d0a5ad8d7d2', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:54:22 GMT', 'model': '1101ca7d-3e9c-49f7-a013-2a02b4dea481', 'model_id': '1101ca7d-3e9c-49f7-a013-2a02b4dea481', 'name': None, 'parent_model': '4787bcf9-1e01-433f-bfd4-0db7f1e0bd72', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:24:50 GMT', 'model': '10f9f46a-17a0-46f0-b503-26f5de16da17', 'model_id': '10f9f46a-17a0-46f0-b503-26f5de16da17', 'name': None, 'parent_model': 'c329368e-4124-4e4b-9749-2deec9b522a7', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 21:18:56 GMT', 'model': '10542aff-b66b-492c-9581-0cef4a8ac376', 'model_id': '10542aff-b66b-492c-9581-0cef4a8ac376', 'name': None, 'parent_model': 'acbe88e3-e16b-483f-b2ae-dfa3092faaa6', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 21:20:09 GMT', 'model': '10106182-31be-4f56-be11-19fe8188622e', 'model_id': '10106182-31be-4f56-be11-19fe8188622e', 'name': None, 'parent_model': '9c14d812-f4c5-406e-970f-9ee120e0cfd5', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 19:11:18 GMT', 'model': '0fbd0d5b-8c51-4e8c-9857-363e5bc8e2f0', 'model_id': '0fbd0d5b-8c51-4e8c-9857-363e5bc8e2f0', 'name': None, 'parent_model': '08dae958-1b22-4041-b4aa-1a47d1d906b7', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:16:27 GMT', 'model': '0ed80466-7642-48ab-bbc1-258178eed980', 'model_id': '0ed80466-7642-48ab-bbc1-258178eed980', 'name': None, 'parent_model': 'a917860b-bcbe-4108-b888-5b9ac268149e', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:31:18 GMT', 'model': '0eb628f8-b9da-44a7-bbe7-4f8366f697c1', 'model_id': '0eb628f8-b9da-44a7-bbe7-4f8366f697c1', 'name': None, 'parent_model': 'e5562c5f-8f08-4e3a-a02b-231bfbc2d7d9', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:41:22 GMT', 'model': '0dc635c2-21cb-4486-8753-05bb6c0b2fef', 'model_id': '0dc635c2-21cb-4486-8753-05bb6c0b2fef', 'name': None, 'parent_model': 'bfe56534-d58e-46a2-8857-0417159420b0', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:30:36 GMT', 'model': '0d5f6c89-2fec-41f9-9314-4ee6801851ee', 'model_id': '0d5f6c89-2fec-41f9-9314-4ee6801851ee', 'name': None, 'parent_model': '379d64b6-4a2b-485e-aed2-12716d3136cf', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 21:05:36 GMT', 'model': '0cfdf850-c1bc-40a0-9f1e-6bb3984d8523', 'model_id': '0cfdf850-c1bc-40a0-9f1e-6bb3984d8523', 'name': None, 'parent_model': '05a985a9-19d2-487f-adff-bf3fa36990ca', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:33:37 GMT', 'model': '0ce1a286-8842-4c45-a421-1695debf83bf', 'model_id': '0ce1a286-8842-4c45-a421-1695debf83bf', 'name': None, 'parent_model': 'b653f40b-8346-40e2-bf0f-52e52b00711c', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:29:56 GMT', 'model': '0bfa74a4-d795-4753-aede-8c85da8020fe', 'model_id': '0bfa74a4-d795-4753-aede-8c85da8020fe', 'name': None, 'parent_model': 'e99b6e2e-7b9d-49ac-b40b-9920328028c1', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:58:12 GMT', 'model': '0bec3be3-cf9a-45e9-8957-15c7f8ab10a8', 'model_id': '0bec3be3-cf9a-45e9-8957-15c7f8ab10a8', 'name': None, 'parent_model': 'f601f7ae-34df-4e44-b38e-ce3c31ac1603', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 18:47:56 GMT', 'model': '0bbb3d24-1d52-42f4-a51b-c383d14a2bd5', 'model_id': '0bbb3d24-1d52-42f4-a51b-c383d14a2bd5', 'name': None, 'parent_model': '84ee590f-dcda-4e27-9e42-9f11cfc03a78', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 17:49:52 GMT', 'model': '0b85f303-511b-4d8b-9372-274553607ef8', 'model_id': '0b85f303-511b-4d8b-9372-274553607ef8', 'name': None, 'parent_model': 'f44d4811-b524-4d9c-8f2e-01b3e087dd50', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:58:53 GMT', 'model': '0b086464-c3f2-4981-9dec-430544a08409', 'model_id': '0b086464-c3f2-4981-9dec-430544a08409', 'name': None, 'parent_model': 'cb8cf515-15be-4457-a15e-a1b04fde924e', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:32:12 GMT', 'model': '0aab2669-a442-40d2-abbc-deaa31cd98a8', 'model_id': '0aab2669-a442-40d2-abbc-deaa31cd98a8', 'name': None, 'parent_model': 'a41506c9-49c7-4173-a6e4-8412d0dbe6bb', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:41:49 GMT', 'model': '0a29f813-ed43-45ab-ac9d-9aebc762ddfa', 'model_id': '0a29f813-ed43-45ab-ac9d-9aebc762ddfa', 'name': None, 'parent_model': '413a6f0c-7a54-4aeb-9118-afd5a2f79c4d', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:54:19 GMT', 'model': '0a0a61cf-7cfb-4935-9f55-a418d1afafba', 'model_id': '0a0a61cf-7cfb-4935-9f55-a418d1afafba', 'name': None, 'parent_model': '3dc9eef1-6906-4554-adae-87118ef38f31', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:29:00 GMT', 'model': '0976bfed-bd0d-4833-a60f-d594fb59cd42', 'model_id': '0976bfed-bd0d-4833-a60f-d594fb59cd42', 'name': None, 'parent_model': 'f23db284-9a07-4665-a9a3-dd69f2b6dc62', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:07:29 GMT', 'model': '093091a5-8918-4b73-9a4f-7d3bcb48a418', 'model_id': '093091a5-8918-4b73-9a4f-7d3bcb48a418', 'name': None, 'parent_model': '940695d2-6e72-42ee-a11e-1718b5ed9199', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:10:54 GMT', 'model': '08dae958-1b22-4041-b4aa-1a47d1d906b7', 'model_id': '08dae958-1b22-4041-b4aa-1a47d1d906b7', 'name': None, 'parent_model': 'ff5678e8-d091-4b10-84aa-20f14176d9b3', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:17:27 GMT', 'model': '08ad21f5-7aeb-4a1a-9080-eea1a6a97f93', 'model_id': '08ad21f5-7aeb-4a1a-9080-eea1a6a97f93', 'name': None, 'parent_model': '75b5c7d5-bf82-4ec9-b149-f95eafcee9eb', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:02:29 GMT', 'model': '08a89783-6199-4431-891a-8f50cc04b8ef', 'model_id': '08a89783-6199-4431-891a-8f50cc04b8ef', 'name': None, 'parent_model': '65a7dac3-63f3-4502-a49c-63f6b9d3a83e', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:36:22 GMT', 'model': '082fdf1e-b70d-438e-89d8-aff3c9b5f2bf', 'model_id': '082fdf1e-b70d-438e-89d8-aff3c9b5f2bf', 'name': None, 'parent_model': '86dbb66d-af42-4cf2-b07c-ea6cb158f1e0', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:45:27 GMT', 'model': '080d1a28-7f9d-4c6d-8b8d-ba149db08604', 'model_id': '080d1a28-7f9d-4c6d-8b8d-ba149db08604', 'name': None, 'parent_model': '4a02be83-a571-4863-b9f5-e5e5bf7470e5', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:22:22 GMT', 'model': '07e2a2c4-45c7-4480-a32f-7f263f8f1461', 'model_id': '07e2a2c4-45c7-4480-a32f-7f263f8f1461', 'name': None, 'parent_model': 'ddc8d2ad-4113-4ab4-be8f-bacdcf910ef3', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:31:59 GMT', 'model': '078e0682-6d2d-462c-8588-152e0f98c8dc', 'model_id': '078e0682-6d2d-462c-8588-152e0f98c8dc', 'name': None, 'parent_model': 'f38c1231-1104-4258-90fc-96393ba79207', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:27:57 GMT', 'model': '07395c38-6a31-43a8-9c24-994a2ac356ed', 'model_id': '07395c38-6a31-43a8-9c24-994a2ac356ed', 'name': None, 'parent_model': '4b9ba400-5ec6-4731-b545-79025ad192d2', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 19:08:52 GMT', 'model': '06fd78cd-5d7a-4081-b97f-f10ed89b5fce', 'model_id': '06fd78cd-5d7a-4081-b97f-f10ed89b5fce', 'name': None, 'parent_model': '475656f4-4b02-4f68-a373-42dd8eccba80', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 20:36:53 GMT', 'model': '068bab00-492f-4fda-9a09-e26383a9de1c', 'model_id': '068bab00-492f-4fda-9a09-e26383a9de1c', 'name': None, 'parent_model': 'f3c56650-5d74-4bd4-92be-3b31ec1437fb', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 21:05:12 GMT', 'model': '05a985a9-19d2-487f-adff-bf3fa36990ca', 'model_id': '05a985a9-19d2-487f-adff-bf3fa36990ca', 'name': None, 'parent_model': '783f15c1-8b99-4a07-a353-9cf324cc8fc0', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:40:33 GMT', 'model': '05a2d395-3411-4da3-8b32-9fd14b25886e', 'model_id': '05a2d395-3411-4da3-8b32-9fd14b25886e', 'name': None, 'parent_model': 'ba361877-a75e-4319-af75-dd983bc1d8fa', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 20:06:15 GMT', 'model': '056242e3-f7fa-4945-8a82-7b6a9b58c287', 'model_id': '056242e3-f7fa-4945-8a82-7b6a9b58c287', 'name': None, 'parent_model': '366ed4f8-6b79-43a3-b076-ae65ca2918ee', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 17:20:19 GMT', 'model': '050e8b49-957a-43b9-8116-2914fb3556f9', 'model_id': '050e8b49-957a-43b9-8116-2914fb3556f9', 'name': None, 'parent_model': 'e2b615d9-5466-49e3-8dca-a4acb2658b3c', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 17:47:51 GMT', 'model': '04ce09d9-8e10-4512-b324-66075852dd62', 'model_id': '04ce09d9-8e10-4512-b324-66075852dd62', 'name': None, 'parent_model': '153c37e5-7fef-498b-849c-51bedc145a82', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 18:28:58 GMT', 'model': '04c21707-bb5b-4ad1-aaf3-117ccdb6c422', 'model_id': '04c21707-bb5b-4ad1-aaf3-117ccdb6c422', 'name': None, 'parent_model': 'efa5418f-d670-495b-8bec-8a298069c200', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:23:36 GMT', 'model': '04b76483-1eb1-4c4d-b275-2bbb56e94ae8', 'model_id': '04b76483-1eb1-4c4d-b275-2bbb56e94ae8', 'name': None, 'parent_model': '633b17bb-6fee-4cbd-92a7-ee112cb1a837', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:00:05 GMT', 'model': '0494a168-30a4-4a5d-b819-96da1b313264', 'model_id': '0494a168-30a4-4a5d-b819-96da1b313264', 'name': None, 'parent_model': '838f401c-494b-4f90-ac6b-b4fca5d2abb9', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 20:53:42 GMT', 'model': '034dbac4-5c1c-4c08-ad0c-6f4b9b11bf4e', 'model_id': '034dbac4-5c1c-4c08-ad0c-6f4b9b11bf4e', 'name': None, 'parent_model': '0062caea-b67a-49ec-933b-8325a5ceebc7', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 20:14:48 GMT', 'model': '032e49fa-fa0d-44fd-b0bf-107fcf69716c', 'model_id': '032e49fa-fa0d-44fd-b0bf-107fcf69716c', 'name': None, 'parent_model': '3f8e1fef-c3b4-41c6-b569-9ee2ea54cb8d', 'session_id': '32e9f083-48ed-49ac-bbea-c65c5a8e3b62'}, {'committed_at': 'Tue, 22 Apr 2025 18:57:22 GMT', 'model': '028d0ce9-7e56-4c41-b652-8b031d18a1ad', 'model_id': '028d0ce9-7e56-4c41-b652-8b031d18a1ad', 'name': None, 'parent_model': 'b8d6b599-f30c-45e7-ac15-e383a95b565e', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 19:46:57 GMT', 'model': '02709899-6add-464b-9527-4d6c2a94cd23', 'model_id': '02709899-6add-464b-9527-4d6c2a94cd23', 'name': None, 'parent_model': '4fc42295-001d-4fa2-81eb-bcae1702417c', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 19:25:41 GMT', 'model': '0219db36-520c-488e-a6ea-a3f6f17a7ff3', 'model_id': '0219db36-520c-488e-a6ea-a3f6f17a7ff3', 'name': None, 'parent_model': '6b11c40f-bc2d-4ce0-941c-3f2fc6f6ff70', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 17:24:24 GMT', 'model': '01b9c6df-1c8c-4e6b-a56b-d8f8dd4c97b0', 'model_id': '01b9c6df-1c8c-4e6b-a56b-d8f8dd4c97b0', 'name': None, 'parent_model': 'c3363d47-936f-44e7-9a05-6b345bb179da', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 19:05:58 GMT', 'model': '0167d98c-2d49-4d15-9b86-e866b6c7b303', 'model_id': '0167d98c-2d49-4d15-9b86-e866b6c7b303', 'name': None, 'parent_model': 'a4cb2d36-ba87-4d77-bf05-0c3ab580e2c4', 'session_id': '1e783a52-a951-4706-9556-8d7b8bd99087'}, {'committed_at': 'Tue, 22 Apr 2025 18:12:51 GMT', 'model': '015c75c5-42a4-4153-88fd-6a594bed7beb', 'model_id': '015c75c5-42a4-4153-88fd-6a594bed7beb', 'name': None, 'parent_model': 'ca494a0b-e774-4d57-a79c-ce6e3cb7a04b', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 18:14:07 GMT', 'model': '014a5d60-1159-4f85-8f12-e5b76bd111fa', 'model_id': '014a5d60-1159-4f85-8f12-e5b76bd111fa', 'name': None, 'parent_model': '6557e769-7188-4448-9d87-e83bbb990c7b', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 17:55:34 GMT', 'model': '0120b2d1-cf8f-4965-9116-dcd191b1849a', 'model_id': '0120b2d1-cf8f-4965-9116-dcd191b1849a', 'name': None, 'parent_model': '72392897-ac2b-443e-8244-bb2a829e78aa', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:01:17 GMT', 'model': '01123a52-71bd-4d2d-bced-cd5e2bf714f6', 'model_id': '01123a52-71bd-4d2d-bced-cd5e2bf714f6', 'name': None, 'parent_model': '551c860b-e146-4c55-820c-18f9bfe0e671', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 18:04:09 GMT', 'model': '01043492-0171-46e3-afab-4d33a76c152e', 'model_id': '01043492-0171-46e3-afab-4d33a76c152e', 'name': None, 'parent_model': 'f38a00ee-3ba0-4b1f-a0c0-d0bb1d54b91e', 'session_id': 'd2da42f5-8aa0-45fd-a62c-ee30fb110a1d'}, {'committed_at': 'Tue, 22 Apr 2025 19:44:55 GMT', 'model': '00ab838f-86f7-4880-9883-1ffd55d8f35c', 'model_id': '00ab838f-86f7-4880-9883-1ffd55d8f35c', 'name': None, 'parent_model': '85a18809-19ab-487a-90df-5b35f33f9f83', 'session_id': 'ca0eb913-306d-40bb-a003-3eda6bc59d27'}, {'committed_at': 'Tue, 22 Apr 2025 20:45:32 GMT', 'model': '00a5c1f6-c938-41f0-8cd4-b204e928a80f', 'model_id': '00a5c1f6-c938-41f0-8cd4-b204e928a80f', 'name': None, 'parent_model': 'f23ef93e-0347-48f4-9cef-80d2076412d8', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}, {'committed_at': 'Tue, 22 Apr 2025 17:19:35 GMT', 'model': '0068194e-63cd-4f17-b5fa-e7c26a783f04', 'model_id': '0068194e-63cd-4f17-b5fa-e7c26a783f04', 'name': None, 'parent_model': '6cffdeb6-7e22-404f-a233-d1420ecdbb5c', 'session_id': 'bf120fbd-ff81-4f3a-a48d-08ec49a22098'}, {'committed_at': 'Tue, 22 Apr 2025 20:53:18 GMT', 'model': '0062caea-b67a-49ec-933b-8325a5ceebc7', 'model_id': '0062caea-b67a-49ec-933b-8325a5ceebc7', 'name': None, 'parent_model': '34518709-7d79-452e-822a-b24ad08a2718', 'session_id': 'b335c0c4-084e-4396-9d6c-a7c03faa88bc'}]\n", + "Model id: ff5678e8-d091-4b10-84aa-20f14176d9b3\n", + "0.8901\n", + "Model id: fe1b5457-23d6-481d-b021-35294ee4d664\n", + "0.90005\n", + "Model id: fd85fd68-8fae-4a90-a27c-aa23ab913c84\n", + "0.88835\n", + "Model id: fc026787-10ac-41e8-af8f-b08b2058628b\n", + "0.87085\n", + "Model id: fbfa18df-716a-482d-a264-fa8bdba5f63e\n", + "0.8945\n", + "Model id: fb875eae-c14a-4437-9cb2-46881756d527\n", + "0.89685\n", + "Model id: fa7b76dc-ce39-4e53-a80d-63eeaafd4589\n", + "0.9003\n", + "Model id: fa618464-4ea6-4a4f-a521-3ec8611f3179\n", + "0.90025\n", + "Model id: fa52fcd6-3799-422e-8281-46b1547040ed\n", + "0.885\n", + "Model id: f82ed811-8879-4b49-9f0e-47ebda9a3902\n", + "0.89315\n", + "Model id: f74785bd-ab87-4aaa-836f-6e93fc28245f\n", + "0.88215\n", + "Model id: f73998dc-dbd2-4907-9730-4c9b803c6b00\n", + "0.88355\n", + "Model id: f72642e1-3169-4eca-943f-a5b6a8934f29\n", + "0.8922\n", + "Model id: f6843d85-a65f-4119-9f9f-28ae34dbab44\n", + "0.8918\n", + "Model id: f601f7ae-34df-4e44-b38e-ce3c31ac1603\n", + "0.89595\n", + "Model id: f4937451-6a01-453b-918d-f5c32c638d3d\n", + "0.89125\n", + "Model id: f464429d-b8c4-4186-8886-ef99a9c14b0c\n", + "0.9006\n", + "Model id: f44d4811-b524-4d9c-8f2e-01b3e087dd50\n", + "0.872\n", + "Model id: f3c56650-5d74-4bd4-92be-3b31ec1437fb\n", + "0.90055\n", + "Model id: f3b456cd-b8f2-4f05-ab26-b153095c765a\n", + "0.8859\n", + "Model id: f38c1231-1104-4258-90fc-96393ba79207\n", + "0.89945\n", + "Model id: f38a00ee-3ba0-4b1f-a0c0-d0bb1d54b91e\n", + "0.8759\n", + "Model id: f369ce9e-fb75-4fb7-892f-4983b827aaba\n", + "0.9005\n", + "Model id: f2561655-de53-4ed6-9d6a-1a9614b8a721\n", + "0.8821\n", + "Model id: f23ef93e-0347-48f4-9cef-80d2076412d8\n", + "0.8905\n", + "Model id: f23db284-9a07-4665-a9a3-dd69f2b6dc62\n", + "0.89265\n", + "Model id: f1ea5492-cca8-4bcb-a4fe-5f52d2592b62\n", + "0.89265\n", + "Model id: f1ab8576-570c-4f9a-a05a-ce4936330cc1\n", + "0.87385\n", + "Model id: f154b375-b563-4d7c-b73a-7b842d2846a6\n", + "0.9005\n", + "Model id: f064448a-c863-4c28-ae03-8472e8e96b5b\n", + "0.83185\n", + "Model id: f026363e-d0df-4274-af52-44e3f3adfab7\n", + "0.85965\n", + "Model id: efa5418f-d670-495b-8bec-8a298069c200\n", + "0.88975\n", + "Model id: ef4c64a7-41fb-4798-8662-852a99169968\n", + "0.88965\n", + "Model id: ef0627ab-e649-4fba-b2b7-62c64e916e0a\n", + "0.89195\n", + "Model id: eed875ba-0c5e-4e72-84bf-2f7cd2818616\n", + "0.89585\n", + "Model id: ee11559f-a6aa-46cf-8c0e-ce6aadcd36b5\n", + "0.8893\n", + "Model id: ee0780cd-6819-48a2-ab3f-2cfd536125b9\n", + "0.8838\n", + "Model id: ed7d4446-4361-4923-9f7e-c662c07f32c5\n", + "0.85735\n", + "Model id: ecea315a-c00e-47a0-897e-6bc6784438c4\n", + "0.8922\n", + "Model id: ebc02fbb-23cb-47ab-99da-4cfc0c89d187\n", + "0.8795\n", + "Model id: eb4c4784-c1e1-4015-b943-3d720e30662b\n", + "0.8711\n", + "Model id: e99b6e2e-7b9d-49ac-b40b-9920328028c1\n", + "0.89795\n", + "Model id: e90e788a-927d-4683-a02d-83c9de1d8b80\n", + "0.8693\n", + "Model id: e8b75195-f7ef-4ae2-bdae-e93591d48030\n", + "0.8605\n", + "Model id: e89be0b2-deeb-4079-b69f-223a9600e727\n", + "0.48775\n", + "Model id: e8305bff-cb91-4e0a-92e9-9e112a32602e\n", + "0.8981\n", + "Model id: e8161d73-02c6-4432-bb48-46ccd98e9b46\n", + "0.88155\n", + "Model id: e76cc2d8-eda1-4e51-ab19-908073decaec\n", + "0.89705\n", + "Model id: e6b7a0d8-4a35-46ba-a168-d07481768d42\n", + "0.8775\n", + "Model id: e6907a1b-358d-4c05-8d8e-570a55d54fb1\n", + "0.81025\n", + "Model id: e677469f-a332-43c6-ae5e-28c86c83bd12\n", + "0.89605\n", + "Model id: e626adf3-78b6-4659-ab8d-58756a64492b\n", + "0.8976\n", + "Model id: e61b7d82-d940-488d-b538-2c22a3a6aa79\n", + "0.8951\n", + "Model id: e57d785e-3100-458f-a27e-c2623c403e7b\n", + "0.90055\n", + "Model id: e5562c5f-8f08-4e3a-a02b-231bfbc2d7d9\n", + "0.8512\n", + "Model id: e5517996-7b1b-4785-be2c-269cfac390bf\n", + "0.8983\n", + "Model id: e476a094-0fbb-4b0c-8cef-a32f3ae07135\n", + "0.8938\n", + "Model id: e404e033-3855-4613-aae1-33635468e9d8\n", + "0.8991\n", + "Model id: e3e8887d-38be-4320-b6b8-c883d45b1cd6\n", + "0.89505\n", + "Model id: e2b615d9-5466-49e3-8dca-a4acb2658b3c\n", + "0.5768\n", + "Model id: e187f02b-091f-41e8-8403-e07a1b879d33\n", + "0.9005\n", + "Model id: e1269ed8-2950-42d6-a4da-92adf9bc71f5\n", + "0.8918\n", + "Model id: e09fcfb1-b66a-438d-b139-a1441be05a23\n", + "0.8917\n", + "Model id: e041fa07-c4c1-4f4e-b30a-2d0a5ad8d7d2\n", + "0.8927\n", + "Model id: e02b1786-0d08-4d89-9199-ec6dc4e1e151\n", + "0.86695\n", + "Model id: dfbee681-261a-4d40-8589-8fe60f05d1eb\n", + "0.87485\n", + "Model id: df25ec02-f8ac-42a7-a14c-7cfa86ef3fc3\n", + "0.89505\n", + "Model id: de4b37dc-4eb6-4fe8-9e78-d3b95dd6b976\n", + "0.8864\n", + "Model id: dde40753-37dc-4917-be82-b70b9e0203b0\n", + "0.89475\n", + "Model id: ddc8d2ad-4113-4ab4-be8f-bacdcf910ef3\n", + "0.89075\n", + "Model id: dd6bb260-b20d-4ee0-9903-f728de991671\n", + "0.90065\n", + "Model id: dcad8dd7-4477-4773-81a9-91eed944c098\n", + "0.8935\n", + "Model id: dc383596-46a7-468a-b44e-3995ee5d9ad6\n", + "0.82275\n", + "Model id: db9edf85-ed30-41e1-8ae8-81f60e472851\n", + "0.8867\n", + "Model id: db3525c9-f410-4979-8eef-a514c50f8334\n", + "0.87895\n", + "Model id: dadc9d4f-d244-4e3d-b606-665426415307\n", + "0.85945\n", + "Model id: da5d1cb1-30a9-4b81-95e3-5c00e1589c22\n", + "0.9002\n", + "Model id: da34a6e5-dc28-4b15-b423-f46c0da3d99c\n", + "0.8929\n", + "Model id: d93f0553-a98c-4be5-be31-2720bec70a1e\n", + "0.9008\n", + "Model id: d934e8b2-4906-4300-b345-11e810c20391\n", + "0.8966\n", + "Model id: d909f93e-495a-4d87-a088-db8eeab6e995\n", + "0.86505\n", + "Model id: d8c8a6da-06f3-408c-8ece-567ac258c8b5\n", + "0.89865\n", + "Model id: d89e7f71-9a6a-4b81-87c9-cf0ee5e4affa\n", + "0.9\n", + "Model id: d75d631b-98ec-45a1-bef8-2e7f959cfff1\n", + "0.8998\n", + "Model id: d5c71e06-9a4e-48b4-a9dd-dca87097780f\n", + "0.8357\n", + "Model id: d5095189-5d45-48f1-b5b0-c887c9422759\n", + "0.89335\n", + "Model id: d501414c-d868-44d2-a949-592d9eabd909\n", + "0.87875\n", + "Model id: d4e60743-f50a-4cc9-9a29-dfdbd242805c\n", + "0.85705\n", + "Model id: d4d705f7-63da-4155-93bf-42ab84bd2db9\n", + "0.89135\n", + "Model id: d47d06d0-b827-4f73-8a4d-6509cee4dfff\n", + "0.8751\n", + "Model id: d45582d3-681a-40c8-ac15-e2db3f3c1d0e\n", + "0.89445\n", + "Model id: d414ecb5-a304-46be-8d8e-a59587d23160\n", + "0.88085\n", + "Model id: d3fe9f7e-39f9-4ab3-ad26-4af509dc0b8e\n", + "0.9003\n", + "Model id: d2628817-4a8b-4cb1-9044-51a3106aa792\n", + "0.8823\n", + "Model id: d13a4d8d-d87d-49da-b03d-a08c975b58f4\n", + "0.89645\n", + "Model id: d12efb27-2c5e-49b3-b4f5-c2fb98581660\n", + "0.8901\n", + "Model id: d0b3fbd1-f648-4aac-a17a-4e24437c35bb\n", + "0.89125\n", + "Model id: d05c2640-2360-4601-9503-a846444306b5\n", + "0.86145\n", + "Model id: d03fbd31-58ec-4e9f-9794-c8d89f812aa4\n", + "0.90025\n", + "Model id: d024c6a5-1587-4821-ba51-af19f529301f\n", + "0.8924\n", + "Model id: cff77a36-05e1-4654-b64d-73337d1179ef\n", + "0.68675\n", + "Model id: cf707d6a-13b3-4677-b8eb-71aceb9ab3a3\n", + "0.8929\n", + "Model id: cf0cc961-7aec-41bc-aa11-f258c62d6db9\n", + "0.89515\n", + "Model id: ce8e3a95-f6d5-4fae-9e94-8ef6ca003e19\n", + "0.85585\n", + "Model id: cdd359ae-ed92-44e7-a93a-4b2aac5165bc\n", + "0.90055\n", + "Model id: ccb51938-06c1-405d-ac00-6c08a34a8845\n", + "0.88545\n", + "Model id: cca1105a-e979-4aae-b1de-abf0d6ce45c0\n", + "0.8888\n", + "Model id: cc5abf0f-9c3a-48f6-a74a-463a99f6249a\n", + "0.89285\n", + "Model id: cc200b7d-cf83-4167-9a27-e46b11b2948c\n", + "0.89025\n", + "Model id: cbe0ac6e-2058-4a8f-be1d-61da590f5a1b\n", + "0.8942\n", + "Model id: cb8cf515-15be-4457-a15e-a1b04fde924e\n", + "0.9006\n", + "Model id: cb237bd2-66ec-42f9-b934-f50b87ff89f4\n", + "0.8902\n", + "Model id: cb142ecb-905d-4d5f-b708-e29bd9e37341\n", + "0.89835\n", + "Model id: cb106dbc-2406-43eb-9c50-3e175f1ebb0f\n", + "0.892\n", + "Model id: cafc3f96-a7ff-4f32-b0a0-b8b201b112d9\n", + "0.9005\n", + "Model id: caa820c7-8787-480a-b0f7-81fbe6c6d9b7\n", + "0.83445\n", + "Model id: ca494a0b-e774-4d57-a79c-ce6e3cb7a04b\n", + "0.8817\n", + "Model id: c949b10a-2baa-4d20-afc6-a99a3b5af1f3\n", + "0.8908\n", + "Model id: c92a477f-f09b-425d-8a7d-5946b32166b1\n", + "0.83\n", + "Model id: c91a1859-6640-4f33-9856-13a421b77774\n", + "0.8912\n", + "Model id: c89524a2-4e1e-4938-9d6f-18548adec203\n", + "0.8781\n", + "Model id: c80317f3-fc8c-49c7-a76c-a6390fcd1e5c\n", + "0.87455\n", + "Model id: c79ec909-1e84-4f31-8125-ed9d78e2b930\n", + "0.8937\n", + "Model id: c719be14-1696-4b47-9966-15cff5b3ac80\n", + "0.87645\n", + "Model id: c661d4bc-7785-4293-bfa4-99f03920b6cd\n", + "0.8946\n", + "Model id: c65a3db1-8441-43b8-9750-d78924989746\n", + "0.87715\n", + "Model id: c4d15b8b-6b82-4def-a91b-0acc07d42a96\n", + "0.90065\n", + "Model id: c497c0ea-6cbf-48d5-8ac4-22a784aaa59c\n", + "0.89635\n", + "Model id: c404f913-0fb4-4ebd-ad68-f797fe49b7eb\n", + "0.89995\n", + "Model id: c3363d47-936f-44e7-9a05-6b345bb179da\n", + "0.81525\n", + "Model id: c329368e-4124-4e4b-9749-2deec9b522a7\n", + "0.8872\n", + "Model id: c30f3b50-112f-4b94-8f0c-ded494d3e6ed\n", + "0.8915\n", + "Model id: c2f217d4-7133-46e6-aabe-fac076e0bd80\n", + "0.89915\n", + "Model id: c1dcd79c-e72e-4579-ae1d-7c1aaa5ac4a6\n", + "0.8942\n", + "Model id: c1964f4d-c1c2-431e-9f41-f36c79fb8950\n", + "0.8915\n", + "Model id: c15f13e0-7295-4a6f-86dd-ab1a74416ecc\n", + "0.88105\n", + "Model id: c1522204-4a16-4bc1-a7c0-dc44f188ab6a\n", + "0.89265\n", + "Model id: bfe56534-d58e-46a2-8857-0417159420b0\n", + "0.90095\n", + "Model id: bf76514a-5f48-47f7-85be-b0fe6d1de8ec\n", + "0.8619\n", + "Model id: bf16d524-25d1-4835-ac7f-69c8811cfd28\n", + "0.9005\n", + "Model id: be8cf200-4aa0-4f63-9ce6-6ba377f04b80\n", + "0.89565\n", + "Model id: be8891ea-41ae-4f42-ab6b-b224b2dbaa91\n", + "0.89075\n", + "Model id: be6e1d85-1738-4281-9997-35e97e6ca7cb\n", + "0.76205\n", + "Model id: bd67f2c7-a640-4194-8820-f3257327ca48\n", + "0.891\n", + "Model id: bd04d093-bfb8-4589-9a3e-e784e439523b\n", + "0.8996\n", + "Model id: bc1d2ac9-6b62-46d8-a8e2-ab99b440ee5d\n", + "0.8911\n", + "Model id: bbb407dd-3075-462b-b866-ff85ba76ad37\n", + "0.8619\n", + "Model id: bafbd518-5189-4dc5-bdf0-6c4d4ff39d95\n", + "0.86525\n", + "Model id: bac98b07-6b49-4f4f-9725-a59edf6dc9b2\n", + "0.90095\n", + "Model id: ba765d36-1375-4666-839e-f6403e192821\n", + "0.8988\n", + "Model id: ba361877-a75e-4319-af75-dd983bc1d8fa\n", + "0.9005\n", + "Model id: ba33aadc-54f7-4c3c-a2d6-3541403dda41\n", + "0.87435\n", + "Model id: b993160d-d164-4dcc-a28c-2debc506515f\n", + "0.8938\n", + "Model id: b8d6b599-f30c-45e7-ac15-e383a95b565e\n", + "0.8833\n", + "Model id: b865f83b-6c51-4da1-a8ee-21d4b9cd4683\n", + "0.87665\n", + "Model id: b66247ea-304f-4059-b594-ec7f638972af\n", + "0.8903\n", + "Model id: b653f40b-8346-40e2-bf0f-52e52b00711c\n", + "0.8998\n", + "Model id: b519702f-72d1-429f-bdd2-d33e8b44dbe1\n", + "0.89365\n", + "Model id: b42115c1-c9d7-446a-bfea-dc17623dcb90\n", + "0.8852\n", + "Model id: b41cc615-7aa3-4a06-8bed-378bf071f194\n", + "0.8798\n", + "Model id: b31ff2a6-88af-4667-ba73-5c53213edba9\n", + "0.8986\n", + "Model id: b2f62c2e-b5a9-45ce-9ea5-14976248fefd\n", + "0.90075\n", + "Model id: b17e6767-a701-4fc6-b9d8-19bff83d7025\n", + "0.87765\n", + "Model id: b12871ec-b2a2-4207-8150-790d7b28e71a\n", + "0.88575\n", + "Model id: b03e0721-b683-4c58-be3d-e79aa3c48ea4\n", + "0.8991\n", + "Model id: afcbb07c-efad-49b7-8e24-92793659b92e\n", + "0.8985\n", + "Model id: afacb56f-f001-4967-9b5e-19fe0b576e79\n", + "0.9007\n", + "Model id: af38ca86-443e-4b90-9720-19dff28f6cee\n", + "0.88605\n", + "Model id: adec5bda-29db-4018-ba44-ec8321dbe595\n", + "0.847\n", + "Model id: ad3797b9-6a62-4fba-bed6-34e4f03bf2d8\n", + "0.89695\n", + "Model id: acff391a-fa15-4dc1-9d78-7666117b5f0d\n", + "0.85865\n", + "Model id: acfe1d3d-de19-4c88-abb2-442841add432\n", + "0.87695\n", + "Model id: aceb722a-2fdf-4982-9ae3-9843027b79de\n", + "0.87425\n", + "Model id: ace3aaff-fe20-42f0-9b33-07d4e95da9c6\n", + "0.8762\n", + "Model id: acbe88e3-e16b-483f-b2ae-dfa3092faaa6\n", + "0.90005\n", + "Model id: ab82c74d-e8f2-45ad-8a0b-ca6e3388ce4a\n", + "0.89405\n", + "Model id: aaeecdd5-3bfb-4e19-9167-a4a5f9bffa10\n", + "0.8857\n", + "Model id: aaeafbfd-fb4f-4ad3-b003-90c96c180419\n", + "0.8786\n", + "Model id: aacb592f-3d84-417f-bfe7-d832ecf31899\n", + "0.7025\n", + "Model id: aab098f1-bda5-49e3-8787-5ef259aabf14\n", + "0.8714\n", + "Model id: aa5b654d-9655-4f5c-92c2-973b04f6499f\n", + "0.89125\n", + "Model id: aa43e35c-a5e1-446e-9971-119f716e5097\n", + "0.9004\n", + "Model id: a989e55e-18d1-4226-8010-ccee096be054\n", + "0.87465\n", + "Model id: a917860b-bcbe-4108-b888-5b9ac268149e\n", + "0.8952\n", + "Model id: a8e862a0-e7b1-4383-a29a-98ac5fa6f952\n", + "0.88725\n", + "Model id: a825f62e-16d2-4e3a-aacc-b9278aebb858\n", + "0.88805\n", + "Model id: a7ef9152-70d3-4df8-bae0-7c3cc2bdf5e5\n", + "0.8995\n", + "Model id: a7d74f0d-1864-4f19-a26c-034ac06fee24\n", + "0.8884\n", + "Model id: a79209fe-1675-4772-b1bf-52f656227d3c\n", + "0.88005\n", + "Model id: a7029d70-c444-42d3-90e2-709f67cd7dd3\n", + "0.8975\n", + "Model id: a6e7102a-a581-40eb-b963-b1b1d63fdc9d\n", + "0.89815\n", + "Model id: a6930266-f3ab-45c9-acd3-36b9321adbd1\n", + "0.89665\n", + "Model id: a639ab0a-eec1-48fc-85b2-03d1d06e0e9f\n", + "0.87325\n", + "Model id: a4cb2d36-ba87-4d77-bf05-0c3ab580e2c4\n", + "0.8879\n", + "Model id: a48596e8-84dd-487b-a087-4fa844596331\n", + "0.89725\n", + "Model id: a41506c9-49c7-4173-a6e4-8412d0dbe6bb\n", + "0.8944\n", + "Model id: a413f162-ce2c-4b42-b635-3bc38a40842a\n", + "0.8993\n", + "Model id: a29b6232-55a3-492d-8a7c-91de7bc167da\n", + "0.8947\n", + "Model id: a29abcb6-4316-4cd2-96ca-53ebaa4030bf\n", + "0.89685\n", + "Model id: a283e30d-8daa-41d3-9c28-2d6db5383fcf\n", + "0.88515\n", + "Model id: a27babc5-907d-4a6d-9a08-555c1389a39a\n", + "0.89635\n", + "Model id: a1fed732-be5c-4916-815f-687183079e24\n", + "0.8667\n", + "Model id: a1fd8545-98af-4696-850d-777c866b3ffd\n", + "0.8982\n", + "Model id: 9f5b13e7-adb9-4dce-8b8c-243e2b5e09de\n", + "0.88995\n", + "Model id: 9f2b2440-0916-4e56-879a-e21f107cbbee\n", + "0.8937\n", + "Model id: 9e5c722c-191d-4816-8458-b7df80e64a8c\n", + "0.8941\n", + "Model id: 9e584841-255f-4103-ab0b-a53964e054e7\n", + "0.8953\n", + "Model id: 9e21e9d1-bd09-4de8-9de1-5b1763037424\n", + "0.88575\n", + "Model id: 9e116ea6-8a55-45b4-80c2-ca86ff6bc0d5\n", + "0.8947\n", + "Model id: 9e0a3195-0a68-45f8-9075-306adec03b00\n", + "0.89605\n", + "Model id: 9dfdf095-5e33-4e9a-8c99-75771050d711\n", + "0.893\n", + "Model id: 9deceb63-de19-4602-9593-eeed272cfe15\n", + "0.891\n", + "Model id: 9dc9005c-d247-4b50-a23e-6ea3cf7b1ddc\n", + "0.895\n", + "Model id: 9da045b7-0124-4453-9693-3e9cfcff4081\n", + "0.87365\n", + "Model id: 9d5e2dfb-1491-41d7-9bdb-4e7fcfbe4bda\n", + "0.8932\n", + "Model id: 9c5ece57-0fab-499a-a36c-4996a1f520f7\n", + "0.83665\n", + "Model id: 9c14d812-f4c5-406e-970f-9ee120e0cfd5\n", + "0.9005\n", + "Model id: 9bf02e67-7362-40fb-b546-66c192cf1058\n", + "0.8916\n", + "Model id: 9a8488af-b74d-440f-bdab-5cf48435fd04\n", + "0.87925\n", + "Model id: 9a67813a-1067-4645-9b8f-4eac84fb0dc7\n", + "0.89875\n", + "Model id: 99ac7fd1-c7a7-43d5-9b4e-9a548ae6d35d\n", + "0.87395\n", + "Model id: 997981d2-7e9e-46fb-a3dd-1ab7fa80ce96\n", + "0.8809\n", + "Model id: 990d8d8f-fae0-48f8-8ff2-a67589a5c439\n", + "0.87845\n", + "Model id: 9902acd5-2a4b-42c5-96e8-4cc4799a2f7b\n", + "0.8936\n", + "Model id: 98f07390-0fd4-4a40-8606-2a4b5e5d2273\n", + "0.895\n", + "Model id: 988bb218-028c-4f2e-b31f-881168dca63a\n", + "0.8903\n", + "Model id: 98384026-3764-4437-a98d-e48d06774ec9\n", + "0.88055\n", + "Model id: 97aba36f-3c07-4ceb-af7c-f5e893959e38\n", + "0.90095\n", + "Model id: 97686b04-003b-4112-89df-bafae9d14093\n", + "0.8844\n", + "Model id: 97082d29-49e3-4560-bcfd-5f0d89b510f1\n", + "0.8914\n", + "Model id: 96129f90-6ef0-4bd7-a9eb-25e121dc2d21\n", + "0.8638\n", + "Model id: 95c8c8cd-156c-4879-8642-b3bf21efe052\n", + "0.87955\n", + "Model id: 952386fc-4163-4b32-a2b6-fc2378e4d044\n", + "0.89125\n", + "Model id: 942f6bb8-1e4a-4dc4-a2e2-c15629abc0cc\n", + "0.8967\n", + "Model id: 940695d2-6e72-42ee-a11e-1718b5ed9199\n", + "0.8923\n", + "Model id: 940624ec-412f-4a10-acd3-d38a6aebed2b\n", + "0.8937\n", + "Model id: 93df3d4e-18ac-49c4-b512-3d8805d695fe\n", + "0.8867\n", + "Model id: 93cc1df9-a616-49be-9cd5-abbcdcb8575c\n", + "0.892\n", + "Model id: 93aa1685-48cf-4cb6-81b2-2b401ff266a4\n", + "0.87755\n", + "Model id: 92b2c175-156d-40a0-9253-8448bfbca308\n", + "0.89565\n", + "Model id: 91d59105-06d8-4167-8d0b-402fc6beae75\n", + "0.8895\n", + "Model id: 917cbea1-cc02-4cd7-9ab2-ae5aa8eb2744\n", + "0.8995\n", + "Model id: 9155dd7d-41be-4515-9b3d-1b65b1bddb2b\n", + "0.8963\n", + "Model id: 914f8068-0df6-4def-8d5d-30d5b72e1449\n", + "0.8997\n", + "Model id: 90e834e8-9b17-4c4b-b83b-95824f0a8d8c\n", + "0.87125\n", + "Model id: 9091c458-cd5f-4a3e-805f-384772f8d611\n", + "0.8928\n", + "Model id: 9070e73d-7dab-4e9e-8bf0-796162cb1ac9\n", + "0.89545\n", + "Model id: 906ee073-ae00-44aa-a9f4-0e2ecadfb18e\n", + "0.8914\n", + "Model id: 903f2f4c-d080-4a9e-aa3a-daa7f83eb903\n", + "0.89345\n", + "Model id: 8fa9728d-fc1f-47f3-adad-b05d7f7c7435\n", + "0.89185\n", + "Model id: 8f80d11e-524c-44fd-8c66-46b506280df5\n", + "0.82595\n", + "Model id: 8f690c79-6213-4ae3-83a9-a6dba1d786f1\n", + "0.87135\n", + "Model id: 8ecc86cf-19ed-49f4-bd02-76354c8850d2\n", + "0.8986\n", + "Model id: 8eb5797d-ac23-413b-a932-6fed86749e88\n", + "0.89245\n", + "Model id: 8e64041d-e896-4ae8-9836-393f2940099b\n", + "0.89825\n", + "Model id: 8d8a7bef-e6a5-4003-9f4e-44fd15d2a4d6\n", + "0.8906\n", + "Model id: 8cffa60c-36b2-4857-b079-f6e8de502334\n", + "0.8864\n", + "Model id: 8cb36005-e8bd-4c76-885a-bfa834b1f1b3\n", + "0.88795\n", + "Model id: 8c6e3f38-04ef-44f1-b2cf-61c123b8fda2\n", + "0.87235\n", + "Model id: 8c31c26c-80a3-48cb-9aa9-87ee58afe60b\n", + "0.887\n", + "Model id: 8b88d2a9-847f-4e52-b1a6-e62c63b91984\n", + "0.88365\n", + "Model id: 8b002342-5460-4384-b194-9be724274744\n", + "0.89925\n", + "Model id: 8aff477e-1f21-4544-aaf0-99bb2c6631da\n", + "0.9007\n", + "Model id: 8a9601b6-2a4f-4c25-a0cd-a72b6f191871\n", + "0.89755\n", + "Model id: 8a83cc4c-7551-4261-ba55-e7b0201df89f\n", + "0.8419\n", + "Model id: 8a06c8df-dc37-4777-b60f-802f0c7628cb\n", + "0.90055\n", + "Model id: 893627c6-cab5-4f27-98f6-709fee0aec22\n", + "0.89575\n", + "Model id: 8935d372-7f24-4eb8-9fff-ad2492379d40\n", + "0.9002\n", + "Model id: 89287967-1617-40d5-adef-00dbdbe6743e\n", + "0.9\n", + "Model id: 888ed478-ce90-4cab-ae30-59e200cd0dd8\n", + "0.89465\n", + "Model id: 88536d97-62d1-4833-8e55-b6a55c87cbb5\n", + "0.87475\n", + "Model id: 879ddd7a-34cc-4a86-b717-0e4445b9d708\n", + "0.8676\n", + "Model id: 875f5a50-7653-48d5-9a43-8615f6ae7f47\n", + "0.828\n", + "Model id: 874ae2d7-3fb6-4d37-8083-5b96a95ef016\n", + "0.8904\n", + "Model id: 873334c0-390a-4d76-81c0-c87a7c21d244\n", + "0.89835\n", + "Model id: 870406a8-2a4a-477f-85a2-195d445183bd\n", + "0.8897\n", + "Model id: 86dbb66d-af42-4cf2-b07c-ea6cb158f1e0\n", + "0.893\n", + "Model id: 85e44978-ec1d-4e8a-bc58-37b0dd364f44\n", + "0.8753\n", + "Model id: 85cf4f2b-652b-42e2-a970-897facf189f8\n", + "0.8917\n", + "Model id: 85a18809-19ab-487a-90df-5b35f33f9f83\n", + "0.89775\n", + "Model id: 85690a5d-6617-4551-9cda-b82aab6a3d96\n", + "0.88325\n", + "Model id: 84ee590f-dcda-4e27-9e42-9f11cfc03a78\n", + "0.87775\n", + "Model id: 84e5445e-d023-41a9-b1a0-b08c050cfdb4\n", + "0.8572\n", + "Model id: 839c278c-506a-4bc2-9d32-ada1bc987c92\n", + "0.8869\n", + "Model id: 838f401c-494b-4f90-ac6b-b4fca5d2abb9\n", + "0.8732\n", + "Model id: 82fad5ba-6747-4acb-95dd-6ea9ddef58f8\n", + "0.89465\n", + "Model id: 82e1a19f-7308-4364-89fe-b87cc7d85aa9\n", + "0.83875\n", + "Model id: 82e19754-f92d-4ed9-a3a1-fa869126f717\n", + "0.89495\n", + "Model id: 82c38af2-d39d-43fb-b79e-a4fe05ccea06\n", + "0.89255\n", + "Model id: 82589fd5-52ea-4d13-a84d-216c5637fc00\n", + "0.8954\n", + "Model id: 81f75659-772b-4a5b-b00c-fcae674f4fbd\n", + "0.8785\n", + "Model id: 810d1050-6a01-48eb-b5ec-4f411713e721\n", + "0.8761\n", + "Model id: 80f4b305-b987-490c-836c-f72e8ee28c08\n", + "0.899\n", + "Model id: 80d7a802-3c2c-43e8-af73-2cac9ba9adf0\n", + "0.79445\n", + "Model id: 801e14be-e743-4871-9158-d85b13793333\n", + "0.89485\n", + "Model id: 7ebfd2cc-5ee5-485b-bd3e-798a0db83d8d\n", + "0.88705\n", + "Model id: 7d38faf0-c0f9-4041-928c-4d22fc8cb15e\n", + "0.8874\n", + "Model id: 7c598d23-a398-4310-987d-a8de819b181f\n", + "0.87885\n", + "Model id: 7c4ac221-f6d1-4038-a5a5-d573d6485e3c\n", + "0.89785\n", + "Model id: 7ba76861-2cb5-456a-bc47-0b6efe0c5fd2\n", + "0.90125\n", + "Model id: 7b5bb36c-2343-49d3-842f-458e0f6f5b0d\n", + "0.89175\n", + "Model id: 7b42305d-9ce6-42f7-b15c-4e1d9edc4ac7\n", + "0.89365\n", + "Model id: 7b171a08-75bb-4bdf-a82e-3896cda2171c\n", + "0.8993\n", + "Model id: 78d52b83-55c2-4751-a841-6835f0b2c1cd\n", + "0.856\n", + "Model id: 783f15c1-8b99-4a07-a353-9cf324cc8fc0\n", + "0.8971\n", + "Model id: 77b959ef-6c15-4b87-b9c4-5f229f96ed9e\n", + "0.85775\n", + "Model id: 779ca47d-8269-45ee-903e-aef988a9d28b\n", + "0.89645\n", + "Model id: 775a5e34-e4cd-429e-a001-8c2050818966\n", + "0.5051\n", + "Model id: 7706c971-8265-4bfa-9878-b27667029a74\n", + "0.89675\n", + "Model id: 767d3df1-b3f2-453f-aa12-26b7f7728216\n", + "0.89765\n", + "Model id: 76185288-e40f-41a8-8470-4ca34caf2c33\n", + "0.89885\n", + "Model id: 75b5c7d5-bf82-4ec9-b149-f95eafcee9eb\n", + "0.88425\n", + "Model id: 7529ea28-024e-419e-a1d9-76c546f5f76c\n", + "0.895\n", + "Model id: 74cfd7c2-1a63-420c-be0e-f7609e8917fd\n", + "0.89865\n", + "Model id: 749729a6-77f2-4d6a-8d47-bc9f21f09651\n", + "0.90025\n", + "Model id: 73fd821b-80d6-4892-b870-c6e291d1892b\n", + "0.8979\n", + "Model id: 73500632-669b-4d17-adc7-5e38493e7f20\n", + "0.90075\n", + "Model id: 731e41d1-a917-426d-8c76-a303ca18498c\n", + "0.8637\n", + "Model id: 72f3746e-04bc-41f7-b47e-a660b80f92e5\n", + "0.89635\n", + "Model id: 72922f7f-8238-4a7b-97ec-2b66fb104bfd\n", + "0.86665\n", + "Model id: 72392897-ac2b-443e-8244-bb2a829e78aa\n", + "0.8765\n", + "Model id: 7130c73d-ecd8-4e1c-9583-15fd69430b5b\n", + "0.89365\n", + "Model id: 70fce658-518e-4b65-9707-7929fd321e2d\n", + "0.89175\n", + "Model id: 704395e9-7482-4026-bf2b-01efe9c440aa\n", + "0.88455\n", + "Model id: 7013ab7b-9f7a-45ed-a14e-0cfb10dd1bcb\n", + "0.8859\n", + "Model id: 6f0002c3-0c30-4229-acb1-46726e1d5928\n", + "0.87015\n", + "Model id: 6ef23645-13d1-45e3-8c31-56a8aa3769af\n", + "0.8947\n", + "Model id: 6ee6c5d2-2ff3-4ccf-a184-01b08267c4fd\n", + "0.88255\n", + "Model id: 6ec95010-aa3a-4eee-b3c8-c57574f27839\n", + "0.8772\n", + "Model id: 6e990e93-8a32-4877-a998-c56b6d317024\n", + "0.8605\n", + "Model id: 6e412898-bbb0-4eaf-a4fe-86fee3029297\n", + "0.87385\n", + "Model id: 6e1733f4-7658-41a9-8746-1ef74e4383cf\n", + "0.8983\n", + "Model id: 6cffdeb6-7e22-404f-a233-d1420ecdbb5c\n", + "0.5046\n", + "Model id: 6cf95fe6-0f72-4b42-9dc5-be038e939ae5\n", + "0.86555\n", + "Model id: 6cbf7e71-a0ce-48c7-9dc0-38c70ac0f813\n", + "0.879\n", + "Model id: 6c8b6c84-884f-40d8-ac50-3f16039f1e9e\n", + "0.8994\n", + "Model id: 6c0a5fa3-bf70-4b73-89d4-34d001d3ee53\n", + "0.89095\n", + "Model id: 6bc5a56d-b68f-4fdc-8aa2-bf7e29aace96\n", + "0.8746\n", + "Model id: 6b718264-4d61-4faf-83c6-8cec696fd260\n", + "0.85825\n", + "Model id: 6b3a5a2f-75c7-4e66-ab8a-f310f67a60a2\n", + "0.88045\n", + "Model id: 6b2c86e8-176b-417d-9c5e-88baafff4298\n", + "0.89555\n", + "Model id: 6b11c40f-bc2d-4ce0-941c-3f2fc6f6ff70\n", + "0.89235\n", + "Model id: 6a019676-ba35-4caa-833b-2080ed2b18ad\n", + "0.89385\n", + "Model id: 69705d84-4ec7-40c6-a555-c7e22f76f471\n", + "0.8997\n", + "Model id: 695aac77-5579-4359-a6e0-0263d4969af1\n", + "0.8987\n", + "Model id: 66a78b86-0704-46e1-87b0-eabc0383ea46\n", + "0.83755\n", + "Model id: 65a7dac3-63f3-4502-a49c-63f6b9d3a83e\n", + "0.87485\n", + "Model id: 6557e769-7188-4448-9d87-e83bbb990c7b\n", + "0.88305\n", + "Model id: 649306e9-9466-4818-a93d-6af6d2c88eb8\n", + "0.8959\n", + "Model id: 6466d2b8-a03e-43de-8940-73f446360c6e\n", + "0.89095\n", + "Model id: 63f8ab71-574b-45cf-880b-dd3edce96854\n", + "0.8742\n", + "Model id: 6384bf43-0c3a-43de-b865-c95f1ce46383\n", + "0.8984\n", + "Model id: 6379939e-f9b2-4910-ab64-967ad4028511\n", + "0.8839\n", + "Model id: 635a8fb2-2147-4feb-9538-52ab176cd8b4\n", + "0.89465\n", + "Model id: 633b17bb-6fee-4cbd-92a7-ee112cb1a837\n", + "0.88745\n", + "Model id: 62d8f671-6b1d-4307-a37e-4f4107820d23\n", + "0.8995\n", + "Model id: 62d1bf65-53e5-4674-9b51-dc249eab11e5\n", + "0.89315\n", + "Model id: 6215e9c7-cd0c-46af-ab4f-28b2d22d9ffc\n", + "0.90035\n", + "Model id: 61494b3c-8fe0-4fe6-9059-f5f37412d189\n", + "0.89705\n", + "Model id: 6118f91d-098f-4e55-ae65-58c9e36317c0\n", + "0.89905\n", + "Model id: 608b24c1-d7f0-4f25-a4f4-6f0487ffe67c\n", + "0.8871\n", + "Model id: 60557e06-391c-465e-a554-f0e32dc95c41\n", + "0.88385\n", + "Model id: 5fedebaa-278f-4eba-b93f-6bad4c1a0ee0\n", + "0.8835\n", + "Model id: 5f389fdd-561f-49ee-9a03-3e1be9c219e6\n", + "0.9004\n", + "Model id: 5f2b67a3-6a70-4539-9d75-99efadc7a61a\n", + "0.89145\n", + "Model id: 5e898a76-71ac-4a87-8c6f-cd6415fbee72\n", + "0.86875\n", + "Model id: 5e46ecd3-ef5c-4f1e-ab06-4a484eae6edb\n", + "0.89615\n", + "Model id: 5d62bd3f-c674-4361-baf7-8ff3a147d029\n", + "0.8987\n", + "Model id: 5d46651a-0648-47b6-a8c6-03e6c4a99a2e\n", + "0.89805\n", + "Model id: 5d446bec-513e-4238-8ab2-ba8853459519\n", + "0.90005\n", + "Model id: 5d1659d5-984e-4a56-a4e0-c2ac634424b9\n", + "0.8921\n", + "Model id: 5bc906ca-1b99-4486-9a9a-578a2138431d\n", + "0.9004\n", + "Model id: 5bb2c3ec-3dbe-41a1-94b2-c65a5160acb0\n", + "0.88085\n", + "Model id: 5b9d10d0-6408-4dd9-8e0e-5d0bc3cad593\n", + "0.8878\n", + "Model id: 5b94d073-6dc8-4af5-a7ac-9f122d9db6e6\n", + "0.87895\n", + "Model id: 5b7f6e1a-83d8-4089-9377-03976ca14bb8\n", + "0.87355\n", + "Model id: 5ab95eea-381b-4b4c-9965-d8d1f78f6862\n", + "0.897\n", + "Model id: 5a6daf6d-b909-4d65-8566-afe0ed59594b\n", + "0.9007\n", + "Model id: 59e5d4b7-27c1-407f-af2d-6a29ccece154\n", + "0.8907\n", + "Model id: 5958aed2-a398-45b2-8701-8dfeb05ef5ff\n", + "0.8972\n", + "Model id: 591c9abb-adcc-4fa4-9bbb-36d938bedab8\n", + "0.88645\n", + "Model id: 58d5f64b-12f2-42f3-af00-b1653c1a9b2e\n", + "0.8882\n", + "Model id: 5884b08c-c31e-49cf-9b9c-5a96ec93da3e\n", + "0.8966\n", + "Model id: 56cea560-25b7-4eb8-9781-be3a3e3481b8\n", + "0.89205\n", + "Model id: 5685ed0f-b2af-4b0e-81a3-a4240942576a\n", + "0.8883\n", + "Model id: 563c20aa-883c-40a6-87e9-46f706ede8c8\n", + "0.87765\n", + "Model id: 55a19400-37dd-4370-ab10-7d724d8700bd\n", + "0.90055\n", + "Model id: 556c0bb9-b538-4af4-b41e-05cf00cb910d\n", + "0.8938\n", + "Model id: 551c860b-e146-4c55-820c-18f9bfe0e671\n", + "0.90075\n", + "Model id: 54a1a509-d9e9-4d40-81d6-25857f2c60fe\n", + "0.8925\n", + "Model id: 54848fcc-2c4b-4417-bf0a-4bb7cf7944f3\n", + "0.89725\n", + "Model id: 540ed0e2-c498-4934-8faa-590d281b9d7e\n", + "0.8893\n", + "Model id: 53bcaebb-5bc3-4ed7-8dda-67782983f022\n", + "0.8944\n", + "Model id: 53a7cffc-ce7f-4b3c-ad28-8239425fdf5a\n", + "0.89185\n", + "Model id: 533cfc99-1a62-439e-a613-93c0063ababd\n", + "0.8995\n", + "Model id: 5223c505-cf13-45fe-9d11-81d1e604aed8\n", + "0.90075\n", + "Model id: 51f097eb-b1a4-474a-806e-837ca810635a\n", + "0.8929\n", + "Model id: 50c8abb9-6fc7-4f3f-a04f-335a8393789a\n", + "0.8964\n", + "Model id: 50b29a25-12ba-4671-9c4d-9af087be892f\n", + "0.89195\n", + "Model id: 50b02046-2752-4e3d-bf6a-95b095156875\n", + "0.8938\n", + "Model id: 5069832c-361c-4076-8179-97b0d990441f\n", + "0.8337\n", + "Model id: 50136f30-5e8c-47a5-bca7-87ed9a8f8407\n", + "0.9005\n", + "Model id: 4fcc56af-259b-4a52-b702-d83144678490\n", + "0.8938\n", + "Model id: 4fc42295-001d-4fa2-81eb-bcae1702417c\n", + "0.8984\n", + "Model id: 4ecbd3d6-0397-44bd-b9f3-3939a328600a\n", + "0.8974\n", + "Model id: 4ddbf542-c15e-484b-be3e-bcb4128934e8\n", + "0.8801\n", + "Model id: 4c800850-badf-4bb5-a661-0804b0de16b7\n", + "0.8787\n", + "Model id: 4c7d7b60-bdd1-44d0-beef-e98845d49229\n", + "0.90005\n", + "Model id: 4c51b608-b4b0-4e92-ae7c-fb26679b8b72\n", + "0.8918\n", + "Model id: 4bc36877-898d-4f53-871e-991983f20aca\n", + "0.8912\n", + "Model id: 4b9ba400-5ec6-4731-b545-79025ad192d2\n", + "0.89865\n", + "Model id: 4b3875f5-0625-4ae6-8714-08dfa8a6ad25\n", + "0.89745\n", + "Model id: 4ab48fb0-09e1-4470-aae3-05c1b14d13b5\n", + "0.892\n", + "Model id: 4a609ed9-6123-4a8c-9b89-84916563f119\n", + "0.8872\n", + "Model id: 4a45b724-3490-4a50-8be7-5df69e8e7345\n", + "0.88705\n", + "Model id: 4a1f79a2-cb6b-484c-b5bc-98f95b2b659c\n", + "0.8928\n", + "Model id: 4a02be83-a571-4863-b9f5-e5e5bf7470e5\n", + "0.8682\n", + "Model id: 492abd7a-87b3-42de-96a3-a7b4bf7b796d\n", + "0.8845\n", + "Model id: 48f4cfc8-6f7f-49b6-9089-71aadbe27e45\n", + "0.896\n", + "Model id: 48ba8e1a-4ee0-4dd8-84dc-b22b548eb16e\n", + "0.9007\n", + "Model id: 4853bd50-016f-4e7a-a955-b44ef30237c0\n", + "0.8945\n", + "Model id: 47950500-d05b-4f86-8fd0-8aea34fd92b4\n", + "0.8962\n", + "Model id: 4787bcf9-1e01-433f-bfd4-0db7f1e0bd72\n", + "0.9003\n", + "Model id: 475656f4-4b02-4f68-a373-42dd8eccba80\n", + "0.88845\n", + "Model id: 469c5be5-c900-4e1e-a5ae-d1c87f4d864f\n", + "0.84475\n", + "Model id: 4617858c-c8b3-4bfc-8ba8-f4a4dda884f1\n", + "0.895\n", + "Model id: 4595b7f3-0d9b-4254-9759-94bd768438ad\n", + "0.8858\n", + "Model id: 43ef44d9-056c-48b8-857c-291ad5f00f84\n", + "0.8808\n", + "Model id: 43853a66-244a-45be-aa00-1e447689b979\n", + "0.7781\n", + "Model id: 41b07e79-dbbf-4abf-9d83-8fcde4affd2e\n", + "0.89895\n", + "Model id: 41a8c79f-b2e6-4592-b7dc-ad3015a646f1\n", + "0.89235\n", + "Model id: 4174f10f-e5e4-49c7-a5a9-5d5bfec3036e\n", + "0.88885\n", + "Model id: 4166a7c3-f43c-4ed5-81f8-21d217573d78\n", + "0.8926\n", + "Model id: 413a6f0c-7a54-4aeb-9118-afd5a2f79c4d\n", + "0.8645\n", + "Model id: 40e4974e-cf5a-4430-a946-06229865b9fe\n", + "0.9007\n", + "Model id: 40e401ff-b056-47e3-942f-d34789d491e9\n", + "0.894\n", + "Model id: 40a961e4-db23-483f-9af7-202e9393e19f\n", + "0.90075\n", + "Model id: 4022ec90-3eb6-400d-a575-9c62c5d0089e\n", + "0.8981\n", + "Model id: 40219deb-9950-4415-8184-577d212095da\n", + "0.80255\n", + "Model id: 40035636-b82e-4a6e-b798-58b4920e9d5b\n", + "0.89735\n", + "Model id: 3ffbd35e-825a-4eaf-a5c7-c0357674ddfc\n", + "0.88015\n", + "Model id: 3f8e1fef-c3b4-41c6-b569-9ee2ea54cb8d\n", + "0.8947\n", + "Model id: 3ed5545d-f3f7-4798-83ea-921a4546f3e0\n", + "0.89445\n", + "Model id: 3e9197c4-a53b-4f12-af20-f25d3d351541\n", + "0.8916\n", + "Model id: 3dc9eef1-6906-4554-adae-87118ef38f31\n", + "0.87545\n", + "Model id: 3dc7deb3-a85d-4155-b390-bcf2cc890c80\n", + "0.8746\n", + "Model id: 3dc24c19-5af9-42c0-aa1e-c75ef735a653\n", + "0.89305\n", + "Model id: 3cf0d5aa-8f1f-489c-ad9a-abdfa963bc69\n", + "0.8551\n", + "Model id: 3c94c8dd-ce4c-48ff-82eb-197abfb9ffd0\n", + "0.8982\n", + "Model id: 3c6e51e4-98af-4430-8ec5-9a16852a529e\n", + "0.89925\n", + "Model id: 3c4cbd82-168e-4d38-af73-f6f9f93e85d4\n", + "0.89685\n", + "Model id: 3c26e627-a191-4e27-a23f-272be9e9dac6\n", + "0.88015\n", + "Model id: 3bd2b407-297c-40d4-bace-69a2ab74d8ee\n", + "0.8933\n", + "Model id: 3bd0fc40-a93f-4fac-84ac-bc24287c0419\n", + "0.89225\n", + "Model id: 3b2df363-57fd-463e-b49b-1256039aa5a5\n", + "0.90085\n", + "Model id: 38cacea3-7e16-4355-ae27-625720dfa34b\n", + "0.8959\n", + "Model id: 38a7d49f-a699-426e-9f8e-8e0c6a421508\n", + "0.89165\n", + "Model id: 388274fd-b918-40b9-9baf-4d63b87e86a2\n", + "0.89245\n", + "Model id: 37e48780-41be-4827-b03c-444bcc485958\n", + "0.87485\n", + "Model id: 379d64b6-4a2b-485e-aed2-12716d3136cf\n", + "0.8906\n", + "Model id: 3793a688-89a2-4cf8-b863-2b63f3c8eb82\n", + "0.89175\n", + "Model id: 375f219a-e3ac-4ef8-90c3-b6b854ff611f\n", + "0.89925\n", + "Model id: 374b5775-28f5-40a7-9b51-b6677158d0aa\n", + "0.7327\n", + "Model id: 3718c3b8-c62e-4939-b18f-c571d738623e\n", + "0.8999\n", + "Model id: 369f491b-d118-45e1-8c6e-3e3540c14162\n", + "0.88735\n", + "Model id: 366ed4f8-6b79-43a3-b076-ae65ca2918ee\n", + "0.8921\n", + "Model id: 35e05901-4a82-473d-8bef-834d691d016e\n", + "0.8781\n", + "Model id: 35517fd4-5a9e-4be0-bca7-768c46a6f8b6\n", + "0.8953\n", + "Model id: 3546efe3-3659-4e1c-9d0b-d9297b850967\n", + "0.88745\n", + "Model id: 34a8396c-7895-4702-801c-e01a366cbdf1\n", + "0.883\n", + "Model id: 34518709-7d79-452e-822a-b24ad08a2718\n", + "0.89355\n", + "Model id: 33e7cd5c-c016-416e-8f72-08ed9bffe0c6\n", + "0.875\n", + "Model id: 32fbb56c-5a34-4e23-be42-092549562bca\n", + "0.872\n", + "Model id: 32d4eeac-69ee-4067-aa0e-534a2004e55f\n", + "0.88385\n", + "Model id: 32d3ba09-6507-44de-89c3-2cacd9800108\n", + "0.90045\n", + "Model id: 32b970c8-3246-487e-aa39-c75c1ee73c34\n", + "0.88025\n", + "Model id: 31d0d4f0-0c24-4893-b4a2-4b5d5dc63cba\n", + "0.89985\n", + "Model id: 318e2ea5-30ed-4299-89e8-653aa0cecac0\n", + "0.89885\n", + "Model id: 30872c0e-d3ae-4785-8cd6-71d8fe5732d1\n", + "0.9\n", + "Model id: 30320bc1-6bd6-4748-8e70-1e58c019d2b1\n", + "0.8997\n", + "Model id: 2f50e10e-b367-4e7f-a7e0-ba7a22a5af85\n", + "0.8788\n", + "Model id: 2e2889d9-8853-4f33-853a-a17ea992423e\n", + "0.88135\n", + "Model id: 2e09c36e-2ec1-4e49-aab4-bd141b9305f0\n", + "0.87255\n", + "Model id: 2db3e2d3-08ca-4a8f-9b73-26e3e06c9d37\n", + "0.89435\n", + "Model id: 2d9e5723-ff17-4b71-9f60-736eabb0e31b\n", + "0.8764\n", + "Model id: 2d8e431e-e98c-4469-a610-9895098a788c\n", + "0.8961\n", + "Model id: 2d3378c4-16c3-4ec1-9ed4-e904a7791dce\n", + "0.88925\n", + "Model id: 2cc33803-b45c-4e99-8d50-fc96f0a3a76a\n", + "0.8964\n", + "Model id: 2c9746bc-6834-4b8a-85b6-e4e6fc1fe8cf\n", + "0.89375\n", + "Model id: 2c2a5062-adc4-4803-894d-133a8c8de9bf\n", + "0.8979\n", + "Model id: 2c27cbbd-d1d3-4ae7-969e-4b031a235a5a\n", + "0.89335\n", + "Model id: 2a93cd3f-1ed2-4afc-af88-febf02f575e4\n", + "0.87435\n", + "Model id: 2a347eb8-7855-4c07-be51-6f395a29bde4\n", + "0.8767\n", + "Model id: 2a051ad9-155e-4c9c-9596-d0db28731ab7\n", + "0.8937\n", + "Model id: 29b80f3f-b5b0-4dfd-b700-6cc322628132\n", + "0.89835\n", + "Model id: 287c950d-7061-427e-af1c-ab5096c6cc4d\n", + "0.8742\n", + "Model id: 275513e5-2d5f-40fd-a735-60114aecfcb0\n", + "0.87435\n", + "Model id: 27457247-28fc-49ef-a76c-64a4db45a893\n", + "0.8546\n", + "Model id: 26dd83fc-9eb6-4cbb-8886-1c5e6aac7395\n", + "0.8948\n", + "Model id: 26bdd07c-04da-4aae-91c3-8e7b74eba5cd\n", + "0.8834\n", + "Model id: 26960d33-5d8e-48b3-bdcb-703578bed79e\n", + "0.89645\n", + "Model id: 26444efa-d831-4072-9e91-0179da3b7946\n", + "0.89665\n", + "Model id: 263b2bfd-cf09-4397-a5b8-6b6d79b68cf8\n", + "0.8906\n", + "Model id: 25f416ab-6816-4168-a3e8-b01f8b451404\n", + "0.89025\n", + "Model id: 25923536-80be-481f-b387-488360f792a9\n", + "0.9003\n", + "Model id: 24fd39e4-c075-4075-866a-b5b0ef937fb4\n", + "0.8905\n", + "Model id: 2308127a-7dcb-479a-9d41-b5d09dee80f1\n", + "0.8957\n", + "Model id: 226b6919-2e0d-4329-9276-1257aeebb5d8\n", + "0.8723\n", + "Model id: 226501b9-837a-403e-a4bc-998f01c9afc1\n", + "0.8814\n", + "Model id: 21c5de81-ff85-46da-ab85-5ed2f1264db4\n", + "0.8585\n", + "Model id: 21532c21-6aff-492b-9b13-97d316be7f7b\n", + "0.8827\n", + "Model id: 212d24e6-aa7e-471f-b759-782f1c011a04\n", + "0.898\n", + "Model id: 21078d4f-3cad-4d95-bf91-363967878f41\n", + "0.88135\n", + "Model id: 1ffc021a-1498-41e1-b7cc-a29063e1eb25\n", + "0.89145\n", + "Model id: 1fc2107d-a8d0-4871-9b0c-48aff13a4d26\n", + "0.87865\n", + "Model id: 1f1d9bc4-1e83-4afa-9c17-497a363a129d\n", + "0.89155\n", + "Model id: 1ef307dc-6956-45e5-a36d-3cd799bd9467\n", + "0.84935\n", + "Model id: 1e5f82c4-72b9-4ad2-b23f-9478b828c436\n", + "0.89725\n", + "Model id: 1e3d3735-8709-4c19-9d69-0f4ebe4c90de\n", + "0.8929\n", + "Model id: 1dc1e1a9-5573-4403-877f-928d09be7237\n", + "0.8837\n", + "Model id: 1d60094f-631c-4a7f-8145-e4386398bff8\n", + "0.90045\n", + "Model id: 1b499746-1be3-4b64-b383-d6a81c60a2f3\n", + "0.87165\n", + "Model id: 1a4bcda4-c0f8-4181-a6ad-5b92e1abf80e\n", + "0.8936\n", + "Model id: 1995c198-63aa-4281-827a-e9c07652223a\n", + "0.8629\n", + "Model id: 1946c0c4-11b8-41c2-bd32-6bb7da4cb681\n", + "0.89925\n", + "Model id: 191238d9-0087-4528-b38b-7dc1e66c46a8\n", + "0.8918\n", + "Model id: 187d786b-1a8f-4473-9321-dbcb0a44d892\n", + "0.8905\n", + "Model id: 185dcbae-cbc0-4568-9020-61613e946233\n", + "0.89865\n", + "Model id: 17e9cde4-9a4b-4bcd-ac89-674870f54d4e\n", + "0.8799\n", + "Model id: 17c3d174-fbe2-4314-a372-fd98abe5eef6\n", + "0.88895\n", + "Model id: 17bc8207-1e77-44ad-89e8-70fecd2e924b\n", + "0.8982\n", + "Model id: 16a344fb-9e3a-4df5-9aea-c37251181c1c\n", + "0.8902\n", + "Model id: 15e0cd65-af74-4fa7-a478-08d46a30ed40\n", + "0.87525\n", + "Model id: 15954bcf-e018-4d51-ad59-4779c39addf4\n", + "0.87955\n", + "Model id: 153c37e5-7fef-498b-849c-51bedc145a82\n", + "0.8703\n", + "Model id: 14eeaf3f-5818-4200-b6a6-a38da8660b17\n", + "0.8955\n", + "Model id: 14ded340-ddc6-49ae-82fc-b93095483059\n", + "0.87455\n", + "Model id: 1285b541-4234-4ca0-9e75-983a2a13272a\n", + "0.86815\n", + "Model id: 12661475-074b-499a-b25f-c3a074aece58\n", + "0.8968\n", + "Model id: 118d0262-3b42-4275-878b-e25a8a8669fc\n", + "0.8928\n", + "Model id: 1101ca7d-3e9c-49f7-a013-2a02b4dea481\n", + "0.9001\n", + "Model id: 10f9f46a-17a0-46f0-b503-26f5de16da17\n", + "0.8881\n", + "Model id: 10542aff-b66b-492c-9581-0cef4a8ac376\n", + "0.90055\n", + "Model id: 10106182-31be-4f56-be11-19fe8188622e\n", + "0.9006\n", + "Model id: 0fbd0d5b-8c51-4e8c-9857-363e5bc8e2f0\n", + "0.8904\n", + "Model id: 0ed80466-7642-48ab-bbc1-258178eed980\n", + "0.89515\n", + "Model id: 0eb628f8-b9da-44a7-bbe7-4f8366f697c1\n", + "0.85325\n", + "Model id: 0dc635c2-21cb-4486-8753-05bb6c0b2fef\n", + "0.90065\n", + "Model id: 0d5f6c89-2fec-41f9-9314-4ee6801851ee\n", + "0.89085\n", + "Model id: 0cfdf850-c1bc-40a0-9f1e-6bb3984d8523\n", + "0.8977\n", + "Model id: 0ce1a286-8842-4c45-a421-1695debf83bf\n", + "0.89995\n", + "Model id: 0bfa74a4-d795-4753-aede-8c85da8020fe\n", + "0.8982\n", + "Model id: 0bec3be3-cf9a-45e9-8957-15c7f8ab10a8\n", + "0.89615\n", + "Model id: 0bbb3d24-1d52-42f4-a51b-c383d14a2bd5\n", + "0.8785\n", + "Model id: 0b85f303-511b-4d8b-9372-274553607ef8\n", + "0.8726\n", + "Model id: 0b086464-c3f2-4981-9dec-430544a08409\n", + "0.9008\n", + "Model id: 0aab2669-a442-40d2-abbc-deaa31cd98a8\n", + "0.8949\n", + "Model id: 0a29f813-ed43-45ab-ac9d-9aebc762ddfa\n", + "0.8643\n", + "Model id: 0a0a61cf-7cfb-4935-9f55-a418d1afafba\n", + "0.8762\n", + "Model id: 0976bfed-bd0d-4833-a60f-d594fb59cd42\n", + "0.8932\n", + "Model id: 093091a5-8918-4b73-9a4f-7d3bcb48a418\n", + "0.89245\n", + "Model id: 08dae958-1b22-4041-b4aa-1a47d1d906b7\n", + "0.8904\n", + "Model id: 08ad21f5-7aeb-4a1a-9080-eea1a6a97f93\n", + "0.8843\n", + "Model id: 08a89783-6199-4431-891a-8f50cc04b8ef\n", + "0.87515\n", + "Model id: 082fdf1e-b70d-438e-89d8-aff3c9b5f2bf\n", + "0.8923\n", + "Model id: 080d1a28-7f9d-4c6d-8b8d-ba149db08604\n", + "0.8681\n", + "Model id: 07e2a2c4-45c7-4480-a32f-7f263f8f1461\n", + "0.8906\n", + "Model id: 078e0682-6d2d-462c-8588-152e0f98c8dc\n", + "0.8993\n", + "Model id: 07395c38-6a31-43a8-9c24-994a2ac356ed\n", + "0.8983\n", + "Model id: 06fd78cd-5d7a-4081-b97f-f10ed89b5fce\n", + "0.88945\n", + "Model id: 068bab00-492f-4fda-9a09-e26383a9de1c\n", + "0.90065\n", + "Model id: 05a985a9-19d2-487f-adff-bf3fa36990ca\n", + "0.8972\n", + "Model id: 05a2d395-3411-4da3-8b32-9fd14b25886e\n", + "0.90085\n", + "Model id: 056242e3-f7fa-4945-8a82-7b6a9b58c287\n", + "0.89175\n", + "Model id: 050e8b49-957a-43b9-8116-2914fb3556f9\n", + "0.67095\n", + "Model id: 04ce09d9-8e10-4512-b324-66075852dd62\n", + "0.8708\n", + "Model id: 04c21707-bb5b-4ad1-aaf3-117ccdb6c422\n", + "0.88975\n", + "Model id: 04b76483-1eb1-4c4d-b275-2bbb56e94ae8\n", + "0.88695\n", + "Model id: 0494a168-30a4-4a5d-b819-96da1b313264\n", + "0.87355\n", + "Model id: 034dbac4-5c1c-4c08-ad0c-6f4b9b11bf4e\n", + "0.89405\n", + "Model id: 032e49fa-fa0d-44fd-b0bf-107fcf69716c\n", + "0.8946\n", + "Model id: 028d0ce9-7e56-4c41-b652-8b031d18a1ad\n", + "0.88345\n", + "Model id: 02709899-6add-464b-9527-4d6c2a94cd23\n", + "0.8983\n", + "Model id: 0219db36-520c-488e-a6ea-a3f6f17a7ff3\n", + "0.8924\n", + "Model id: 01b9c6df-1c8c-4e6b-a56b-d8f8dd4c97b0\n", + "0.81875\n", + "Model id: 0167d98c-2d49-4d15-9b86-e866b6c7b303\n", + "0.8878\n", + "Model id: 015c75c5-42a4-4153-88fd-6a594bed7beb\n", + "0.88175\n", + "Model id: 014a5d60-1159-4f85-8f12-e5b76bd111fa\n", + "0.8829\n", + "Model id: 0120b2d1-cf8f-4965-9116-dcd191b1849a\n", + "0.87685\n", + "Model id: 01123a52-71bd-4d2d-bced-cd5e2bf714f6\n", + "0.9012\n", + "Model id: 01043492-0171-46e3-afab-4d33a76c152e\n", + "0.87605\n", + "Model id: 00ab838f-86f7-4880-9883-1ffd55d8f35c\n", + "0.8981\n", + "Model id: 00a5c1f6-c938-41f0-8cd4-b204e928a80f\n", + "0.8909\n", + "Model id: 0068194e-63cd-4f17-b5fa-e7c26a783f04\n", + "0.5792\n", + "Model id: 0062caea-b67a-49ec-933b-8325a5ceebc7\n", + "0.894\n" + ] + } + ], "source": [ - "model_trail_fedavg = client.get_model_trail()\n", + "model_trail = client.get_models()\n", + "\n", "\n", - "acc_fedavg = []\n", - "for model in model_trail_fedavg: \n", - " model = load_fedn_model(model['id'])\n", - " acc_fedavg.append(accuracy_score(y_test, model.predict(X_test)))" + "models = model_trail['result']\n", + "print(models)\n", + "acc_model_trail = []\n", + "nr = 0\n", + "for idx in models: \n", + " nr += 1\n", + " model_id = idx['model']\n", + " print('Model id: ', model_id)\n", + " model = load_fedn_model(model_id, nr)\n", + " acc_model_trail.append(accuracy_score(y_test, model.predict(X_test)))\n", + " print(accuracy_score(y_test, model.predict(X_test)))" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "1b7bb534", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "600" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "client.get_rounds_count()\n", + "#client.get_sessions_count()" ] }, { @@ -311,7 +1540,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 13, "id": "f0c3c51c", "metadata": { "scrolled": true @@ -320,16 +1549,16 @@ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 50, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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/PCDy/kWLlxIrVq18PPzo2PHjhw+fLhEr6ktU+Ln50e1atUKnfell16iVq1amM1mqlWrxqhRo1xmtd66dSsdO3YkMDCQoKAgmjdvzsaNG+33r1q1ig4dOuDn50dcXBzDhg0jPT29RK/L4cOH0TSNuXPn0rFjR8xmM40bN2bt2rUux5T2MYRnSXDjRrIquBDF03WdjJy8q/5ln1yzBM6dO8fixYsZMmQI/v7+he4PCQmx/3z33Xdz+vRpFi1axKZNm2jWrBmdO3fm3Llz9n0OHDjA/Pnz+eWXX/jll19Yvnw548ePB+CDDz6gbdu2DBo0iJMnT3Ly5Eni4uLsx7788suMHz+e3bt306hRI9LS0rjllltYunQpf//9Nz169KBXr14kJCSU6LmNGDHC/jgnT55k4sSJmM1mWrRoAcC4ceP46quvmDJlCjt37uS5557j/vvvZ/ny5QAcPXqUO++8k169erFlyxYee+wxXn755RI99qhRo+jbty9bt25lwIAB3Hvvvezevdt+f2BgIF9++SW7du3igw8+4LPPPuO9996z3z9gwAAqVarEhg0b2LRpEy+//LJ9DpQDBw7Qo0cP+vbty7Zt25g9ezarVq1i6NChJWqbzciRIxkxYgRbtmyhVq1a3HfffeTl5bn1McTVI91S7iQ1N0IUKzPXQr3Xf73qj7vrje6YfUr2Vrd//350XadOnToX3W/VqlWsX7+e06dPYzKZAJg4cSLz58/nhx9+4PHHHwdUNuTLL78kMDAQgAceeIClS5fy3//+l+DgYHx8fDCbzUV2k7zxxht07drVfjssLIzGjRvbb7/55pvMmzePn376qUQX2YCAAAICAgD466+/eO2115g+fToNGjQgOzubt956iyVLltC2bVsAqlWrxqpVq/j000+56aab+OSTT6hevTqTJk0CoHbt2mzfvp0JEyZc8rHvvvtuHnvsMXu7f//9dz788EM+/vhjAF577TX7vvHx8YwYMYJZs2bx4osvApCQkMALL7xg/73UrFnTvv+4ceMYMGCAvXapZs2aTJ482d5mX1/fS7YPVPB36623AjB27Fjq16/P/v37qVOnjtseQ1w9Ety4k+0Doiy/IES5VNIsz9atW0lLSys0+3JmZiYHDhyw346Pj7cHNgAxMTGcPn26RI9hy6jYpKWlMWbMGBYsWMDJkyfJy8sjMzOz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KgsyWsqNdGv+UHApKBbiunHTTTdx44030rdvX37//XcOHTrEokWLWLx4cbHH1KpViwEDBjBw4EDmzp3LoUOHWL9+PePGjWPBggWX3RZ3nTc+Pp5t27axd+9ezpw5Q25uLjVr1iQhIYFZs2Zx4MABJk+ezLx58y67raBGX61Zs4ahQ4eyZcsW9u3bx48//mgfwVO5cmV8fHz48MMPOXjwID/99BNvvvnmFT3mlXj66adZuHAh7777Lvv27ePTTz9l0aJFLlm5S/H39+fJJ5/khRdeYPHixezatYtBgwaRkZHBo48+WuLzvP7663z11VeMHTuWnTt3snv3bmbNmsVrr71W7DFjxoxh0qRJTJ48mX379rF582Y+/PDDEj3ekCFDOHfuHPfddx8bNmzgwIED/Prrrzz88MOlClZsAVZiYiLnz58v8XGlJcGNO+VnbjTJ3AhxXZkzZw4tW7bkvvvuo169erz44ouXfMOfNm0aAwcO5Pnnn6d27dr07t2bDRs2ULly5StqizvOO2jQIGrXrk2LFi2IjIxk9erV3H777Tz33HMMHTqUJk2asGbNmssaKeSsUaNGLF++nH/++YcOHTrQtGlTXn/9dWJj1ai6yMhIvvzyS77//nvq1avH+PHjmThx4hU95pVo3749U6ZM4d1336Vx48YsXryY5557zqUbyDaE2nn4fUHjx4+nb9++PPDAAzRr1oz9+/fz66+/2kd5lUT37t355Zdf+O2332jZsiVt2rThvffes2eDivLggw/y/vvv8/HHH1O/fn1uu+029u3bV6LHi42NZfXq1VgsFrp160bDhg159tlnCQkJKdWcTZMmTeL3338nLi6Opk2blvi40tL0S3W0XmNSUlIIDg4mOTn5igrhinL8hRdJ+flnol56ifCHH3LruYUoT7Kysjh06BBVq1Ytsv9fiGvFoEGD2LNnDytXrgTgzz//5M477+TgwYOlClaEcrH3jtJcv6Xmxp2koFgIIa5pEydOpGvXrvj7+7No0SKmT5/Oxx9/bL9/4cKFvPrqqxLYeJgEN25kLyg2Sm+fEEJci9avX8/bb79Namoq1apVY/LkyTz22GP2+9955x0Ptk7YSHDjRvaCYpnnRgghrknfffedp5sgSkBSDO4kBcVCCCGEx0lw40YyQ7EQQgjheRLcuJOtoFgyN0IIIYTHSHDjRrolf4rp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\n", + "image/png": "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z6GAhw7Rmi0V1IJ/wd42u6ViEhQ0rCmv/1cQCimis4dQWD5rFj1z77307JQptGDxYvGd1WoVDT7m15s+fTpt3LiRnnvuOTHylYEgQfHT999/nzZs2CA6NXQq6Di//PJL+ueff+jSSy8VIkOZmIHfIRMVyyDGEZ0eOoYff/xRvNDxv/vuu/TNN984fuPrel966SXxe4QUQGxBEAH8j/IesuDBCx0rgMCAOMIxvv7666Kjf/XVVwM+r9u3bxfbQWIK9nXGjBlCmCg7XIinp556itatWyeECDpduM20PPjgg+K8o+YaSpMor9vq1avphRdeoCeffJIWLFjgcZ8eeeQRcQ7QqUPIXXnllQ7BgGt8yy230Pjx48X38F4888wzfh83On+UTsH+/fnnn9SxY0caMWIEHT161GVfXn75Zfrjjz+EJer66693fLd8+XIaPXq02BdcD9wHuDae9uedd96h22+/nW6++WZav369ENzYti9AwGPKpRNPPFHsD+7Hw0eO0LX2+0Zuyz2dc1nAffjhh+K+Ugq6oGOLMYqLi9HSif+Dzhsn2WwTM2y2XStt4eDg08/YNnbpKl5HP//c47LVO3c6lt3/yCO2+sRcXOzYdukvy23RTMH77zuONZaorKy0bdy4UfzvoLpMej7q+VWzZ7utYv168ao9etTrvo8ZM8Z2wQUXOD4PGTLENnjwYPG+audOsZ7ePXrY7r/3XvG3+fPn24xGo23z5s266/vwww9Fm/P33387/rZ7926byWSy7d+/X7XssGHDbA899JDqd9u2bXN8P27cOFtKSoqttLTU8bcRI0aIv9dlvZMnT7Y1adLE7Tlwx4svvmjr3bu34/PEiRNtxx9/vNvllyxZIrZ97Ngx8fmGG26w3Xzzzaplli9fLs6n6t5R8Pvvv4t1yOdAXufs2bNVyymvm0zfvn1tDzzwgOMzfjdr1izxfufOneLz+++/7/h+w4YN4m+bNm0Sny+//HLbOeeco1rn1VdfbcvMzLT5SllZmS0+Pt722WefOf5WU1Nja968ue2FF15QHdPChQsdy/zwww/ib/J5wTWdNGmSat2ffvqprVmzZm63jW08otPeo03GfY31f/vVV6rz8ddff4nPTz31lO3MM89U/W731q1imXXffWer3r/f73Puc9sRQP8dAZGvUUTY3VIUULZUeCsUM0zkI1sCZJrm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nJwc23333Werra1VrQfLn3DCCbaEhARb+/btbR9++KHL9t98801b69atxTL9+vWz/fbbb17Pw5QpU2xdunQRbXyzZs1sd955p+M77OesWbPE+507d4rPf/31l+P7LVu22EaNGmXLysoS+961a1fb3XffbbNareL7IUOG2MaPH6/aHs45zonM3LlzbR07drTFxcXZ2rRpI/qXK664wtaqVStxHM2bN7fdcccdqnsM+6F3/DITJ060HX/88W6/X7Nmje2MM86wpaWl2VJTU229evWyPfPMM47vDx48aDvnnHNsiYmJ4nx+8sknYt9effVVsX/Z2dm2F154QXfdzz//vC0vL89WU1MjPr/99ttif0ePHu2y7KZNm2yDBg0Sx4lz991334ll582b51jm888/F9cnnFgtFsezVXPoUMjbDn/6bwP+oQCAqoW6hWpFuiJibhoCiGTPzMwUqY0w70YThR99REeek/zcLd95m9JPO83j8pu6SiOextddR00efKBe9pGJDaqqqkQQJuLzwh1May4spNqDB8X7hLZtyaQT6OorNfsPkOWYVDQtsUsXMvqYhowROEbksGLoBdoydQf3G1xWcC0FkkUVycB6g7o3qJItB1BjyiN4UeDiChc2m42q7BbJuNxcitdk5QWCp7bDn/474JKSuHmi7QaKqto27HZimOAT4GOFzCAE2KLRdle0j6kbP/74o8jYjYZ+CYX5IIJxLBA0CPtAHRxZ2KAgIuKN4PpjgiRuLr74YuFThH9SyQsvvCD8n4HWgmDCCAshJprxJxYthHAB1NCCWJ9oAYX70MciHgvZdkh7RwyUDP6GOCsmiAHFv/zyC5199tkuf0eRK3zHREYj7s9UDFzEj2H8h4cETKjApJhIPYeLBjN/Y04xzkYOsbhBJL88D4c2bQ/+MCaccHPLMB7hR4RhYgK/xQ38xai0qAWFlOTaC0wEwK4mJgIIMF8hYuGnimEoMA9BPbcZfsfcPPbYYyKQCVH/mBwNoBYBSk5zvE2YCTSgmIUQE2Tk6rOoxVHXircRCz83DBN05Bnv5clS603coKw2pl3HpGiorIiGC5kAmJIdE4wxDbCxjbLRNRN+0DBhzh25um5KSkpIRnm+YK6pIbO9rL+1urpOjWat2UwWeYqAqioyKErsM0wsUm1/Hsy1tWSpqqrTulA9G7Oxo71QTl8RCAH9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", "text/plain": [ "
" ] @@ -339,14 +1568,17 @@ } ], "source": [ - "x = range(1,len(acc_fedavg)+1)\n", + "from scipy.interpolate import make_interp_spline\n", + "\n", + "\n", + "x = range(1, len(acc_model_trail)+1)\n", "plt.plot(x,[central_test_acc]*len(x))\n", "plt.plot(range(n_global_rounds), central_acc_one_client)\n", "plt.plot(range(n_global_rounds), central_acc_all_clients)\n", - "plt.plot(range(len(acc_fedavg)),acc_fedavg)\n", + "plt.plot(range(len(acc_model_trail)),acc_model_trail)\n", "plt.xlabel('Global round')\n", "plt.ylabel('Accuracy score')\n", - "plt.legend(['Centralized baseline', 'Incremental learning, one client','Incremental learning, all clients', 'FL (10 clients, FedAdam)'])" + "plt.legend(['Centralized baseline', 'Incremental learning, one client','Incremental learning, all clients', f'FL ({client.get_clients_count()} clients, FedAvg)'])" ] }, { @@ -356,11 +1588,150 @@ "source": [ "As can be seen, FEDn trains a federated model that reaches the same level of performace as the centralized baseline, with convergence close to the simulated case where 10 clients send data to a central server. Here we used FedAdam with a fixed learning rate 1e-2 as the server-side aggregator. It is possible that hyperparameter tuning, or adapting the learning rate, could improve convergence further. This was not the focus of this experiment though - the objective was to set up a pseudo-local experiment environment where clients connect intermittently and validate the robustness of FEDn in this scenario. In future parts of this series we will build on this in different ways as we explore various aspects of cross-device FL. " ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "e471a8f2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1000" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "client.get_clients_count()" + ] + }, + { + "cell_type": "markdown", + "id": "77dd2cac", + "metadata": {}, + "source": [ + "# Visualisation of Experiment" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "8712feec", + "metadata": {}, + "outputs": [], + "source": [ + "# Load control.rounds.json \n", + "import json\n", + "data = []\n", + "with open('/Users/sigvard/Desktop/Vinnova/23:4:25 - 1000 Clients 600 rounds/Mongo express DB/control.rounds.json') as f:\n", + " for line in f:\n", + " try:\n", + " data.append(json.loads(line))\n", + " # Process each JSON object here\n", + " except json.JSONDecodeError as e:\n", + " print(f\"Error decoding JSON: {e}\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb29c55c", + "metadata": {}, + "outputs": [], + "source": [ + "# Iterate over the data dict and load: \n", + "# rounds\n", + "# nr of aggregated models each round\n", + "# time executing training\n", + "\n", + "rounds = []\n", + "aggregated_models = []\n", + "models_trained = []\n", + "time_exec_training = []\n", + "\n", + "for round_data in data:\n", + " rounds.append(round_data['round_id'])\n", + " \n", + " # Number of aggregated models\n", + " nr_aggregated_models = round_data['combiners'][0]['data']['aggregation_time']['nr_aggregated_models']\n", + " aggregated_models.append(nr_aggregated_models)\n", + " \n", + " # Stragglers\n", + " requested_clients = round_data['round_config']['requested_clients']\n", + " \n", + " models_trained.append(requested_clients)\n", + " \n", + " # Time for training\n", + " time_exec_training.append(round_data['combiners'][0]['time_exec_training'])\n", + "\n", + "rounds = list(map(int, rounds))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "id": "a7a1be22", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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ZYwfP166du0am5bCccMI/dGzBQAAgGQTcTVC7cXStbaC6balS5dGq11wQdhiGCEAAACSWcRhq1q1aiErEeqixnoZ4oewBQAAALio9Puzzz4rnTp1kunTp8uFF15otmmP1rp16+Sjjz6KRRuRA8IWAAAA4KKerXbt2plgdfXVV8uuXbvMSX//6aefzGWIH8IWAAAA4LJFjatWrSpPPvlk9FuDiBC2AAAAAJeFrT179siECRNkzZo15vzZZ58td955p5QsWTLa7UMuCFsAAACAi4YRLl++XE477TQzd8saRqil4HXbihUrYtNKnJQdw5Y/whYAAACSTcQ9W/fff79ZV0srEhYo4P3zY8eOyT//+U/p27evLFiwIBbthAMDFj1bAAAASGYF8tKz5R+0zI0UKCAPPfRQyPW3EDsMIwQAAABcNIywRIkSsmnTpmzbN2/eLMWLF49Wu+CysJWVlciWAAAAAA4IWzfeeKN069ZN3n//fROw9PTee++ZYYQ33XRTbFoJx4cterYAAACQbCIeRjhq1ChJSUmR2267zczVUunp6dKzZ08ZMWJELNqIHBC2AAAAABeFrYIFC8rYsWNl+PDh8ssvv5htWomwSJEisWgfckHYAgAAAFy2zpbScFW/fv3otgauClv+CFsAAABINmGHLV20OByvvfZaftqDPLJj2KJnCwAAAMks7LA1ceJEqVGjhjRq1Eg8djyyT0J279kibAEAACCZhR22tADGpEmTZMOGDXLHHXfILbfcImXKlIlt65ArOwYsf4QtAAAAJLOwS7+/8MILsm3bNrN48eeffy7VqlWTG264QWbOnElPV4LQswUAAAC4ZJ2tjIwMs5bW7NmzZfXq1XL22WfLPffcIzVr1pQDBw7ErpUIibAFAAAAuGhRY98fpqaa9ba0VysrKyu6rUJYCFsAAACAS8LWkSNHzLyt1q1byxlnnCHfffedPP/887Jp0yYpVqxY7FoJR4Ytf4QtAAAAJJuwC2TocMH33nvPzNXSMvAausqVKxfb1sHRYcu/TXR+AgAAINmEHbZeeuklqV69upx66qkyf/58cwplypQp0WwfwmT3sEXPFgAAAJJN2GHrtttuM3O0YB9O6tkibAEAACDZRLSoMeyFsAUAAAC4sBohEs+OAcsfYQsAAADJjLDlYPRsAQAAAPZF2HIwu4ctf4QtAAAAJBvC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65Zt+5133imrV6+WWFm2bJm8/PLLpifNnwa/zz//XD788EOZP3++bN26VTrq+Lm/ZWVlmaB19OhRWbRokbzxxhumuqIbqibmFrasoXr+PVvaq/TFFyKTJnl/xrqXibAFAPnHosYAkERh65RTTpGVK1dm267bYlU4QxdR7tKli7zyyitSunRp3/a9e/fKhAkTTO/a5ZdfLo0bN5bXX3/dhKqvv/7aXGfWrFkmBL799ttmvbArr7xShg0bJi+88IIJYMnSs6XfjNasKdKihcjNN3t/6vkpU2LfPsIWAESnZ4s5WwDg8gIZ3bt3lx49esj69evl4osvNtu++uoreeqpp6Rfv36xaKMZJqi9U61atZLHH3/ct/2bb76RzMxMs91Sp04dqV69uixevFguuugi87N+/fpSoUIF33XatGkjPXv2lB9++EEaNWqU7f6OHDliTpZ9+/aZn3pfekoU676tn8ePp+WYl48fz5LMTOt/aG/a+e03TT9/pzAR2bLFI9ddJ/Lee1ly7bW5JLc8OnZM25YmqanaheadRHboUKbksB520grer3AH9qv7JGqfZmam+P67/usv/892RAPvVXdiv7pPpo32aSRtiDhsDRgwQIoXLy6jR4+W/v37m22VK1eWwYMHS28tgRdlur7XihUrzDDCYNu3b5eCBQtKqVKlArZrsNLLrOv4By3rcuuyUIYPHy5DhgzJtl17yXTeV6JZhUh27WoqImVDXuenn9bKtGnr5PBhDTxX/731RNBSHo+e90ivXkelQIHZvqIa0fLLL3VFpLZs3LhRUlNryfHjqTJr1lwpU+ZwdO/IJSgw407sV/eJ9z5dvlxHjTQxv69cuVqmTVsf1/tPFrxX3Yn96j6zbbBPDx06FLuwpQUqdJ6Unvbv32+2afiKhc2bN5uS8vqkFipUSOJFQ6R/L532bFWrVk2uuOIKKaELWCUwRetz0bp1a0lPT5cRI3JOR2eddaa0a1db5s0LDFjZpciffxaREiWukubNo9u7tXCht9ft1FNrSnp6imhn4aWXXi7Vq0f1bhwveL/CHdiv7pOofer9Yszr9NPrSrt2deJ238mA96o7sV/dJ9NG+9Qa9RazdbYssQpZ/sMEd+zYIeeee25AwYsFCxbI888/b8rQ67yrPXv2BPRuaTXCihUrmt/1p5ap92dVK7SuEywjI8OcgumOTfTO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taKS/77V0vD6n4b/vtVha/7/Meg38FquNvg/HCSGvs+OHDnC/nSoli1bmn2ivZXWSb851zk+1u/sV+fTst6//PKLOVjnvepcl1xySbYlVH766SfTa+maY6ZEV+hAeN577z1TeWXixImm6kqPHj08pUqVCqiqA3tVwfr222/NSd9mzzzzjPn9119/NZePGDHC7L9PP/3Us2rVKk/79u09tWrV8vz111++22jbtq2nUaNGniVLlngWLlxoqmrddNNNCXxU6Nmzp6dkyZKeL774wrNt2zbf6dChQ77r3H333Z7q1at75s6d61m+fLmnSZMm5mQ5duyYp169ep4rrrjCs3LlSs+MGTM8p5xyiqd///4JelTJ7eGHHzbVJDds2GDei3peK1jNmjXLXM7+dAf/aoSK/eo8DzzwgPns1ffqV1995WnVqpWnXLlypiqsYp8609KlSz0FChTwPPHEE55169Z53nnnHU+RIkU8b7/9tu86Tj9mImw5yHPPPWc+SAoWLGhKwX/99deJbhJyMG/ePBOygk9du3b1lTIdMGCAp0KFCiZEt2zZ0rN27dqA29i5c6f5oChWrJgpTXvHHXeYEIfECbVP9fT666/7rqMf/vfcc48pH67/YVx77bUmkPnbuHGj58orr/QULlzYHCzoQURmZmYCHhHuvPNOT40aNcznqh546XvRClqK/enOsMV+dZ4bb7zRU6lSJfNerVKlijn/888/+y5nnzrX559/boKwHg/VqVPHM378+IDLnX7MlKL/JLp3DQAAAADchjlbAAAAABADhC0AAAAAiAHCFgAAAADEAGELAAAAAGKAsAUAAAAAMUDYAgAAAIAYIGwBAAAAQAwQtgAAAAAgBghbAABE2WWXXSZ9+/ZNdDMAAAlG2AIAOM7tt98uKSkp5pSeni61atWShx56SA4fPixOMHHiRClVqlTAeevxpKWlSenSpeXCCy+UoUOHyt69exPaVgBA3hG2AACO1LZtW9m2bZusX79enn32WXn55Zdl0KBB4lQlSpQwj+e3336TRYsWSY8ePeTNN9+Uhg0bytatWxPdPABAHhC2AACOlJGRIRUrVpRq1apJhw4dpFWrVjJ79mzf5UeOHJHevXtL+fLlpVChQtK0aVNZtmxZjr1L6pNPPjG9S5bBgwebsPPWW29JzZo1pWTJktK5c2fZv3+/7zoHDx6U2267TYoVKyaVKlWS0aNH5+nx6P3q49HbOOuss6Rbt24mdB04cMD02gEAnIewBQBwvO+//94Ek4IFC/q2aUD56KOP5I033pAVK1bI6aefLm3atJFdu3ZFdNu//PKLCWFTp041p/nz58uIESN8lz/44INm26effiqzZs2SL774wtxfNGhQ7NKli3z22WeSlZUVldsEAMQPYQsA4EgafLQ3SXut6tevLzt27DDBx+ptevHFF+Xpp5+WK6+8UurWrSuvvPKKFC5cWCZMmBDR/Rw/ftz0gtWrV0+aNWsmt956q8yZM8dcpr1OenujRo2Sli1bmnZouDt27FjUHmedOnVMT9rOnTujdpsAgPgoEKf7AQAgqlq0aGEClQYrnbNVoEAB6dSpk683KjMzUy655BLf9bWQxgUXXCBr1qyJ6H50+GDx4sV953WYnwY7636OHj1qillYypQpI2eeeaZEi8fjMT/9hzcCAJyBni0AgCMVLVrUDA1s0KCBvPbaa7JkyZKIeq1SU1N9QcaiAS2YhjR/Gnq0tyteNBxq8YyyZcvG7T4BANFB2AIAOJ4Gp0ceeUQee+wx+euvv+S0004z87e++uqrgCClBTJ0SKE65ZRTzPA87RmzrFy5MqL71fvRMKZBz7J792756aefovK4tAft3XffNQVA9DECAJyFT24AgCtcf/31Zo2qF154wfR69ezZ08zhmjFjhqxevVq6d+8uhw4dMlX+lA79K1KkiAlpOhxQQ43OzYqEzhnT29P7mTt3rinUoWuA5SUYaS/b9u3bTfl37c3S3rqLL77YVED0L8gBAHAOwhYAwBV0zta9994rI0eONL1VGlB0DpcWtDj33HPl559/lpkzZ5oFg625VW+//bZMmzbNFLaYNGmSKfUeKS3CoYUzrr76alN+XkvMN27cOOLb2bdvn5kPVqVKFWnSpIlZN6xr167y7bffmu0AAOdJ8QQPWAcAAAAA5Bs9WwAAAAAQA4QtAAAAAIgBwhYAAAAAxABhCwAAAABigLAFAAAAADFA2AIAAACAGCBsAQAAAEAMELYAAAAAIAYIWwAAAAAQA4QtAAAAAIgBwhYAAAAASPT9P+GWiPYndh+AAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA0oAAAIjCAYAAAA9VuvLAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjEsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvc2/+5QAAAAlwSFlzAAAPYQAAD2EBqD+naQAAsYNJREFUeJzt3QeYE1XXB/CzjYUFlt57kw6KlQ7S8QOlCNiwK4ogIqjYALuICiqC7RUsFGmiiAhKERRUigrSpEoVkLJ0tuR7zl1umExmkplkkplJ/j+fld1sNplkJjP33HvuuQkej8dDAAAAAAAA4JV48VsAAAAAAABgCJQAAAAAAABUECgBAAAAAACoIFACAAAAAABQQaAEAAAAAACggkAJAAAAAABABYESAAAAAACACgIlAAAAAAAAFQRKAAAAAAAAKgiUAABc7I477qDKlSuH9LcjRoyghIQEcjJ+bfwaIXa0atVKfAEAOB0CJQCACOAAxMjXkiVLKJ7w6zX63oA2DhyV71Nqaipdcskl9Oyzz9LZs2ft3jwAgJiRbPcGAADEok8//dTn508++YQWLlzod3vt2rXDep4PPviAcnJyQvrbp59+mp544gmKJn696vdg2LBhVKBAAXrqqaf87r9582ZKTESfnhoHRx9++KH4/vjx4zRnzhx6/vnnadu2bfT555/bvXkAADEhwePxeOzeCACAWPfQQw/RuHHjKNgp9/Tp05SWlkbxpF69elS8ePG4G13Tw8cIjwzly5dPd0RpxowZdPLkSZ+/adKkCf3yyy+0f/9+KlWqFDmVTLvD/gYAp0M3HQCAjQ1GDhJWr15NLVq0EAHSk08+KX7HIwTXXXcdlS1bVoweVKtWTYwYZGdnB5yjtHPnTpGONXr0aHr//ffF3/HfX3nllfTbb78FnaPEP3NQ9+WXX4pt47+tW7cuzZ8/32/7uaF7xRVXUN68ecXzvPfee5bPe1LPUZo4caJ4/OXLl9PAgQOpRIkSVLhwYbr//vvp/PnzdOzYMerbty8VKVJEfD322GN+wSmPwI0ZM0a8Lt52Dir4748ePRp0e3hbePRr+/bt1KFDB8qfP7/YR88991zIz8Ov8f/+7//ou+++E+8nB0j8XprB70mzZs3ENvC2Kb377rtiG3hf8rb2799fvE+B3me9+UQydfKLL76gF198kcqXLy9eW5s2bWjr1q1+fy+PQX5NV111FS1btszU6wIAsBNS7wAAbPTff/9Rp06dqE+fPnTrrbd6RwI4IOAG+eDBg8W/ixYtEnNQMjIy6LXXXgv6uJMnT6YTJ06Ihjk3bEeNGkXdu3cXjeiUlJSAf8tByKxZs+jBBx+kggUL0ltvvUU9evSgf/75h4oVKybus3btWurYsSOVKVOGRo4cKQI4DhY4cImGAQMGUOnSpcVzr1y5UjTIOWD6+eefqWLFivTSSy/RvHnzxHvFAR8HTxK/J/z+3nnnnSLY2rFjB73zzjviNf30009B3x9+rfzar7nmGvG+chA5fPhwysrKEu9BKM/DKYY33XST+Jt7772Xatasafo94SCZcYAoceDK71Hbtm3pgQceEM8zfvx4ETQbea16XnnlFZESOWTIEJH6x+/DLbfcIka0pI8++ki8Hh7pGjRokDj2unbtSkWLFqUKFSqE9LwAAFHFqXcAABBZ/fv35+EGn9tatmwpbpswYYLf/U+fPu132/333+9JS0vznD171nvb7bff7qlUqZL35x07dojHLFasmOfIkSPe2+fMmSNu//rrr723DR8+3G+b+Oc8efJ4tm7d6r3tjz/+ELe//fbb3tu6dOkitmXv3r3e2/7++29PcnKy32MGU7duXfFeaOHXxq9R+vjjj8Xjd+jQwZOTk+O9vXHjxp6EhARPv379vLdlZWV5ypcv7/PYy5YtE3//+eef+zzP/PnzNW9X423h+w0YMMB7G2/HddddJ963Q4cOmX4efo18G//OCN6G/Pnzi+fiL95Xo0ePFq+/Xr163vfl4MGDYpvat2/vyc7O9v79O++8I57vf//7n+77LPF7p3z/Fi9eLP62du3annPnznlvHzt2rLh93bp14ufz5897SpYs6bn00kt97vf++++L++ntbwAAJ0HqHQCAjTgdikcc1JTzU3hk6PDhw9S8eXMxh2nTpk1BH7d3794+Iwv8t0ydlqWFRx84XUpq0KABpaene/+WR1S+//57uuGGG0Qql1S9enUxOhYNd999t0+K39VXXy3Szvh2KSkpSaSyKV/z9OnTqVChQtSuXTvxnsqvyy+/XIzcLV682NDzc3qiOl2RU//4fQnleapUqSJS+Yw6deqUGL3jL37feWSnadOmImVTvi+8LbxNPJqjLIjBI1a8P7/55hsKFR+zefLk0T2+Vq1aRQcP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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Number of Aggregated Models per Round\n", + "plt.figure(figsize=(10, 6))\n", + "plt.plot(rounds, aggregated_models, marker='o', linestyle='-', color='b')\n", + "plt.title(\"Number of Aggregated Models per Round\")\n", + "plt.xlabel(\"Round ID\")\n", + "plt.ylabel(\"Number of Aggregated Models\")\n", + "plt.grid(True)\n", + "plt.show()\n", + "\n", + "\n", + "# Training Time per Round\n", + "plt.figure(figsize=(10, 6))\n", + "plt.plot(rounds, time_exec_training, marker='o', linestyle='-', color='g')\n", + "plt.title(\"Training Time per Round\")\n", + "plt.xlabel(\"Round ID\")\n", + "plt.ylabel(\"Training Time (seconds)\")\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5989c307", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "async_env", "language": "python", "name": "python3" }, @@ -374,12 +1745,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.6" - }, - "vscode": { - "interpreter": { - "hash": "21345b455230dd04cf84c108e7c182ecfe8d1aa1242b8b64881a6d2c0a5951ac" - } + "version": "3.12.10" } }, "nbformat": 4, diff --git a/examples/async-clients/README.rst b/examples/async-clients/README.rst index dea415fa5..9b84fbb61 100644 --- a/examples/async-clients/README.rst +++ b/examples/async-clients/README.rst @@ -7,12 +7,11 @@ Prerequisites ------------- - [Python 3.8, 3.9 or 3.10](https://www.python.org/downloads) -- [Docker](https://docs.docker.com/get-docker) -- [Docker Compose](https://docs.docker.com/compose/install) - -Running the example (pseudo-distributed, single host) ------------------------------------------------------ +- [Docker (if running locally)](https://docs.docker.com/get-docker) +- [Docker Compose (if running locally)](https://docs.docker.com/compose/install) +Set up environment +------------------ First, make sure that FEDn is installed (we recommend using a virtual environment) @@ -28,54 +27,94 @@ Install FEDn pip install fedn - -Prepare the example environment and seed model -------------------------------------------------------------------- - Standing in the folder fedn/examples/async-clients .. code-block:: pip install -r requirements.txt +Upload seed model +------------ + Create the seed model .. code-block:: - python init_seed.py seed.npz + python init_seed.py -You will now have a file 'seed.npz' in the directory. +You will now have a file 'seed.npz' in the directory. Add this seed model to the FEDn instance: -Running a simulation --------------------- +.. code-block:: -Deploy FEDn on localhost. Standing in the the FEDn root directory + python init_fedn.py + +Project configuration +------------ + +The file ``config.py`` contains all configuration settings for this example. The most important setting is ``USE_LOCAL`` which determines whether to run with a local FEDn deployment or connect to a remote instance such as a Studio instance project. + +**For local deployment:** +- Set ``USE_LOCAL = True`` in ``config.py`` +- Deploy FEDn locally using Docker Compose. Standing in the FEDn root directory: .. code-block:: - docker-compose up + docker compose up +**For remote deployment:** +- Set ``USE_LOCAL = False`` in ``config.py`` +- Set up a project in Scaleout Studio +- Update the ``DISCOVER_HOST`` in ``REMOTE_CONFIG`` with your API URL +- Create a ``tokens.json`` file with the following structure: + +.. code-block:: -Initialize FEDn with the seed model + { + "api.fedn.scaleoutsystems.com/your-project-name": { + "CLIENT_TOKEN": "your-client-token-here", + "ADMIN_TOKEN": "your-admin-token-here" + } + } + +Replace ``your-project-name``, ``your-client-token-here``, and ``your-admin-token-here`` with your actual values. + +Monitoring client status (optional) +---------------------------------- + +If you want to monitor client statuses, edit ``client_status.py`` and update the ``MACHINE_NAMES`` list with the names of the machines running your clients. Then run: .. code-block:: - python init_fedn.py + python client_status.py + +This will periodically check and record client statuses to a CSV file. -Start simulating clients +Running clients and analyzing participation +------------------------------------------ + +Start simulating clients: .. code-block:: python run_clients.py +You can use the ``--intermittent`` flag to simulate clients that periodically disconnect and reconnect. + Start the experiment / training sessions: .. code-block:: python run_experiment.py -Once global models start being produced, you can start analyzing results using API Client, refer to the notebook "Experiment.ipynb" for instructions. +You can adjust the number of sequential training sessions by modifying the ``N_SESSIONS`` parameter in ``config.py``. If you are using Scaleout Studio, you can also start a session directly through the Studio interface or use the APIClient. + +To analyze client participation and identify potential issues after a session has started: + +.. code-block:: + python client_participation.py +This will generate plots showing the number of aggregated models and validations per round, helping you understand client participation patterns and identify where things might be going wrong. +Once global models start being produced, you can start analyzing results using API Client, refer to the notebook "Experiment.ipynb" for instructions. diff --git a/examples/async-clients/asyncio_run_clients.py b/examples/async-clients/asyncio_run_clients.py new file mode 100644 index 000000000..ccbcba57e --- /dev/null +++ b/examples/async-clients/asyncio_run_clients.py @@ -0,0 +1,137 @@ +import asyncio +import random +import time +import uuid + +import numpy as np +from io import BytesIO +from sklearn.metrics import accuracy_score + +from init_seed import compile_model, make_data +from config import settings +from fedn import FednClient + +HELPER_MODULE = "numpyhelper" + +def get_api_url(host: str, port: int = None, secure: bool = False): + if secure: + url = f"https://{host}:{port}" if port else f"https://{host}" + else: + url = f"http://{host}:{port}" if port else f"http://{host}" + if not url.endswith("/"): + url += "/" + return url + +def load_parameters(model_bytes_io: BytesIO): + """Load model parameters from a BytesIO object.""" + model_bytes_io.seek(0) # Ensure we're at the start of the BytesIO object + a = np.load(model_bytes_io) + weights = [a[str(i)] for i in range(len(a.files))] + return weights + + +def load_model(model_bytes_io: BytesIO): + parameters = load_parameters(model_bytes_io) + + model = compile_model() + n = len(parameters) // 2 + model.coefs_ = parameters[:n] + model.intercepts_ = parameters[n:] + + return model + +def on_train(in_model, client_settings): + print("Running training callback...") + model = load_model(in_model) + + X_train, y_train, _, _ = make_data() + epochs = settings["N_EPOCHS"] + for i in range(epochs): + model.partial_fit(X_train, y_train) + + # Prepare updated model parameters + updated_parameters = model.coefs_ + model.intercepts_ + out_model = BytesIO() + np.savez_compressed(out_model, **{str(i): w for i, w in enumerate(updated_parameters)}) + out_model.seek(0) + + # Metadata needed for aggregation server side + training_metadata = { + "num_examples": len(X_train), + "training_metadata": { + "epochs": epochs, + "batch_size": len(X_train), + "learning_rate": model.learning_rate_init, + }, + } + + metadata = {"training_metadata": training_metadata} + + return out_model, metadata + +def on_validate(in_model): + model = load_model(in_model) + + X_train, y_train, X_test, y_test = make_data() + + # JSON schema + metrics = {"validation_accuracy": accuracy_score(y_test, model.predict(X_test)), "training_accuracy": accuracy_score(y_train, model.predict(X_train))} + + return metrics + +async def async_run_fedn(fl_client): + """Run fl_client.run() in a thread to avoid blocking event loop""" + await asyncio.to_thread(fl_client.run) + +async def simulated_client(client_index): + """ + Simulate one client with random offline-online intervals with one process under asyncio + """ + client_id = str(uuid.uuid4()) + name = f"client{client_index + 1}" + + for i in range(settings["N_CYCLES"]): + # Sample a delay until the client starts + t_start = np.random.randint(1, settings["CLIENTS_MAX_DELAY"]) + await asyncio.sleep(t_start) + + fl_client = FednClient(train_callback=on_train, validate_callback=on_validate) + fl_client.set_name(name) + fl_client.set_client_id(client_id) + + controller_config = { + "name": fl_client.name, + "client_id": fl_client.client_id, + "package": "local", + "preferred_combiner": "", + } + + url = get_api_url(host=settings["DISCOVER_HOST"], port=settings["DISCOVER_PORT"], secure=settings["SECURE"]) + + result, combiner_config = fl_client.connect_to_api(url, settings["CLIENT_TOKEN"], controller_config) + combiner_config.host = "100.84.229.36" + fl_client.init_grpchandler(config=combiner_config, + client_name=fl_client.client_id, + token=settings["CLIENT_TOKEN"]) + + fedn_task = asyncio.create_task(async_run_fedn(fl_client)) + + online_for = settings["CLIENTS_ONLINE_FOR_SECONDS"] + await asyncio.sleep(online_for) + + + fl_client.grpc_handler._disconnect() + print(f"{name} Disconnected, after online {online_for}") + + await fedn_task + + print(f"{name} All cycles finished") + +async def main(): + N = settings["N_CLIENTS"] + tasks = [asyncio.create_task(simulated_client(i)) for i in range(N)] + print("debug tasks: ", tasks) + await asyncio.gather(*tasks) + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/examples/async-clients/client_participation.py b/examples/async-clients/client_participation.py new file mode 100644 index 000000000..13e97c981 --- /dev/null +++ b/examples/async-clients/client_participation.py @@ -0,0 +1,108 @@ +"""This script analyzes client participation in a FEDn session. + +It retrieves data from the FEDn API about a specific training session (or the most recent one +if not specified) and generates a plot showing the number of aggregated models and validations +per round. This visualization helps in understanding client participation patterns and the +overall health of the federated learning process across training rounds. + +The script can be used to monitor client engagement and identify potential issues with +client participation or model validation in the federated learning network. +""" + +import click +from config import settings +from fedn import APIClient +import matplotlib.pyplot as plt +from datetime import datetime, timezone + +def get_latest_session_id(client): + """Get the most recent session ID from the API.""" + sessions = client.get_sessions() + if "result" in sessions and sessions["result"]: + # Sort sessions by committed_at in descending order + # Parse the date string to ensure proper date comparison + + def parse_date(date_str): + try: + return datetime.strptime(date_str, "%a, %d %b %Y %H:%M:%S %Z") + except (ValueError, TypeError): + # Return a very old date as fallback for invalid dates + return datetime(1900, 1, 1, tzinfo=timezone.utc) + + sorted_sessions = sorted( + sessions["result"], + key=lambda x: parse_date(x.get("committed_at", "")), + reverse=True + ) + for session in sorted_sessions: + print(f"Session: {session['session_id']} - Committed at: {session['committed_at']}") + return sorted_sessions[0]["session_id"] + return None + +def plot_aggregation_data(session_id): + """Plot aggregation data for the specified session.""" + client = APIClient( + host=settings["DISCOVER_HOST"], + port=settings["DISCOVER_PORT"], + secure=settings["SECURE"], + verify=settings["VERIFY"], + token=settings["ADMIN_TOKEN"], + ) + + if not session_id: + session_id = get_latest_session_id(client) + if not session_id: + print("No sessions found.") + return + print(f"Using latest session: {session_id}") + + rounds = client.get_rounds() + round_ids = [] + nr_aggregated_models = [] + nr_validations_per_round = [] + + for round in rounds["result"]: + if "combiners" in round and round["round_config"]["session_id"] == session_id: + round_ids.append(round["round_id"]) + nr_aggregated_models.append(round["combiners"][0]["data"]["aggregation_time"]["nr_aggregated_models"]) + + model_id = round["round_config"]["model_id"] + validations = client.get_validations(model_id=model_id) + nr_validations_per_round.append(len(validations["result"])) + + print(f"Round IDs: {round_ids}") + print(f"Number of aggregated models: {nr_aggregated_models}") + print(f"Number of validations per round: {nr_validations_per_round}") + + # Create the line plot + plt.figure(figsize=(10, 6)) + plt.plot(round_ids, nr_aggregated_models, marker="o", label="Aggregated Models") + plt.plot(round_ids, nr_validations_per_round, marker="s", label="Validations") + + # Customize the plot + plt.xlabel("Round ID") + plt.ylabel("Count") + plt.title(f"Session ID: {session_id}") + plt.legend() + plt.grid(True) + + # Rotate x-axis labels for better readability + plt.xticks(rotation=45) + + # Adjust layout to prevent label cutoff + plt.tight_layout() + + # Show the plot + plt.show() + +if __name__ == "__main__": + @click.command() + @click.option("--session-id", "-s", default=None, help="Session ID to analyze") + def main(session_id): + """Plot the number of aggregated models and validations per round for a session. + + If no session ID is provided, the most recent session will be used. + """ + plot_aggregation_data(session_id) + + main() diff --git a/examples/async-clients/client_status.py b/examples/async-clients/client_status.py new file mode 100644 index 000000000..fbb29b7d5 --- /dev/null +++ b/examples/async-clients/client_status.py @@ -0,0 +1,73 @@ +"""This script monitors and records the status of FEDn clients. + +It periodically queries the FEDn API to get the status of all clients and records +each client's status in a CSV file. The CSV format makes it easy to import the data +into plotting tools for visualization and analysis of client availability patterns over time. + +The script runs continuously, collecting data at regular intervals specified by the user. +Each line in the CSV contains: timestamp, client_name, status. +""" + +import time +import csv +import click +import os +from datetime import datetime +from config import settings +from fedn import APIClient + +@click.command() +@click.option("--csv-filename", "-f", default=None, + help="CSV filename to store client status data. Defaults to a timestamped filename.") +@click.option("--interval", "-i", default=5, + help="Time interval in seconds between status checks. Default is 5 seconds.") +def monitor_client_status(csv_filename, interval): + """Monitor and record the status of FEDn clients. + + Records one line per client per iteration with timestamp, client_name, and status. + """ + # Ensure logs directory exists + logs_dir = "logs" + os.makedirs(logs_dir, exist_ok=True) + + # Set default filename with timestamp if not provided + if not csv_filename: + csv_filename = f"client_status_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.csv" + + # Prepend logs directory to filename + csv_path = os.path.join(logs_dir, csv_filename) + + api_client = APIClient( + host=settings["DISCOVER_HOST"], + port=settings["DISCOVER_PORT"], + secure=settings["SECURE"], + verify=settings["VERIFY"], + token=settings["ADMIN_TOKEN"], + ) + + # Create header row + header = ["timestamp", "client_name", "status"] + + # Create/open CSV file with header if it doesn't exist + with open(csv_path, "a", newline="") as f: + writer = csv.writer(f) + if f.tell() == 0: # Check if file is empty + writer.writerow(header) + + while True: + fl_clients = api_client.get_clients() + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Write one row per client + with open(csv_path, "a", newline="") as f: + writer = csv.writer(f) + + for client in fl_clients["result"]: + client_name = client["name"] + status = client["status"] + writer.writerow([timestamp, client_name, status]) + + time.sleep(interval) + +if __name__ == "__main__": + monitor_client_status() diff --git a/examples/async-clients/config.py b/examples/async-clients/config.py index 251e6f19d..a3abef296 100644 --- a/examples/async-clients/config.py +++ b/examples/async-clients/config.py @@ -1,3 +1,6 @@ +import json +from pathlib import Path + # Environment configurations LOCAL_CONFIG = { "DISCOVER_HOST": "127.0.0.1", @@ -10,22 +13,22 @@ } REMOTE_CONFIG = { - "DISCOVER_HOST": "fedn.scaleoutsystems.com/", + "DISCOVER_HOST": "api.studio.scaleoutplatform.com/asyncclitest-zmh-fedn-reducer", "DISCOVER_PORT": None, "IS_LOCAL": False, "SECURE": True, "VERIFY": True, - "CLIENT_TOKEN": None, - "ADMIN_TOKEN": None, + "CLIENT_TOKEN": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzQ4MDkyODgyLCJpYXQiOjE3NDU1MDA4ODIsImp0aSI6IjUxZGUxNzhiN2Y4OTQ3ZWJiYjNkNTg0ODYyNzBmYTFmIiwidXNlcl9pZCI6NTgsImNyZWF0b3IiOiJzaWd2YXJkQHNjYWxlb3V0c3lzdGVtcy5jb20iLCJyb2xlIjoiY2xpZW50IiwicHJvamVjdF9zbHVnIjoiYXN5bmNjbGl0ZXN0LXptaCJ9.lhnb-7n80fqsprKuF5M4qOdVAlJlsaXEgXG_yAY0n10", + "ADMIN_TOKEN": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzQ4MDkyODY3LCJpYXQiOjE3NDU1MDA4NjcsImp0aSI6ImU4NDFjMjVlZDM2NTRlNDc4NmIxN2E5Yjg0MzU0NjM5IiwidXNlcl9pZCI6NTgsImNyZWF0b3IiOiJzaWd2YXJkQHNjYWxlb3V0c3lzdGVtcy5jb20iLCJyb2xlIjoiYWRtaW4iLCJwcm9qZWN0X3NsdWciOiJhc3luY2NsaXRlc3Qtem1oIn0.xa9r413N_FyGxo7kvG_8iGlSf1z-LnJucoF41aRXris", } # Common settings that don't change between environments COMMON_SETTINGS = { - "N_CLIENTS": 10, + "N_CLIENTS": 200, "N_EPOCHS": 10, - "N_ROUNDS": 50, - "N_SESSIONS": 1, - "N_CYCLES": 1, + "N_ROUNDS": 100, + "N_SESSIONS": 6, + "N_CYCLES": 30, "CLIENTS_MAX_DELAY": 10, "CLIENTS_ONLINE_FOR_SECONDS": 120, } @@ -35,3 +38,22 @@ # Combine the selected environment config with common settings settings = {**COMMON_SETTINGS, **(LOCAL_CONFIG if USE_LOCAL else REMOTE_CONFIG)} + +# Only try to load tokens for remote configuration +if not USE_LOCAL: + tokens_file = Path(__file__).parent / "tokens.json" + if tokens_file.exists(): + try: + with open(tokens_file, "r") as f: + tokens = json.load(f) + + # Use the discover host as the key to find the right tokens + discover_host = settings["DISCOVER_HOST"] + if discover_host in tokens: + settings.update({k: v for k, v in tokens[discover_host].items() if k in settings}) + else: + print(f"Warning: No tokens found for host '{discover_host}' in tokens.json") + except Exception as e: + print(f"Warning: Could not load tokens from {tokens_file}: {e}") + else: + print(f"Warning: No tokens file found at {tokens_file}. Required for remote configuration.") diff --git a/examples/async-clients/init_fedn.py b/examples/async-clients/init_fedn.py index e4547b5bc..3701a644c 100644 --- a/examples/async-clients/init_fedn.py +++ b/examples/async-clients/init_fedn.py @@ -1,13 +1,26 @@ +import click from config import settings from fedn import APIClient -client = APIClient( - host=settings["DISCOVER_HOST"], - port=settings["DISCOVER_PORT"], - secure=settings["SECURE"], - verify=settings["VERIFY"], - token=settings["ADMIN_TOKEN"], -) -result = client.set_active_model("seed.npz") -print(result["message"]) +def init_fedn(seed_path): + client = APIClient( + host=settings["DISCOVER_HOST"], + port=settings["DISCOVER_PORT"], + secure=settings["SECURE"], + verify=settings["VERIFY"], + token=settings["ADMIN_TOKEN"], + ) + + result = client.set_active_model(seed_path) + print(result["message"]) + + +if __name__ == "__main__": + @click.command() + @click.argument("seed_path", type=str, default="seed.npz") + def main(seed_path): + """Initialize FEDn with a seed model from the specified path.""" + init_fedn(seed_path) + + main() diff --git a/examples/async-clients/init_seed.py b/examples/async-clients/init_seed.py index ec20651d7..8cc4b4ca2 100644 --- a/examples/async-clients/init_seed.py +++ b/examples/async-clients/init_seed.py @@ -1,9 +1,9 @@ -import sys import numpy as np from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.neural_network import MLPClassifier +import click from fedn.utils.helpers.helpers import get_helper @@ -49,6 +49,12 @@ def save_parameters(model, out_path): helper.save(parameters, out_path) +def load_parameters(model_path): + helper = get_helper(HELPER_MODULE) + parameters = np.load(model_path) + + + return parameters def init_seed(out_path="seed.npz"): """Initialize seed model. @@ -61,5 +67,12 @@ def init_seed(out_path="seed.npz"): save_parameters(model, out_path) + if __name__ == "__main__": - init_seed(sys.argv[1]) + @click.command() + @click.argument("out_path", type=str, default="seed.npz") + def main(out_path): + """Initialize a seed model and save it to the specified path.""" + init_seed(out_path) + + main() diff --git a/examples/async-clients/run_clients.py b/examples/async-clients/run_clients.py index b76893592..9adbd6d36 100644 --- a/examples/async-clients/run_clients.py +++ b/examples/async-clients/run_clients.py @@ -1,10 +1,6 @@ -"""This scripts starts N_CLIENTS using the SDK. +"""This scripts starts N_CLIENTS clients using the SDK. - - - - -If you are running with a local deploy of FEDn +If you are running with a local deployment of FEDn using docker compose, you need to make sure that clients are able to resolve the name "combiner" to 127.0.0.1 @@ -25,9 +21,11 @@ import numpy as np from init_seed import compile_model, make_data from sklearn.metrics import accuracy_score +import click from config import settings from fedn import FednClient +from fedn.network.clients.fedn_client import GrpcConnectionOptions HELPER_MODULE = "numpyhelper" @@ -60,15 +58,15 @@ def load_model(model_bytes_io: BytesIO): return model +def callback_train(client_id, client_name): + def on_train(in_model, client_settings): + print("Running training callback...") + model = load_model(in_model) -def on_train(in_model, client_settings): - print("Running training callback...") - model = load_model(in_model) - - X_train, y_train, _, _ = make_data() - epochs = settings["N_EPOCHS"] - for i in range(epochs): - model.partial_fit(X_train, y_train) + X_train, y_train, _, _ = make_data() + epochs = settings["N_EPOCHS"] + for i in range(epochs): + model.partial_fit(X_train, y_train) # Prepare updated model parameters updated_parameters = model.coefs_ + model.intercepts_ @@ -86,9 +84,13 @@ def on_train(in_model, client_settings): }, } - metadata = {"training_metadata": training_metadata} + metadata = {"training_metadata": training_metadata} + metadata = {"training_metadata": training_metadata} - return out_model, metadata + return out_model, metadata + return on_train + return out_model, metadata + return on_train def on_validate(in_model): @@ -102,15 +104,29 @@ def on_validate(in_model): return metrics -def run_client(online_for=120, name="client", client_id=None): - """Simulates a client that starts and stops - at random intervals. +def run_client(name="client", client_id=None, no_discovery=False, intermittent=False, online_for=120): + """Run a FEDn client with configurable connection options. + + The client can either connect directly to a combiner or use the discovery service. + It can also run in continuous or intermittent mode. - The client will start after a random time 'mean_delay', - stay online for 'online_for' seconds (deterministic), - then disconnect. + In intermittent mode, the client will: + 1. Wait a random delay between 0 and CLIENTS_MAX_DELAY seconds + 2. Connect to the combiner + 3. Stay online for online_for seconds + 4. Disconnect + 5. Repeat this cycle N_CYCLES times - This is repeated for N_CYCLES. + In continuous mode, the client will: + 1. Connect to the combiner + 2. Stay connected until the program exits + + Args: + name (str, optional): Name of the client. Defaults to "client". + client_id (str, optional): Unique ID for the client. If None, a UUID will be generated. + no_discovery (bool, optional): If True, connect directly to combiner. If False, use discovery service. Defaults to False. + intermittent (bool, optional): If True, use intermittent connection mode. Defaults to False. + online_for (int, optional): In intermittent mode, how long to stay connected in seconds. Defaults to 120. """ if client_id is None: @@ -125,23 +141,37 @@ def run_client(online_for=120, name="client", client_id=None): fl_client.set_name(name) fl_client.set_client_id(client_id) + if no_discovery: + combiner_config = GrpcConnectionOptions(host=settings["COMBINER_HOST"], port=settings["COMBINER_PORT"]) + else: controller_config = { "name": fl_client.name, "client_id": fl_client.client_id, "package": "local", - "preferred_combiner": "", } url = get_api_url(host=settings["DISCOVER_HOST"], port=settings["DISCOVER_PORT"], secure=settings["SECURE"]) result, combiner_config = fl_client.connect_to_api(url, settings["CLIENT_TOKEN"], controller_config) + #combiner_config.host = "100.84.229.36" + fl_client.init_grpchandler(config=combiner_config, client_name=fl_client.client_id, token=settings["CLIENT_TOKEN"]) fl_client.init_grpchandler(config=combiner_config, client_name=fl_client.client_id, token=settings["CLIENT_TOKEN"]) - threading.Thread(target=fl_client.run, daemon=True).start() - time.sleep(online_for) - fl_client.grpc_handler._disconnect() + if intermittent: + for i in range(settings["N_CYCLES"]): + if i != 0: + fl_client.grpc_handler._reconnect() + threading.Thread(target=fl_client.run, daemon=True).start() + time.sleep(online_for) + fl_client.grpc_handler._disconnect() + + # Sample a delay until the client reconnects + delay = np.random.randint(0, settings["CLIENTS_MAX_DELAY"]) + time.sleep(delay) + else: + fl_client.run() if __name__ == "__main__": # We start N_CLIENTS independent client processes @@ -158,5 +188,7 @@ def run_client(online_for=120, name="client", client_id=None): processes.append(p) p.start() - for p in processes: - p.join() + for p in processes: + p.join() + + main() diff --git a/examples/async-clients/run_experiment.py b/examples/async-clients/run_experiment.py index 92a14cb80..773abce99 100644 --- a/examples/async-clients/run_experiment.py +++ b/examples/async-clients/run_experiment.py @@ -1,3 +1,15 @@ +"""This script runs federated learning training sessions in FEDn. + +It initiates a configurable number of sequential training sessions, each with a specified +number of rounds. The script connects to a FEDn network using the API client, starts each +session with the appropriate configuration, and monitors the session until completion. + +This is useful for automating experiments with different federated learning configurations +and for running multiple training sessions in sequence without manual intervention. +The script uses settings from the config module to determine the number of sessions and +rounds to run. +""" + import time import uuid @@ -16,11 +28,12 @@ # Run six sessions, each with 100 rounds. for s in range(settings["N_SESSIONS"]): active_model = client.get_active_model() + print(active_model) session_config = { "helper": "numpyhelper", "name": f"async-test-{s+1}-{str(uuid.uuid4())[:4]}", "aggregator": "fedavg", - "round_timeout": 20, + "round_timeout": 60, "rounds": settings["N_ROUNDS"], "validate": True, "model_id": active_model["model"], diff --git a/examples/async-clients/threaded_run_clients.py b/examples/async-clients/threaded_run_clients.py new file mode 100644 index 000000000..2a270e315 --- /dev/null +++ b/examples/async-clients/threaded_run_clients.py @@ -0,0 +1,364 @@ +"""This scripts starts N_CLIENTS using the SDK. + + +If you are running with a local deploy of FEDn +using docker compose, you need to make sure that clients +are able to resolve the name "combiner" to 127.0.0.1 + +One way to accomplish this is to edit your /etc/host, +adding the line: + +combiner 127.0.0.1 + +(this requires root previliges) +""" + +import threading +import time +import uuid +from io import BytesIO +from multiprocessing import Process + +import numpy as np +from init_seed import compile_model, make_data +from sklearn.metrics import accuracy_score + +import random +from config import settings +from fedn import FednClient +import json + +HELPER_MODULE = "numpyhelper" + +log_lock = threading.Lock() + +LOG_FILE = "/Users/sigvard/Desktop/client_update.json" # Logging file for all clients + + +def get_api_url(host: str, port: int = None, secure: bool = False): + if secure: + url = f"https://{host}:{port}" if port else f"https://{host}" + else: + url = f"http://{host}:{port}" if port else f"http://{host}" + if not url.endswith("/"): + url += "/" + return url + + +def load_parameters(model_bytes_io: BytesIO): + """Load model parameters from a BytesIO object.""" + model_bytes_io.seek(0) # Ensure we're at the start of the BytesIO object + a = np.load(model_bytes_io) + weights = [a[str(i)] for i in range(len(a.files))] + return weights + + +def load_model(model_bytes_io: BytesIO): + parameters = load_parameters(model_bytes_io) + + model = compile_model() + n = len(parameters) // 2 + model.coefs_ = parameters[:n] + model.intercepts_ = parameters[n:] + + return model + +def callback_train(client_id, client_name): + def on_train(in_model, client_settings): + print(in_model) + start_time = time.perf_counter() + model = load_model(in_model) + X_train, y_train, _, _ = make_data() + epochs = settings["N_EPOCHS"] + for i in range(epochs): + model.partial_fit(X_train, y_train) + + # Prepare updated model parameters + updated_parameters = model.coefs_ + model.intercepts_ + out_model = BytesIO() + np.savez_compressed(out_model, **{str(i): w for i, w in enumerate(updated_parameters)}) + out_model.seek(0) + + # Metadata needed for aggregation server side + training_metadata = { + "num_examples": len(X_train), + "training_metadata": { + "epochs": epochs, + "batch_size": len(X_train), + "learning_rate": model.learning_rate_init, + }, + } + + metadata = {"training_metadata": training_metadata} + + train_time = time.perf_counter() - start_time + + log_entry = {"client_id": client_id, + "client_name": client_name, + "train_time": train_time, + "time stamp": time.time() + } + + + with log_lock: + with open(LOG_FILE, "a") as f: + f.write(json.dumps(log_entry) + "\n") + + + return out_model, metadata + return on_train + +def on_validate(in_model): + model = load_model(in_model) + + X_train, y_train, X_test, y_test = make_data() + + # JSON schema + metrics = {"validation_accuracy": accuracy_score(y_test, model.predict(X_test)), "training_accuracy": accuracy_score(y_train, model.predict(X_train))} + + return metrics + + +def run_client(online_for=120, name="client", client_id=None): + """Simulates a client that starts and stops + at random intervals. + + The client will start after a random time 'mean_delay', + stay online for 'online_for' seconds (deterministic), + then disconnect. + + This is repeated for N_CYCLES. + + """ + if client_id is None: + client_id = str(uuid.uuid4()) + + for i in range(settings["N_CYCLES"]): + # Sample a delay until the client starts + t_start = np.random.randint(1, settings["CLIENTS_MAX_DELAY"]) + time.sleep(t_start) + + fl_client = FednClient(train_callback=callback_train(client_id, name), validate_callback=on_validate) + fl_client.set_name(name) + fl_client.set_client_id(client_id) + + controller_config = { + "name": fl_client.name, + "client_id": fl_client.client_id, + "package": "local", + "preferred_combiner": "", + } + + url = get_api_url(host=settings["DISCOVER_HOST"], port=settings["DISCOVER_PORT"], secure=settings["SECURE"]) + + result, combiner_config = fl_client.connect_to_api(url, settings["CLIENT_TOKEN"], controller_config) + #combiner_config.host = "100.84.229.36" + combiner_config.host = "127.0.0.1" + fl_client.init_grpchandler(config=combiner_config, client_name=fl_client.client_id, token=settings["CLIENT_TOKEN"]) + + threading.Thread(target=fl_client.run, daemon=True).start() + time.sleep(online_for) + fl_client.grpc_handler._disconnect() + + +if __name__ == "__main__": + # We start N_CLIENTS independent client threads + threads = [] + for i in range(settings["N_CLIENTS"]): + time.sleep(0.1) + t = threading.Thread( + target=run_client, + args=( + settings["CLIENTS_ONLINE_FOR_SECONDS"], + "client{}".format(i + 1), + str(uuid.uuid4())), + daemon=False + ) + + threads.append(t) + t.start() + + for t in threads: + t.join() +"""This scripts starts N_CLIENTS using the SDK. + + +If you are running with a local deploy of FEDn +using docker compose, you need to make sure that clients +are able to resolve the name "combiner" to 127.0.0.1 + +One way to accomplish this is to edit your /etc/host, +adding the line: + +combiner 127.0.0.1 + +(this requires root previliges) +""" + +import threading +import time +import uuid +from io import BytesIO +from multiprocessing import Process + +import numpy as np +from init_seed import compile_model, make_data +from sklearn.metrics import accuracy_score + +import random +from config import settings +from fedn import FednClient +import json + +HELPER_MODULE = "numpyhelper" + +log_lock = threading.Lock() + +LOG_FILE = "/Users/sigvard/Desktop/client_update.json" # Logging file for all clients + + +def get_api_url(host: str, port: int = None, secure: bool = False): + if secure: + url = f"https://{host}:{port}" if port else f"https://{host}" + else: + url = f"http://{host}:{port}" if port else f"http://{host}" + if not url.endswith("/"): + url += "/" + return url + + +def load_parameters(model_bytes_io: BytesIO): + """Load model parameters from a BytesIO object.""" + model_bytes_io.seek(0) # Ensure we're at the start of the BytesIO object + a = np.load(model_bytes_io) + weights = [a[str(i)] for i in range(len(a.files))] + return weights + + +def load_model(model_bytes_io: BytesIO): + parameters = load_parameters(model_bytes_io) + + model = compile_model() + n = len(parameters) // 2 + model.coefs_ = parameters[:n] + model.intercepts_ = parameters[n:] + + return model + +def callback_train(client_id, client_name): + def on_train(in_model, client_settings): + print(in_model) + start_time = time.perf_counter() + model = load_model(in_model) + X_train, y_train, _, _ = make_data() + epochs = settings["N_EPOCHS"] + for i in range(epochs): + model.partial_fit(X_train, y_train) + + # Prepare updated model parameters + updated_parameters = model.coefs_ + model.intercepts_ + out_model = BytesIO() + np.savez_compressed(out_model, **{str(i): w for i, w in enumerate(updated_parameters)}) + out_model.seek(0) + + # Metadata needed for aggregation server side + training_metadata = { + "num_examples": len(X_train), + "training_metadata": { + "epochs": epochs, + "batch_size": len(X_train), + "learning_rate": model.learning_rate_init, + }, + } + + metadata = {"training_metadata": training_metadata} + + train_time = time.perf_counter() - start_time + + log_entry = {"client_id": client_id, + "client_name": client_name, + "train_time": train_time, + "time stamp": time.time() + } + + + with log_lock: + with open(LOG_FILE, "a") as f: + f.write(json.dumps(log_entry) + "\n") + + + return out_model, metadata + return on_train + +def on_validate(in_model): + model = load_model(in_model) + + X_train, y_train, X_test, y_test = make_data() + + # JSON schema + metrics = {"validation_accuracy": accuracy_score(y_test, model.predict(X_test)), "training_accuracy": accuracy_score(y_train, model.predict(X_train))} + + return metrics + + +def run_client(online_for=120, name="client", client_id=None): + """Simulates a client that starts and stops + at random intervals. + + The client will start after a random time 'mean_delay', + stay online for 'online_for' seconds (deterministic), + then disconnect. + + This is repeated for N_CYCLES. + + """ + if client_id is None: + client_id = str(uuid.uuid4()) + + for i in range(settings["N_CYCLES"]): + # Sample a delay until the client starts + t_start = np.random.randint(1, settings["CLIENTS_MAX_DELAY"]) + time.sleep(t_start) + + fl_client = FednClient(train_callback=callback_train(client_id, name), validate_callback=on_validate) + fl_client.set_name(name) + fl_client.set_client_id(client_id) + + controller_config = { + "name": fl_client.name, + "client_id": fl_client.client_id, + "package": "local", + "preferred_combiner": "", + } + + url = get_api_url(host=settings["DISCOVER_HOST"], port=settings["DISCOVER_PORT"], secure=settings["SECURE"]) + + result, combiner_config = fl_client.connect_to_api(url, settings["CLIENT_TOKEN"], controller_config) + #combiner_config.host = "100.84.229.36" + combiner_config.host = "127.0.0.1" + fl_client.init_grpchandler(config=combiner_config, client_name=fl_client.client_id, token=settings["CLIENT_TOKEN"]) + + threading.Thread(target=fl_client.run, daemon=True).start() + time.sleep(online_for) + fl_client.grpc_handler._disconnect() + + +if __name__ == "__main__": + # We start N_CLIENTS independent client threads + threads = [] + for i in range(settings["N_CLIENTS"]): + time.sleep(0.1) + t = threading.Thread( + target=run_client, + args=( + settings["CLIENTS_ONLINE_FOR_SECONDS"], + "client{}".format(i + 1), + str(uuid.uuid4())), + daemon=False + ) + + threads.append(t) + t.start() + + for t in threads: + t.join() diff --git a/examples/async-clients/threads_run_clients.py b/examples/async-clients/threads_run_clients.py new file mode 100644 index 000000000..f0ee16406 --- /dev/null +++ b/examples/async-clients/threads_run_clients.py @@ -0,0 +1,182 @@ +"""This scripts starts N_CLIENTS using the SDK. + + +If you are running with a local deploy of FEDn +using docker compose, you need to make sure that clients +are able to resolve the name "combiner" to 127.0.0.1 + +One way to accomplish this is to edit your /etc/host, +adding the line: + +combiner 127.0.0.1 + +(this requires root previliges) +""" + +import threading +import time +import uuid +from io import BytesIO +from multiprocessing import Process + +import numpy as np +from init_seed import compile_model, make_data +from sklearn.metrics import accuracy_score + +import random +from config import settings +from fedn import FednClient +import json + +HELPER_MODULE = "numpyhelper" + +log_lock = threading.Lock() + +LOG_FILE = "/Users/sigvard/Desktop/client_update.json" # Logging file for all clients + + +def get_api_url(host: str, port: int = None, secure: bool = False): + if secure: + url = f"https://{host}:{port}" if port else f"https://{host}" + else: + url = f"http://{host}:{port}" if port else f"http://{host}" + if not url.endswith("/"): + url += "/" + return url + + +def load_parameters(model_bytes_io: BytesIO): + """Load model parameters from a BytesIO object.""" + model_bytes_io.seek(0) # Ensure we're at the start of the BytesIO object + a = np.load(model_bytes_io) + weights = [a[str(i)] for i in range(len(a.files))] + return weights + + +def load_model(model_bytes_io: BytesIO): + parameters = load_parameters(model_bytes_io) + + model = compile_model() + n = len(parameters) // 2 + model.coefs_ = parameters[:n] + model.intercepts_ = parameters[n:] + + return model + +def callback_train(client_id, client_name): + def on_train(in_model, client_settings): + print(in_model) + start_time = time.perf_counter() + model = load_model(in_model) + X_train, y_train, _, _ = make_data() + epochs = settings["N_EPOCHS"] + for i in range(epochs): + model.partial_fit(X_train, y_train) + + # Prepare updated model parameters + updated_parameters = model.coefs_ + model.intercepts_ + out_model = BytesIO() + np.savez_compressed(out_model, **{str(i): w for i, w in enumerate(updated_parameters)}) + out_model.seek(0) + + # Metadata needed for aggregation server side + training_metadata = { + "num_examples": len(X_train), + "training_metadata": { + "epochs": epochs, + "batch_size": len(X_train), + "learning_rate": model.learning_rate_init, + }, + } + + metadata = {"training_metadata": training_metadata} + + train_time = time.perf_counter() - start_time + + log_entry = {"client_id": client_id, + "client_name": client_name, + "train_time": train_time, + "time stamp": time.time() + } + + + with log_lock: + with open(LOG_FILE, "a") as f: + f.write(json.dumps(log_entry) + "\n") + + + return out_model, metadata + return on_train + +def on_validate(in_model): + model = load_model(in_model) + + X_train, y_train, X_test, y_test = make_data() + + # JSON schema + metrics = {"validation_accuracy": accuracy_score(y_test, model.predict(X_test)), "training_accuracy": accuracy_score(y_train, model.predict(X_train))} + + return metrics + + +def run_client(online_for=120, name="client", client_id=None): + """Simulates a client that starts and stops + at random intervals. + + The client will start after a random time 'mean_delay', + stay online for 'online_for' seconds (deterministic), + then disconnect. + + This is repeated for N_CYCLES. + + """ + if client_id is None: + client_id = str(uuid.uuid4()) + + for i in range(settings["N_CYCLES"]): + # Sample a delay until the client starts + t_start = np.random.randint(1, settings["CLIENTS_MAX_DELAY"]) + time.sleep(t_start) + + fl_client = FednClient(train_callback=callback_train(client_id, name), validate_callback=on_validate) + fl_client.set_name(name) + fl_client.set_client_id(client_id) + + controller_config = { + "name": fl_client.name, + "client_id": fl_client.client_id, + "package": "local", + "preferred_combiner": "", + } + + url = get_api_url(host=settings["DISCOVER_HOST"], port=settings["DISCOVER_PORT"], secure=settings["SECURE"]) + + result, combiner_config = fl_client.connect_to_api(url, settings["CLIENT_TOKEN"], controller_config) + #combiner_config.host = "100.84.229.36" + combiner_config.host = "127.0.0.1" + fl_client.init_grpchandler(config=combiner_config, client_name=fl_client.client_id, token=settings["CLIENT_TOKEN"]) + + threading.Thread(target=fl_client.run, daemon=True).start() + time.sleep(online_for) + fl_client.grpc_handler._disconnect() + + +if __name__ == "__main__": + # We start N_CLIENTS independent client threads + threads = [] + for i in range(settings["N_CLIENTS"]): + time.sleep(0.01) + t = threading.Thread( + target=run_client, + args=( + settings["CLIENTS_ONLINE_FOR_SECONDS"], + "client{}".format(i + 1), + str(uuid.uuid4())), + daemon=False + ) + + threads.append(t) + t.start() + + for t in threads: + t.join() diff --git a/examples/load-test/client/model.py b/examples/load-test/client/model.py index 765dd68fa..635fb7466 100644 --- a/examples/load-test/client/model.py +++ b/examples/load-test/client/model.py @@ -7,9 +7,9 @@ from fedn.utils.helpers.helpers import get_helper HELPER_MODULE = "numpyhelper" -ARRAY_SIZE_FACTOR = int(os.environ.get("FEDN_ARRAY_SIZE_FACTOR", 1)) +ARRAY_SIZE_FACTOR = float(os.environ.get("FEDN_ARRAY_SIZE_FACTOR", 1)) # 144 MB * ARRAY_SIZE_FACTOR -ARRAY_SIZE = 20000000 * ARRAY_SIZE_FACTOR +ARRAY_SIZE = int(20000000 * ARRAY_SIZE_FACTOR) def save_model(weights, out_path): diff --git a/examples/mnist-pytorch/client/data.py b/examples/mnist-pytorch/client/data.py index f10fd1558..d500ebbff 100644 --- a/examples/mnist-pytorch/client/data.py +++ b/examples/mnist-pytorch/client/data.py @@ -10,14 +10,13 @@ def get_data(out_dir="data"): # Make dir if necessary - if not os.path.exists(out_dir): - os.mkdir(out_dir) + os.makedirs(out_dir, exist_ok=True) # Only download if not already downloaded - if not os.path.exists(f"{out_dir}/train"): - torchvision.datasets.MNIST(root=f"{out_dir}/train", transform=torchvision.transforms.ToTensor, train=True, download=True) - if not os.path.exists(f"{out_dir}/test"): - torchvision.datasets.MNIST(root=f"{out_dir}/test", transform=torchvision.transforms.ToTensor, train=False, download=True) + if not os.path.exists(f"{out_dir}/MNIST/processed/training.pt"): + torchvision.datasets.MNIST(root=out_dir, transform=torchvision.transforms.ToTensor(), train=True, download=True) + if not os.path.exists(f"{out_dir}/MNIST/processed/test.pt"): + torchvision.datasets.MNIST(root=out_dir, transform=torchvision.transforms.ToTensor(), train=False, download=True) def load_data(data_path, is_train=True): @@ -61,12 +60,11 @@ def split(out_dir="data"): n_splits = int(os.environ.get("FEDN_NUM_DATA_SPLITS", 2)) # Make dir - if not os.path.exists(f"{out_dir}/clients"): - os.mkdir(f"{out_dir}/clients") + os.makedirs(f"{out_dir}/clients", exist_ok=True) # Load and convert to dict - train_data = torchvision.datasets.MNIST(root=f"{out_dir}/train", transform=torchvision.transforms.ToTensor, train=True) - test_data = torchvision.datasets.MNIST(root=f"{out_dir}/test", transform=torchvision.transforms.ToTensor, train=False) + train_data = torchvision.datasets.MNIST(root=out_dir, transform=torchvision.transforms.ToTensor(), train=True) + test_data = torchvision.datasets.MNIST(root=out_dir, transform=torchvision.transforms.ToTensor(), train=False) data = { "x_train": splitset(train_data.data, n_splits), "y_train": splitset(train_data.targets, n_splits), @@ -76,9 +74,8 @@ def split(out_dir="data"): # Make splits for i in range(n_splits): - subdir = f"{out_dir}/clients/{str(i+1)}" - if not os.path.exists(subdir): - os.mkdir(subdir) + subdir = f"{out_dir}/clients/{str(i + 1)}" + os.makedirs(subdir, exist_ok=True) torch.save( { "x_train": data["x_train"][i], diff --git a/examples/server-functions/README.rst b/examples/server-functions/README.rst index b2fb017c8..7ccf2809e 100644 --- a/examples/server-functions/README.rst +++ b/examples/server-functions/README.rst @@ -1,6 +1,14 @@ -FEDn Project: Server functions toy example ------------------------------ +FEDn Project: Server functions +============================== +This example demonstrates how to use custom server functions (in ``server_functions.py``) to: + +- **Leverage client attributes** to select specific clients for training _(beta feature; requires ``send_attributes.py``)_. +- **Send dynamic, customizable payloads** from the server to clients via Python dictionaries. +- **Implement custom aggregation logic**. + +Additionally, for large-scale experiments, ``sf_incremental_aggregation.py`` demonstrates +**memory-safe incremental averaging** using server functions. For details on the functionality of server-functions see either the file server_functions.py, the docs https://docs.scaleoutsystems.com/en/stable/serverfunctions.html or the youtube video diff --git a/examples/server-functions/send_attributes.py b/examples/server-functions/send_attributes.py new file mode 100644 index 000000000..6fd5a04d5 --- /dev/null +++ b/examples/server-functions/send_attributes.py @@ -0,0 +1,38 @@ +import argparse +import json +import random +import time + +from fedn import APIClient + + +def parse_args(): + parser = argparse.ArgumentParser(description="Send a random 'charging' attribute to the controller for a client.") + parser.add_argument("--api-url", required=True, help="Base URL of the API server (the same api-url that is used to connect clients)") + parser.add_argument("--admin-token", required=True, help="Authentication token for the API (admin token which can be generated from the studio UI)") + parser.add_argument("--client-id", required=True, help="client ID for the current client (the same api-url that is used to connect the client)") + parser.add_argument("--delay", required=False, default=30, help="Delay between sending attributes.") + return parser.parse_args() + + +def main(): + # --------- NOTE ------------ + # run this script with --token (fetch admin token from studio) + # if this script is not running the server functions example will default to picking clients. + args = parse_args() + client = APIClient(host=args.api_url, token=args.admin_token, secure=True, verify=True) + while True: + # Prepare a random charging status + attribute_payload = {"key": "charging", "value": random.choice(["True", "False"]), "sender": {"name": "", "role": "", "client_id": args.client_id}} + + # Send to server + try: + result = client.add_attributes(attribute_payload) + print(json.dumps(result, indent=2)) + except Exception as e: + print(f"Failed to send attributes: {e}") + time.sleep(args.delay) + + +if __name__ == "__main__": + main() diff --git a/examples/server-functions/server_functions.py b/examples/server-functions/server_functions.py index 1378e4f7e..aabfa9a25 100644 --- a/examples/server-functions/server_functions.py +++ b/examples/server-functions/server_functions.py @@ -1,4 +1,4 @@ -from fedn.network.combiner.hooks.allowed_import import Dict, List, ServerFunctionsBase, Tuple, np, random +from fedn.network.combiner.hooks.allowed_import import Dict, List, ServerFunctionsBase, Tuple, api_client, np # See allowed_imports for what packages you can use in this class. @@ -13,10 +13,31 @@ def __init__(self) -> None: # Skip any function to use the default FEDn implementation for the function. # Called first in the beggining of a round to select clients. - def client_selection(self, client_ids: List[str]) -> List: - # Pick 10 random clients - client_ids = random.sample(client_ids, min(len(client_ids), 10)) # noqa: F405 - return client_ids + def client_selection(self, client_ids: list[str]) -> list[str]: + """Select clients that are currently charging. + If no attributes exist (empty response) or service error, default to selecting all. + """ + try: + attrs_map = api_client.get_current_attributes(client_ids) + except Exception as e: + print(f"Warning: unable to fetch attributes ({e}), selecting all clients") + return client_ids + + selected = [] + charging_count = 0 + for cid in client_ids: + client_attrs = attrs_map.get(cid) or {} + charging = client_attrs.get("charging", None) + # Default to select to not depend on beta version of attributes. + if charging == "True" or charging is None: + selected.append(cid) + charging_count += 1 + if len(selected) < 20: + print(f"Selected clients: {selected}.") + else: + print(f"Selected {len(selected)} clients.") + print(f"{charging_count} clients selected based on client attributes, out of {len(client_ids)} clients.") + return selected # Called secondly before sending the global model. def client_settings(self, global_model: List[np.ndarray]) -> dict: @@ -30,6 +51,9 @@ def client_settings(self, global_model: List[np.ndarray]) -> dict: # Called third to aggregate the client updates. def aggregate(self, previous_global: List[np.ndarray], client_updates: Dict[str, Tuple[List[np.ndarray], dict]]) -> List[np.ndarray]: # Weighted fedavg implementation. + if len(client_updates) == 0: + print("Received no client updates. Returning previous model.") + return previous_global weighted_sum = [np.zeros_like(param) for param in previous_global] total_weight = 0 for client_id, (client_parameters, metadata) in client_updates.items(): diff --git a/examples/server-functions/server_functions_running_agg.py b/examples/server-functions/sf_incremental_aggregation.py similarity index 87% rename from examples/server-functions/server_functions_running_agg.py rename to examples/server-functions/sf_incremental_aggregation.py index c7c724f2d..56d37e78d 100644 --- a/examples/server-functions/server_functions_running_agg.py +++ b/examples/server-functions/sf_incremental_aggregation.py @@ -21,8 +21,9 @@ def client_settings(self, global_model: List[np.ndarray]) -> dict: return {"learning_rate": self.lr} def incremental_aggregate(self, client_id: str, model: List[np.ndarray], client_metadata: Dict, previous_global: List[np.ndarray]): + # set previous global to fail safe if no updates + self.previous_global = previous_global # Initialize the global model during the first aggregation. - # Use the client metadata to get the number of examples the client has. num_examples = client_metadata.get("num_examples", 1) self.total_examples += num_examples @@ -40,4 +41,7 @@ def get_incremental_aggregate_model(self) -> List[np.ndarray]: # Return the current running aggregate global model and reset it. ret = self.global_model self.global_model = None + if ret is None: + # if no model updates was received, return the previous global model. + return self.previous_global return ret diff --git a/examples/splitlearning_diabetes/.dockerignore b/examples/splitlearning_diabetes/.dockerignore new file mode 100644 index 000000000..874ca079a --- /dev/null +++ b/examples/splitlearning_diabetes/.dockerignore @@ -0,0 +1,3 @@ +seed.npz +*.tgz +*.tar.gz \ No newline at end of file diff --git a/examples/splitlearning_diabetes/client/.fednignore b/examples/splitlearning_diabetes/client/.fednignore new file mode 100644 index 000000000..6c9921672 --- /dev/null +++ b/examples/splitlearning_diabetes/client/.fednignore @@ -0,0 +1,2 @@ +__pycache__ +data \ No newline at end of file diff --git a/examples/splitlearning_diabetes/client/backward.py b/examples/splitlearning_diabetes/client/backward.py new file mode 100644 index 000000000..289f42933 --- /dev/null +++ b/examples/splitlearning_diabetes/client/backward.py @@ -0,0 +1,60 @@ +import os +import sys + +import torch +from data import load_data +from model import load_client_model, save_client_model +from torch import optim + +from fedn.common.log_config import logger +from fedn.utils.helpers.helpers import get_helper + +dir_path = os.path.dirname(os.path.realpath(__file__)) +abs_path = os.path.abspath(dir_path) + +HELPER_MODULE = "splitlearninghelper" +helper = get_helper(HELPER_MODULE) + +seed = 42 +torch.manual_seed(seed) + + +def backward_pass(gradient_path, client_id): + """Loads gradients and extracts the relevant gradients to update the client model for the given client. + + param gradient_path: Path to the gradients file. + :type gradient_path: str + param client_id: ID of the client to update. + :type client_id: str + """ + logger.info(f"Performing backward pass for client {client_id}") + + x_train = load_data(data_path=None, is_train=True) + num_local_features = x_train.shape[1] + + client_model = load_client_model(client_id, num_local_features) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + client_model.to(device) + + client_optimizer = optim.Adam(client_model.parameters(), lr=0.01) + client_optimizer.zero_grad() + + # recomputing the computational graph + embedding = client_model(x_train) + + gradients = helper.load(gradient_path) + + local_gradients = gradients[client_id] + local_gradients = torch.tensor(local_gradients, dtype=torch.float32) + + embedding.backward(local_gradients) + + client_optimizer.step() + + # save the updated model + save_client_model(client_model, client_id) + + +if __name__ == "__main__": + backward_pass(sys.argv[1], sys.argv[2]) diff --git a/examples/splitlearning_diabetes/client/data.py b/examples/splitlearning_diabetes/client/data.py new file mode 100644 index 000000000..f7e046315 --- /dev/null +++ b/examples/splitlearning_diabetes/client/data.py @@ -0,0 +1,142 @@ +import os + +import numpy as np +import pandas as pd +import torch +from sklearn.impute import SimpleImputer +from sklearn.model_selection import train_test_split +from sklearn.preprocessing import StandardScaler + +dir_path = os.path.dirname(os.path.realpath(__file__)) +abs_path = os.path.abspath(dir_path) + + +def load_data(data_path=None, is_train=True): + """Load data from data_path. If data_path is None, load data from default path. + + param data_path: Path to the data file. + :type data_path: str + param is_train: Whether to load train or test data. + :type is_train: bool + :return: The data. + :rtype: torch.Tensor + """ + if data_path is None: + data_path = os.environ.get("FEDN_DATA_PATH", abs_path + "/data/clients/1/diabetes.pt") + + data = torch.load(data_path, weights_only=True) + if is_train: + return data["X_train"] + else: + return data["X_test"] + + +def load_labels(data_path=None, is_train=True): + """Load data from data_path. If data_path is None, load data from default path. + + param data_path: Path to the data file. + :type data_path: str + param is_train: Whether to load train or test data. + :type is_train: bool + :return: The labels. + :rtype: torch.Tensor + """ + if data_path is None: + data_path = os.environ.get("FEDN_DATA_PATH", abs_path + "/data/clients/labels.pt") + data = torch.load(data_path, weights_only=True) + if is_train: + return data["y_train"] + else: + return data["y_test"] + + +def vertical_split(out_dir="data", n_splits=2, data_path="../data/diabetes.csv"): + """Generates *n_split* vertical datasplits for the diabetes dataset. + + param out_dir: Path to the output directory. + :type out_dir: str + param n_splits: Number of vertical splits. + :type n_splits: int + """ + if not os.path.exists(f"{out_dir}/clients"): + os.makedirs(f"{out_dir}/clients") + + data_path = "/app/data/diabetes.csv" if os.getenv("USE_DOCKER_PATH") else "../data/diabetes.csv" + df_diabetes = pd.read_csv(data_path) + + # data preprocessing + df_diabetes[["Glucose", "BloodPressure", "SkinThickness", "BMI"]] = df_diabetes[["Glucose", "BloodPressure", "SkinThickness", "BMI"]].replace(0, np.nan) + imputer = SimpleImputer(strategy="mean") + df_diabetes[["Glucose", "BloodPressure", "SkinThickness", "BMI"]] = imputer.fit_transform(df_diabetes[["Glucose", "BloodPressure", "SkinThickness", "BMI"]]) + + y = df_diabetes["Outcome"].to_numpy() + X = df_diabetes.drop(columns=["Outcome"]) + + X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y) + + # vertical data split + features_1 = ["Pregnancies", "Glucose", "BloodPressure", "SkinThickness"] + features_2 = ["Insulin", "BMI", "DiabetesPedigreeFunction", "Age"] + + X_train_1 = X_train[features_1] + X_train_2 = X_train[features_2] + + X_test_1 = X_test[features_1] + X_test_2 = X_test[features_2] + + # scaling + scaler_1 = StandardScaler() + scaler_2 = StandardScaler() + + # train + X_train_1_scaled = scaler_1.fit_transform(X_train_1) + X_train_2_scaled = scaler_2.fit_transform(X_train_2) + + # test + X_test_1_scaled = scaler_1.transform(X_test_1) + X_test_2_scaled = scaler_2.transform(X_test_2) + + # to tensor + X_train_1_tensor = torch.tensor(X_train_1_scaled, dtype=torch.float32) + X_train_2_tensor = torch.tensor(X_train_2_scaled, dtype=torch.float32) + y_train_tensor = torch.tensor(y_train, dtype=torch.float32).view(-1, 1) + + X_test_1_tensor = torch.tensor(X_test_1_scaled, dtype=torch.float32) + X_test_2_tensor = torch.tensor(X_test_2_scaled, dtype=torch.float32) + y_test_tensor = torch.tensor(y_test, dtype=torch.float32).view(-1, 1) + + data = { + "train_features": [X_train_1_tensor, X_train_2_tensor], + "train_labels": y_train_tensor, + "test_features": [X_test_1_tensor, X_test_2_tensor], + "test_labels": y_test_tensor, + } + + # create vertical splits + for i in range(n_splits): + subdir = f"{out_dir}/clients/{str(i + 1)}" + if not os.path.exists(subdir): + os.mkdir(subdir) + # save features + torch.save( + { + "X_train": data["train_features"][i], + "X_test": data["test_features"][i], + }, + f"{subdir}/diabetes.pt", + ) + # save labels + subdir = f"{out_dir}/clients" + torch.save( + { + "y_train": data["train_labels"], + "y_test": data["test_labels"], + }, + f"{subdir}/labels.pt", + ) + + +if __name__ == "__main__": + # Prepare data if not already done + if not os.path.exists(abs_path + "/data/clients/1"): + vertical_split() diff --git a/examples/splitlearning_diabetes/client/fedn.yaml b/examples/splitlearning_diabetes/client/fedn.yaml new file mode 100644 index 000000000..bf07175fb --- /dev/null +++ b/examples/splitlearning_diabetes/client/fedn.yaml @@ -0,0 +1,8 @@ +python_env: python_env.yaml +entry_points: + startup: + command: python data.py + forward: + command: python forward.py + backward: + command: python backward.py \ No newline at end of file diff --git a/examples/splitlearning_diabetes/client/forward.py b/examples/splitlearning_diabetes/client/forward.py new file mode 100644 index 000000000..57c8c6786 --- /dev/null +++ b/examples/splitlearning_diabetes/client/forward.py @@ -0,0 +1,68 @@ +import os +import sys + +import torch +from model import compile_model, load_client_model, save_client_model + +from data import load_data +from fedn.common.log_config import logger +from fedn.utils.helpers.helpers import get_helper, save_metadata + +dir_path = os.path.dirname(os.path.realpath(__file__)) +abs_path = os.path.abspath(dir_path) + +HELPER_MODULE = "splitlearninghelper" +helper = get_helper(HELPER_MODULE) + +seed = 42 +torch.manual_seed(seed) + + +def forward_pass(client_id, out_embedding_path, is_sl_inference, data_path=None): + """Complete a forward pass on the client side (client model) based on the local client model to produce embeddings that are sent to the combiner. + + If the forward pass is used for validation, the test dataset is loaded. + + param client_id: ID of the client to forward pass. + :type client_id: str + param out_embedding_path: Path to the output embedding file. + :type out_embedding_path: str + param is_sl_inference: Whether to perform a forward pass with inference (used for validation) or not. + :type is_sl_inference: str + param data_path: Path to the data file. + :type data_path: str + """ + if is_sl_inference == "True": + logger.info(f"Client-side inference forward pass for client {client_id}") + X = load_data(data_path, is_train=False) + else: + logger.info(f"Client-side training forward pass for client {client_id}") + X = load_data(data_path, is_train=True) + + num_local_features = X.shape[1] + + if not os.path.exists(f"{abs_path}/local_models/{client_id}.pth"): + model = compile_model(num_local_features) + save_client_model(model, client_id) + + model = load_client_model(client_id, num_local_features) + + model.eval() + with torch.no_grad(): + embedding = model(X) + + # Metadata needed for aggregation server side + metadata = { + "num_examples": len(X), # number of examples are mandatory + } + + # Save JSON metadata file (mandatory) + save_metadata(metadata, out_embedding_path) + + # save embeddings + embedding_dict = {str(client_id): embedding.numpy()} + helper.save(embedding_dict, out_embedding_path) + + +if __name__ == "__main__": + forward_pass(sys.argv[1], sys.argv[2], sys.argv[3]) # test with: python forward.py 1 . "False" data/clients/1/diabetes.pt diff --git a/examples/splitlearning_diabetes/client/model.py b/examples/splitlearning_diabetes/client/model.py new file mode 100644 index 000000000..15f518bec --- /dev/null +++ b/examples/splitlearning_diabetes/client/model.py @@ -0,0 +1,60 @@ +import os + +import torch +from torch import nn + +dir_path = os.path.dirname(os.path.realpath(__file__)) +abs_path = os.path.abspath(dir_path) + + +class ClientModel(nn.Module): + """Client-side model""" + + def __init__(self, input_features): + super(ClientModel, self).__init__() + self.fc1 = nn.Linear(input_features, 8) + self.fc2 = nn.Linear(8, 4) + self.relu = nn.ReLU() + + def forward(self, x): + x = self.relu(self.fc1(x)) + x = self.relu(self.fc2(x)) + return x + + +def compile_model(num_local_features): + """Build the client model. + + param num_local_features: Number of features in the local dataset. + :type num_local_features: int + :return: The client model. + :rtype: ClientModel + """ + model = ClientModel(num_local_features) + return model + + +def save_client_model(model, client_id): + """Save the client model to the local_models directory (saves model locally). + + param model: The client model. + :type model: ClientModel + param client_id: ID of the client to save the model. + :type client_id: str + """ + if not os.path.exists(f"{abs_path}/local_models"): + os.makedirs(f"{abs_path}/local_models", exist_ok=True) + torch.save(model.state_dict(), f"{abs_path}/local_models/{client_id}.pth") + + +def load_client_model(client_id, num_local_features): + """Load the client model from the local_models directory. + + param client_id: ID of the client to load the model. + :type client_id: str + param num_local_features: Number of features in the local dataset. + :type num_local_features: int + """ + model = compile_model(num_local_features) + model.load_state_dict(torch.load(f"{abs_path}/local_models/{client_id}.pth", weights_only=True)) + return model diff --git a/examples/splitlearning_diabetes/client/python_env.yaml b/examples/splitlearning_diabetes/client/python_env.yaml new file mode 100644 index 000000000..376d116ec --- /dev/null +++ b/examples/splitlearning_diabetes/client/python_env.yaml @@ -0,0 +1,16 @@ +name: .splitlearning +build_dependencies: + - pip + - setuptools + - wheel +dependencies: + - pandas==2.2.3 + - scikit-learn==1.6.0 + - torch==2.4.1; (sys_platform == "darwin" and platform_machine == "arm64") or (sys_platform == "win32" or sys_platform == "win64" or sys_platform == "linux") + # PyTorch macOS x86 builds deprecation + - torch==2.2.2; sys_platform == "darwin" and platform_machine == "x86_64" + - torchvision==0.19.1; (sys_platform == "darwin" and platform_machine == "arm64") or (sys_platform == "win32" or sys_platform == "win64" or sys_platform == "linux") + - torchvision==0.17.2; sys_platform == "darwin" and platform_machine == "x86_64" + - numpy==2.0.2; (sys_platform == "darwin" and platform_machine == "arm64") or (sys_platform == "win32" or sys_platform == "win64" or sys_platform == "linux") + - numpy==1.26.4; (sys_platform == "darwin" and platform_machine == "x86_64") + - fedn \ No newline at end of file diff --git a/examples/splitlearning_diabetes/data/diabetes.csv b/examples/splitlearning_diabetes/data/diabetes.csv new file mode 100644 index 000000000..db6f31768 --- /dev/null +++ b/examples/splitlearning_diabetes/data/diabetes.csv @@ -0,0 +1,769 @@ +Pregnancies,Glucose,BloodPressure,SkinThickness,Insulin,BMI,DiabetesPedigreeFunction,Age,Outcome +6,148,72,35,0,33.6,0.627,50,1 +1,85,66,29,0,26.6,0.351,31,0 +8,183,64,0,0,23.3,0.672,32,1 +1,89,66,23,94,28.1,0.167,21,0 +0,137,40,35,168,43.1,2.288,33,1 +5,116,74,0,0,25.6,0.201,30,0 +3,78,50,32,88,31,0.248,26,1 +10,115,0,0,0,35.3,0.134,29,0 +2,197,70,45,543,30.5,0.158,53,1 +8,125,96,0,0,0,0.232,54,1 +4,110,92,0,0,37.6,0.191,30,0 +10,168,74,0,0,38,0.537,34,1 +10,139,80,0,0,27.1,1.441,57,0 +1,189,60,23,846,30.1,0.398,59,1 +5,166,72,19,175,25.8,0.587,51,1 +7,100,0,0,0,30,0.484,32,1 +0,118,84,47,230,45.8,0.551,31,1 +7,107,74,0,0,29.6,0.254,31,1 +1,103,30,38,83,43.3,0.183,33,0 +1,115,70,30,96,34.6,0.529,32,1 +3,126,88,41,235,39.3,0.704,27,0 +8,99,84,0,0,35.4,0.388,50,0 +7,196,90,0,0,39.8,0.451,41,1 +9,119,80,35,0,29,0.263,29,1 +11,143,94,33,146,36.6,0.254,51,1 +10,125,70,26,115,31.1,0.205,41,1 +7,147,76,0,0,39.4,0.257,43,1 +1,97,66,15,140,23.2,0.487,22,0 +13,145,82,19,110,22.2,0.245,57,0 +5,117,92,0,0,34.1,0.337,38,0 +5,109,75,26,0,36,0.546,60,0 +3,158,76,36,245,31.6,0.851,28,1 +3,88,58,11,54,24.8,0.267,22,0 +6,92,92,0,0,19.9,0.188,28,0 +10,122,78,31,0,27.6,0.512,45,0 +4,103,60,33,192,24,0.966,33,0 +11,138,76,0,0,33.2,0.42,35,0 +9,102,76,37,0,32.9,0.665,46,1 +2,90,68,42,0,38.2,0.503,27,1 +4,111,72,47,207,37.1,1.39,56,1 +3,180,64,25,70,34,0.271,26,0 +7,133,84,0,0,40.2,0.696,37,0 +7,106,92,18,0,22.7,0.235,48,0 +9,171,110,24,240,45.4,0.721,54,1 +7,159,64,0,0,27.4,0.294,40,0 +0,180,66,39,0,42,1.893,25,1 +1,146,56,0,0,29.7,0.564,29,0 +2,71,70,27,0,28,0.586,22,0 +7,103,66,32,0,39.1,0.344,31,1 +7,105,0,0,0,0,0.305,24,0 +1,103,80,11,82,19.4,0.491,22,0 +1,101,50,15,36,24.2,0.526,26,0 +5,88,66,21,23,24.4,0.342,30,0 +8,176,90,34,300,33.7,0.467,58,1 +7,150,66,42,342,34.7,0.718,42,0 +1,73,50,10,0,23,0.248,21,0 +7,187,68,39,304,37.7,0.254,41,1 +0,100,88,60,110,46.8,0.962,31,0 +0,146,82,0,0,40.5,1.781,44,0 +0,105,64,41,142,41.5,0.173,22,0 +2,84,0,0,0,0,0.304,21,0 +8,133,72,0,0,32.9,0.27,39,1 +5,44,62,0,0,25,0.587,36,0 +2,141,58,34,128,25.4,0.699,24,0 +7,114,66,0,0,32.8,0.258,42,1 +5,99,74,27,0,29,0.203,32,0 +0,109,88,30,0,32.5,0.855,38,1 +2,109,92,0,0,42.7,0.845,54,0 +1,95,66,13,38,19.6,0.334,25,0 +4,146,85,27,100,28.9,0.189,27,0 +2,100,66,20,90,32.9,0.867,28,1 +5,139,64,35,140,28.6,0.411,26,0 +13,126,90,0,0,43.4,0.583,42,1 +4,129,86,20,270,35.1,0.231,23,0 +1,79,75,30,0,32,0.396,22,0 +1,0,48,20,0,24.7,0.14,22,0 +7,62,78,0,0,32.6,0.391,41,0 +5,95,72,33,0,37.7,0.37,27,0 +0,131,0,0,0,43.2,0.27,26,1 +2,112,66,22,0,25,0.307,24,0 +3,113,44,13,0,22.4,0.14,22,0 +2,74,0,0,0,0,0.102,22,0 +7,83,78,26,71,29.3,0.767,36,0 +0,101,65,28,0,24.6,0.237,22,0 +5,137,108,0,0,48.8,0.227,37,1 +2,110,74,29,125,32.4,0.698,27,0 +13,106,72,54,0,36.6,0.178,45,0 +2,100,68,25,71,38.5,0.324,26,0 +15,136,70,32,110,37.1,0.153,43,1 +1,107,68,19,0,26.5,0.165,24,0 +1,80,55,0,0,19.1,0.258,21,0 +4,123,80,15,176,32,0.443,34,0 +7,81,78,40,48,46.7,0.261,42,0 +4,134,72,0,0,23.8,0.277,60,1 +2,142,82,18,64,24.7,0.761,21,0 +6,144,72,27,228,33.9,0.255,40,0 +2,92,62,28,0,31.6,0.13,24,0 +1,71,48,18,76,20.4,0.323,22,0 +6,93,50,30,64,28.7,0.356,23,0 +1,122,90,51,220,49.7,0.325,31,1 +1,163,72,0,0,39,1.222,33,1 +1,151,60,0,0,26.1,0.179,22,0 +0,125,96,0,0,22.5,0.262,21,0 +1,81,72,18,40,26.6,0.283,24,0 +2,85,65,0,0,39.6,0.93,27,0 +1,126,56,29,152,28.7,0.801,21,0 +1,96,122,0,0,22.4,0.207,27,0 +4,144,58,28,140,29.5,0.287,37,0 +3,83,58,31,18,34.3,0.336,25,0 +0,95,85,25,36,37.4,0.247,24,1 +3,171,72,33,135,33.3,0.199,24,1 +8,155,62,26,495,34,0.543,46,1 +1,89,76,34,37,31.2,0.192,23,0 +4,76,62,0,0,34,0.391,25,0 +7,160,54,32,175,30.5,0.588,39,1 +4,146,92,0,0,31.2,0.539,61,1 +5,124,74,0,0,34,0.22,38,1 +5,78,48,0,0,33.7,0.654,25,0 +4,97,60,23,0,28.2,0.443,22,0 +4,99,76,15,51,23.2,0.223,21,0 +0,162,76,56,100,53.2,0.759,25,1 +6,111,64,39,0,34.2,0.26,24,0 +2,107,74,30,100,33.6,0.404,23,0 +5,132,80,0,0,26.8,0.186,69,0 +0,113,76,0,0,33.3,0.278,23,1 +1,88,30,42,99,55,0.496,26,1 +3,120,70,30,135,42.9,0.452,30,0 +1,118,58,36,94,33.3,0.261,23,0 +1,117,88,24,145,34.5,0.403,40,1 +0,105,84,0,0,27.9,0.741,62,1 +4,173,70,14,168,29.7,0.361,33,1 +9,122,56,0,0,33.3,1.114,33,1 +3,170,64,37,225,34.5,0.356,30,1 +8,84,74,31,0,38.3,0.457,39,0 +2,96,68,13,49,21.1,0.647,26,0 +2,125,60,20,140,33.8,0.088,31,0 +0,100,70,26,50,30.8,0.597,21,0 +0,93,60,25,92,28.7,0.532,22,0 +0,129,80,0,0,31.2,0.703,29,0 +5,105,72,29,325,36.9,0.159,28,0 +3,128,78,0,0,21.1,0.268,55,0 +5,106,82,30,0,39.5,0.286,38,0 +2,108,52,26,63,32.5,0.318,22,0 +10,108,66,0,0,32.4,0.272,42,1 +4,154,62,31,284,32.8,0.237,23,0 +0,102,75,23,0,0,0.572,21,0 +9,57,80,37,0,32.8,0.096,41,0 +2,106,64,35,119,30.5,1.4,34,0 +5,147,78,0,0,33.7,0.218,65,0 +2,90,70,17,0,27.3,0.085,22,0 +1,136,74,50,204,37.4,0.399,24,0 +4,114,65,0,0,21.9,0.432,37,0 +9,156,86,28,155,34.3,1.189,42,1 +1,153,82,42,485,40.6,0.687,23,0 +8,188,78,0,0,47.9,0.137,43,1 +7,152,88,44,0,50,0.337,36,1 +2,99,52,15,94,24.6,0.637,21,0 +1,109,56,21,135,25.2,0.833,23,0 +2,88,74,19,53,29,0.229,22,0 +17,163,72,41,114,40.9,0.817,47,1 +4,151,90,38,0,29.7,0.294,36,0 +7,102,74,40,105,37.2,0.204,45,0 +0,114,80,34,285,44.2,0.167,27,0 +2,100,64,23,0,29.7,0.368,21,0 +0,131,88,0,0,31.6,0.743,32,1 +6,104,74,18,156,29.9,0.722,41,1 +3,148,66,25,0,32.5,0.256,22,0 +4,120,68,0,0,29.6,0.709,34,0 +4,110,66,0,0,31.9,0.471,29,0 +3,111,90,12,78,28.4,0.495,29,0 +6,102,82,0,0,30.8,0.18,36,1 +6,134,70,23,130,35.4,0.542,29,1 +2,87,0,23,0,28.9,0.773,25,0 +1,79,60,42,48,43.5,0.678,23,0 +2,75,64,24,55,29.7,0.37,33,0 +8,179,72,42,130,32.7,0.719,36,1 +6,85,78,0,0,31.2,0.382,42,0 +0,129,110,46,130,67.1,0.319,26,1 +5,143,78,0,0,45,0.19,47,0 +5,130,82,0,0,39.1,0.956,37,1 +6,87,80,0,0,23.2,0.084,32,0 +0,119,64,18,92,34.9,0.725,23,0 +1,0,74,20,23,27.7,0.299,21,0 +5,73,60,0,0,26.8,0.268,27,0 +4,141,74,0,0,27.6,0.244,40,0 +7,194,68,28,0,35.9,0.745,41,1 +8,181,68,36,495,30.1,0.615,60,1 +1,128,98,41,58,32,1.321,33,1 +8,109,76,39,114,27.9,0.64,31,1 +5,139,80,35,160,31.6,0.361,25,1 +3,111,62,0,0,22.6,0.142,21,0 +9,123,70,44,94,33.1,0.374,40,0 +7,159,66,0,0,30.4,0.383,36,1 +11,135,0,0,0,52.3,0.578,40,1 +8,85,55,20,0,24.4,0.136,42,0 +5,158,84,41,210,39.4,0.395,29,1 +1,105,58,0,0,24.3,0.187,21,0 +3,107,62,13,48,22.9,0.678,23,1 +4,109,64,44,99,34.8,0.905,26,1 +4,148,60,27,318,30.9,0.15,29,1 +0,113,80,16,0,31,0.874,21,0 +1,138,82,0,0,40.1,0.236,28,0 +0,108,68,20,0,27.3,0.787,32,0 +2,99,70,16,44,20.4,0.235,27,0 +6,103,72,32,190,37.7,0.324,55,0 +5,111,72,28,0,23.9,0.407,27,0 +8,196,76,29,280,37.5,0.605,57,1 +5,162,104,0,0,37.7,0.151,52,1 +1,96,64,27,87,33.2,0.289,21,0 +7,184,84,33,0,35.5,0.355,41,1 +2,81,60,22,0,27.7,0.29,25,0 +0,147,85,54,0,42.8,0.375,24,0 +7,179,95,31,0,34.2,0.164,60,0 +0,140,65,26,130,42.6,0.431,24,1 +9,112,82,32,175,34.2,0.26,36,1 +12,151,70,40,271,41.8,0.742,38,1 +5,109,62,41,129,35.8,0.514,25,1 +6,125,68,30,120,30,0.464,32,0 +5,85,74,22,0,29,1.224,32,1 +5,112,66,0,0,37.8,0.261,41,1 +0,177,60,29,478,34.6,1.072,21,1 +2,158,90,0,0,31.6,0.805,66,1 +7,119,0,0,0,25.2,0.209,37,0 +7,142,60,33,190,28.8,0.687,61,0 +1,100,66,15,56,23.6,0.666,26,0 +1,87,78,27,32,34.6,0.101,22,0 +0,101,76,0,0,35.7,0.198,26,0 +3,162,52,38,0,37.2,0.652,24,1 +4,197,70,39,744,36.7,2.329,31,0 +0,117,80,31,53,45.2,0.089,24,0 +4,142,86,0,0,44,0.645,22,1 +6,134,80,37,370,46.2,0.238,46,1 +1,79,80,25,37,25.4,0.583,22,0 +4,122,68,0,0,35,0.394,29,0 +3,74,68,28,45,29.7,0.293,23,0 +4,171,72,0,0,43.6,0.479,26,1 +7,181,84,21,192,35.9,0.586,51,1 +0,179,90,27,0,44.1,0.686,23,1 +9,164,84,21,0,30.8,0.831,32,1 +0,104,76,0,0,18.4,0.582,27,0 +1,91,64,24,0,29.2,0.192,21,0 +4,91,70,32,88,33.1,0.446,22,0 +3,139,54,0,0,25.6,0.402,22,1 +6,119,50,22,176,27.1,1.318,33,1 +2,146,76,35,194,38.2,0.329,29,0 +9,184,85,15,0,30,1.213,49,1 +10,122,68,0,0,31.2,0.258,41,0 +0,165,90,33,680,52.3,0.427,23,0 +9,124,70,33,402,35.4,0.282,34,0 +1,111,86,19,0,30.1,0.143,23,0 +9,106,52,0,0,31.2,0.38,42,0 +2,129,84,0,0,28,0.284,27,0 +2,90,80,14,55,24.4,0.249,24,0 +0,86,68,32,0,35.8,0.238,25,0 +12,92,62,7,258,27.6,0.926,44,1 +1,113,64,35,0,33.6,0.543,21,1 +3,111,56,39,0,30.1,0.557,30,0 +2,114,68,22,0,28.7,0.092,25,0 +1,193,50,16,375,25.9,0.655,24,0 +11,155,76,28,150,33.3,1.353,51,1 +3,191,68,15,130,30.9,0.299,34,0 +3,141,0,0,0,30,0.761,27,1 +4,95,70,32,0,32.1,0.612,24,0 +3,142,80,15,0,32.4,0.2,63,0 +4,123,62,0,0,32,0.226,35,1 +5,96,74,18,67,33.6,0.997,43,0 +0,138,0,0,0,36.3,0.933,25,1 +2,128,64,42,0,40,1.101,24,0 +0,102,52,0,0,25.1,0.078,21,0 +2,146,0,0,0,27.5,0.24,28,1 +10,101,86,37,0,45.6,1.136,38,1 +2,108,62,32,56,25.2,0.128,21,0 +3,122,78,0,0,23,0.254,40,0 +1,71,78,50,45,33.2,0.422,21,0 +13,106,70,0,0,34.2,0.251,52,0 +2,100,70,52,57,40.5,0.677,25,0 +7,106,60,24,0,26.5,0.296,29,1 +0,104,64,23,116,27.8,0.454,23,0 +5,114,74,0,0,24.9,0.744,57,0 +2,108,62,10,278,25.3,0.881,22,0 +0,146,70,0,0,37.9,0.334,28,1 +10,129,76,28,122,35.9,0.28,39,0 +7,133,88,15,155,32.4,0.262,37,0 +7,161,86,0,0,30.4,0.165,47,1 +2,108,80,0,0,27,0.259,52,1 +7,136,74,26,135,26,0.647,51,0 +5,155,84,44,545,38.7,0.619,34,0 +1,119,86,39,220,45.6,0.808,29,1 +4,96,56,17,49,20.8,0.34,26,0 +5,108,72,43,75,36.1,0.263,33,0 +0,78,88,29,40,36.9,0.434,21,0 +0,107,62,30,74,36.6,0.757,25,1 +2,128,78,37,182,43.3,1.224,31,1 +1,128,48,45,194,40.5,0.613,24,1 +0,161,50,0,0,21.9,0.254,65,0 +6,151,62,31,120,35.5,0.692,28,0 +2,146,70,38,360,28,0.337,29,1 +0,126,84,29,215,30.7,0.52,24,0 +14,100,78,25,184,36.6,0.412,46,1 +8,112,72,0,0,23.6,0.84,58,0 +0,167,0,0,0,32.3,0.839,30,1 +2,144,58,33,135,31.6,0.422,25,1 +5,77,82,41,42,35.8,0.156,35,0 +5,115,98,0,0,52.9,0.209,28,1 +3,150,76,0,0,21,0.207,37,0 +2,120,76,37,105,39.7,0.215,29,0 +10,161,68,23,132,25.5,0.326,47,1 +0,137,68,14,148,24.8,0.143,21,0 +0,128,68,19,180,30.5,1.391,25,1 +2,124,68,28,205,32.9,0.875,30,1 +6,80,66,30,0,26.2,0.313,41,0 +0,106,70,37,148,39.4,0.605,22,0 +2,155,74,17,96,26.6,0.433,27,1 +3,113,50,10,85,29.5,0.626,25,0 +7,109,80,31,0,35.9,1.127,43,1 +2,112,68,22,94,34.1,0.315,26,0 +3,99,80,11,64,19.3,0.284,30,0 +3,182,74,0,0,30.5,0.345,29,1 +3,115,66,39,140,38.1,0.15,28,0 +6,194,78,0,0,23.5,0.129,59,1 +4,129,60,12,231,27.5,0.527,31,0 +3,112,74,30,0,31.6,0.197,25,1 +0,124,70,20,0,27.4,0.254,36,1 +13,152,90,33,29,26.8,0.731,43,1 +2,112,75,32,0,35.7,0.148,21,0 +1,157,72,21,168,25.6,0.123,24,0 +1,122,64,32,156,35.1,0.692,30,1 +10,179,70,0,0,35.1,0.2,37,0 +2,102,86,36,120,45.5,0.127,23,1 +6,105,70,32,68,30.8,0.122,37,0 +8,118,72,19,0,23.1,1.476,46,0 +2,87,58,16,52,32.7,0.166,25,0 +1,180,0,0,0,43.3,0.282,41,1 +12,106,80,0,0,23.6,0.137,44,0 +1,95,60,18,58,23.9,0.26,22,0 +0,165,76,43,255,47.9,0.259,26,0 +0,117,0,0,0,33.8,0.932,44,0 +5,115,76,0,0,31.2,0.343,44,1 +9,152,78,34,171,34.2,0.893,33,1 +7,178,84,0,0,39.9,0.331,41,1 +1,130,70,13,105,25.9,0.472,22,0 +1,95,74,21,73,25.9,0.673,36,0 +1,0,68,35,0,32,0.389,22,0 +5,122,86,0,0,34.7,0.29,33,0 +8,95,72,0,0,36.8,0.485,57,0 +8,126,88,36,108,38.5,0.349,49,0 +1,139,46,19,83,28.7,0.654,22,0 +3,116,0,0,0,23.5,0.187,23,0 +3,99,62,19,74,21.8,0.279,26,0 +5,0,80,32,0,41,0.346,37,1 +4,92,80,0,0,42.2,0.237,29,0 +4,137,84,0,0,31.2,0.252,30,0 +3,61,82,28,0,34.4,0.243,46,0 +1,90,62,12,43,27.2,0.58,24,0 +3,90,78,0,0,42.7,0.559,21,0 +9,165,88,0,0,30.4,0.302,49,1 +1,125,50,40,167,33.3,0.962,28,1 +13,129,0,30,0,39.9,0.569,44,1 +12,88,74,40,54,35.3,0.378,48,0 +1,196,76,36,249,36.5,0.875,29,1 +5,189,64,33,325,31.2,0.583,29,1 +5,158,70,0,0,29.8,0.207,63,0 +5,103,108,37,0,39.2,0.305,65,0 +4,146,78,0,0,38.5,0.52,67,1 +4,147,74,25,293,34.9,0.385,30,0 +5,99,54,28,83,34,0.499,30,0 +6,124,72,0,0,27.6,0.368,29,1 +0,101,64,17,0,21,0.252,21,0 +3,81,86,16,66,27.5,0.306,22,0 +1,133,102,28,140,32.8,0.234,45,1 +3,173,82,48,465,38.4,2.137,25,1 +0,118,64,23,89,0,1.731,21,0 +0,84,64,22,66,35.8,0.545,21,0 +2,105,58,40,94,34.9,0.225,25,0 +2,122,52,43,158,36.2,0.816,28,0 +12,140,82,43,325,39.2,0.528,58,1 +0,98,82,15,84,25.2,0.299,22,0 +1,87,60,37,75,37.2,0.509,22,0 +4,156,75,0,0,48.3,0.238,32,1 +0,93,100,39,72,43.4,1.021,35,0 +1,107,72,30,82,30.8,0.821,24,0 +0,105,68,22,0,20,0.236,22,0 +1,109,60,8,182,25.4,0.947,21,0 +1,90,62,18,59,25.1,1.268,25,0 +1,125,70,24,110,24.3,0.221,25,0 +1,119,54,13,50,22.3,0.205,24,0 +5,116,74,29,0,32.3,0.66,35,1 +8,105,100,36,0,43.3,0.239,45,1 +5,144,82,26,285,32,0.452,58,1 +3,100,68,23,81,31.6,0.949,28,0 +1,100,66,29,196,32,0.444,42,0 +5,166,76,0,0,45.7,0.34,27,1 +1,131,64,14,415,23.7,0.389,21,0 +4,116,72,12,87,22.1,0.463,37,0 +4,158,78,0,0,32.9,0.803,31,1 +2,127,58,24,275,27.7,1.6,25,0 +3,96,56,34,115,24.7,0.944,39,0 +0,131,66,40,0,34.3,0.196,22,1 +3,82,70,0,0,21.1,0.389,25,0 +3,193,70,31,0,34.9,0.241,25,1 +4,95,64,0,0,32,0.161,31,1 +6,137,61,0,0,24.2,0.151,55,0 +5,136,84,41,88,35,0.286,35,1 +9,72,78,25,0,31.6,0.28,38,0 +5,168,64,0,0,32.9,0.135,41,1 +2,123,48,32,165,42.1,0.52,26,0 +4,115,72,0,0,28.9,0.376,46,1 +0,101,62,0,0,21.9,0.336,25,0 +8,197,74,0,0,25.9,1.191,39,1 +1,172,68,49,579,42.4,0.702,28,1 +6,102,90,39,0,35.7,0.674,28,0 +1,112,72,30,176,34.4,0.528,25,0 +1,143,84,23,310,42.4,1.076,22,0 +1,143,74,22,61,26.2,0.256,21,0 +0,138,60,35,167,34.6,0.534,21,1 +3,173,84,33,474,35.7,0.258,22,1 +1,97,68,21,0,27.2,1.095,22,0 +4,144,82,32,0,38.5,0.554,37,1 +1,83,68,0,0,18.2,0.624,27,0 +3,129,64,29,115,26.4,0.219,28,1 +1,119,88,41,170,45.3,0.507,26,0 +2,94,68,18,76,26,0.561,21,0 +0,102,64,46,78,40.6,0.496,21,0 +2,115,64,22,0,30.8,0.421,21,0 +8,151,78,32,210,42.9,0.516,36,1 +4,184,78,39,277,37,0.264,31,1 +0,94,0,0,0,0,0.256,25,0 +1,181,64,30,180,34.1,0.328,38,1 +0,135,94,46,145,40.6,0.284,26,0 +1,95,82,25,180,35,0.233,43,1 +2,99,0,0,0,22.2,0.108,23,0 +3,89,74,16,85,30.4,0.551,38,0 +1,80,74,11,60,30,0.527,22,0 +2,139,75,0,0,25.6,0.167,29,0 +1,90,68,8,0,24.5,1.138,36,0 +0,141,0,0,0,42.4,0.205,29,1 +12,140,85,33,0,37.4,0.244,41,0 +5,147,75,0,0,29.9,0.434,28,0 +1,97,70,15,0,18.2,0.147,21,0 +6,107,88,0,0,36.8,0.727,31,0 +0,189,104,25,0,34.3,0.435,41,1 +2,83,66,23,50,32.2,0.497,22,0 +4,117,64,27,120,33.2,0.23,24,0 +8,108,70,0,0,30.5,0.955,33,1 +4,117,62,12,0,29.7,0.38,30,1 +0,180,78,63,14,59.4,2.42,25,1 +1,100,72,12,70,25.3,0.658,28,0 +0,95,80,45,92,36.5,0.33,26,0 +0,104,64,37,64,33.6,0.51,22,1 +0,120,74,18,63,30.5,0.285,26,0 +1,82,64,13,95,21.2,0.415,23,0 +2,134,70,0,0,28.9,0.542,23,1 +0,91,68,32,210,39.9,0.381,25,0 +2,119,0,0,0,19.6,0.832,72,0 +2,100,54,28,105,37.8,0.498,24,0 +14,175,62,30,0,33.6,0.212,38,1 +1,135,54,0,0,26.7,0.687,62,0 +5,86,68,28,71,30.2,0.364,24,0 +10,148,84,48,237,37.6,1.001,51,1 +9,134,74,33,60,25.9,0.46,81,0 +9,120,72,22,56,20.8,0.733,48,0 +1,71,62,0,0,21.8,0.416,26,0 +8,74,70,40,49,35.3,0.705,39,0 +5,88,78,30,0,27.6,0.258,37,0 +10,115,98,0,0,24,1.022,34,0 +0,124,56,13,105,21.8,0.452,21,0 +0,74,52,10,36,27.8,0.269,22,0 +0,97,64,36,100,36.8,0.6,25,0 +8,120,0,0,0,30,0.183,38,1 +6,154,78,41,140,46.1,0.571,27,0 +1,144,82,40,0,41.3,0.607,28,0 +0,137,70,38,0,33.2,0.17,22,0 +0,119,66,27,0,38.8,0.259,22,0 +7,136,90,0,0,29.9,0.21,50,0 +4,114,64,0,0,28.9,0.126,24,0 +0,137,84,27,0,27.3,0.231,59,0 +2,105,80,45,191,33.7,0.711,29,1 +7,114,76,17,110,23.8,0.466,31,0 +8,126,74,38,75,25.9,0.162,39,0 +4,132,86,31,0,28,0.419,63,0 +3,158,70,30,328,35.5,0.344,35,1 +0,123,88,37,0,35.2,0.197,29,0 +4,85,58,22,49,27.8,0.306,28,0 +0,84,82,31,125,38.2,0.233,23,0 +0,145,0,0,0,44.2,0.63,31,1 +0,135,68,42,250,42.3,0.365,24,1 +1,139,62,41,480,40.7,0.536,21,0 +0,173,78,32,265,46.5,1.159,58,0 +4,99,72,17,0,25.6,0.294,28,0 +8,194,80,0,0,26.1,0.551,67,0 +2,83,65,28,66,36.8,0.629,24,0 +2,89,90,30,0,33.5,0.292,42,0 +4,99,68,38,0,32.8,0.145,33,0 +4,125,70,18,122,28.9,1.144,45,1 +3,80,0,0,0,0,0.174,22,0 +6,166,74,0,0,26.6,0.304,66,0 +5,110,68,0,0,26,0.292,30,0 +2,81,72,15,76,30.1,0.547,25,0 +7,195,70,33,145,25.1,0.163,55,1 +6,154,74,32,193,29.3,0.839,39,0 +2,117,90,19,71,25.2,0.313,21,0 +3,84,72,32,0,37.2,0.267,28,0 +6,0,68,41,0,39,0.727,41,1 +7,94,64,25,79,33.3,0.738,41,0 +3,96,78,39,0,37.3,0.238,40,0 +10,75,82,0,0,33.3,0.263,38,0 +0,180,90,26,90,36.5,0.314,35,1 +1,130,60,23,170,28.6,0.692,21,0 +2,84,50,23,76,30.4,0.968,21,0 +8,120,78,0,0,25,0.409,64,0 +12,84,72,31,0,29.7,0.297,46,1 +0,139,62,17,210,22.1,0.207,21,0 +9,91,68,0,0,24.2,0.2,58,0 +2,91,62,0,0,27.3,0.525,22,0 +3,99,54,19,86,25.6,0.154,24,0 +3,163,70,18,105,31.6,0.268,28,1 +9,145,88,34,165,30.3,0.771,53,1 +7,125,86,0,0,37.6,0.304,51,0 +13,76,60,0,0,32.8,0.18,41,0 +6,129,90,7,326,19.6,0.582,60,0 +2,68,70,32,66,25,0.187,25,0 +3,124,80,33,130,33.2,0.305,26,0 +6,114,0,0,0,0,0.189,26,0 +9,130,70,0,0,34.2,0.652,45,1 +3,125,58,0,0,31.6,0.151,24,0 +3,87,60,18,0,21.8,0.444,21,0 +1,97,64,19,82,18.2,0.299,21,0 +3,116,74,15,105,26.3,0.107,24,0 +0,117,66,31,188,30.8,0.493,22,0 +0,111,65,0,0,24.6,0.66,31,0 +2,122,60,18,106,29.8,0.717,22,0 +0,107,76,0,0,45.3,0.686,24,0 +1,86,66,52,65,41.3,0.917,29,0 +6,91,0,0,0,29.8,0.501,31,0 +1,77,56,30,56,33.3,1.251,24,0 +4,132,0,0,0,32.9,0.302,23,1 +0,105,90,0,0,29.6,0.197,46,0 +0,57,60,0,0,21.7,0.735,67,0 +0,127,80,37,210,36.3,0.804,23,0 +3,129,92,49,155,36.4,0.968,32,1 +8,100,74,40,215,39.4,0.661,43,1 +3,128,72,25,190,32.4,0.549,27,1 +10,90,85,32,0,34.9,0.825,56,1 +4,84,90,23,56,39.5,0.159,25,0 +1,88,78,29,76,32,0.365,29,0 +8,186,90,35,225,34.5,0.423,37,1 +5,187,76,27,207,43.6,1.034,53,1 +4,131,68,21,166,33.1,0.16,28,0 +1,164,82,43,67,32.8,0.341,50,0 +4,189,110,31,0,28.5,0.68,37,0 +1,116,70,28,0,27.4,0.204,21,0 +3,84,68,30,106,31.9,0.591,25,0 +6,114,88,0,0,27.8,0.247,66,0 +1,88,62,24,44,29.9,0.422,23,0 +1,84,64,23,115,36.9,0.471,28,0 +7,124,70,33,215,25.5,0.161,37,0 +1,97,70,40,0,38.1,0.218,30,0 +8,110,76,0,0,27.8,0.237,58,0 +11,103,68,40,0,46.2,0.126,42,0 +11,85,74,0,0,30.1,0.3,35,0 +6,125,76,0,0,33.8,0.121,54,1 +0,198,66,32,274,41.3,0.502,28,1 +1,87,68,34,77,37.6,0.401,24,0 +6,99,60,19,54,26.9,0.497,32,0 +0,91,80,0,0,32.4,0.601,27,0 +2,95,54,14,88,26.1,0.748,22,0 +1,99,72,30,18,38.6,0.412,21,0 +6,92,62,32,126,32,0.085,46,0 +4,154,72,29,126,31.3,0.338,37,0 +0,121,66,30,165,34.3,0.203,33,1 +3,78,70,0,0,32.5,0.27,39,0 +2,130,96,0,0,22.6,0.268,21,0 +3,111,58,31,44,29.5,0.43,22,0 +2,98,60,17,120,34.7,0.198,22,0 +1,143,86,30,330,30.1,0.892,23,0 +1,119,44,47,63,35.5,0.28,25,0 +6,108,44,20,130,24,0.813,35,0 +2,118,80,0,0,42.9,0.693,21,1 +10,133,68,0,0,27,0.245,36,0 +2,197,70,99,0,34.7,0.575,62,1 +0,151,90,46,0,42.1,0.371,21,1 +6,109,60,27,0,25,0.206,27,0 +12,121,78,17,0,26.5,0.259,62,0 +8,100,76,0,0,38.7,0.19,42,0 +8,124,76,24,600,28.7,0.687,52,1 +1,93,56,11,0,22.5,0.417,22,0 +8,143,66,0,0,34.9,0.129,41,1 +6,103,66,0,0,24.3,0.249,29,0 +3,176,86,27,156,33.3,1.154,52,1 +0,73,0,0,0,21.1,0.342,25,0 +11,111,84,40,0,46.8,0.925,45,1 +2,112,78,50,140,39.4,0.175,24,0 +3,132,80,0,0,34.4,0.402,44,1 +2,82,52,22,115,28.5,1.699,25,0 +6,123,72,45,230,33.6,0.733,34,0 +0,188,82,14,185,32,0.682,22,1 +0,67,76,0,0,45.3,0.194,46,0 +1,89,24,19,25,27.8,0.559,21,0 +1,173,74,0,0,36.8,0.088,38,1 +1,109,38,18,120,23.1,0.407,26,0 +1,108,88,19,0,27.1,0.4,24,0 +6,96,0,0,0,23.7,0.19,28,0 +1,124,74,36,0,27.8,0.1,30,0 +7,150,78,29,126,35.2,0.692,54,1 +4,183,0,0,0,28.4,0.212,36,1 +1,124,60,32,0,35.8,0.514,21,0 +1,181,78,42,293,40,1.258,22,1 +1,92,62,25,41,19.5,0.482,25,0 +0,152,82,39,272,41.5,0.27,27,0 +1,111,62,13,182,24,0.138,23,0 +3,106,54,21,158,30.9,0.292,24,0 +3,174,58,22,194,32.9,0.593,36,1 +7,168,88,42,321,38.2,0.787,40,1 +6,105,80,28,0,32.5,0.878,26,0 +11,138,74,26,144,36.1,0.557,50,1 +3,106,72,0,0,25.8,0.207,27,0 +6,117,96,0,0,28.7,0.157,30,0 +2,68,62,13,15,20.1,0.257,23,0 +9,112,82,24,0,28.2,1.282,50,1 +0,119,0,0,0,32.4,0.141,24,1 +2,112,86,42,160,38.4,0.246,28,0 +2,92,76,20,0,24.2,1.698,28,0 +6,183,94,0,0,40.8,1.461,45,0 +0,94,70,27,115,43.5,0.347,21,0 +2,108,64,0,0,30.8,0.158,21,0 +4,90,88,47,54,37.7,0.362,29,0 +0,125,68,0,0,24.7,0.206,21,0 +0,132,78,0,0,32.4,0.393,21,0 +5,128,80,0,0,34.6,0.144,45,0 +4,94,65,22,0,24.7,0.148,21,0 +7,114,64,0,0,27.4,0.732,34,1 +0,102,78,40,90,34.5,0.238,24,0 +2,111,60,0,0,26.2,0.343,23,0 +1,128,82,17,183,27.5,0.115,22,0 +10,92,62,0,0,25.9,0.167,31,0 +13,104,72,0,0,31.2,0.465,38,1 +5,104,74,0,0,28.8,0.153,48,0 +2,94,76,18,66,31.6,0.649,23,0 +7,97,76,32,91,40.9,0.871,32,1 +1,100,74,12,46,19.5,0.149,28,0 +0,102,86,17,105,29.3,0.695,27,0 +4,128,70,0,0,34.3,0.303,24,0 +6,147,80,0,0,29.5,0.178,50,1 +4,90,0,0,0,28,0.61,31,0 +3,103,72,30,152,27.6,0.73,27,0 +2,157,74,35,440,39.4,0.134,30,0 +1,167,74,17,144,23.4,0.447,33,1 +0,179,50,36,159,37.8,0.455,22,1 +11,136,84,35,130,28.3,0.26,42,1 +0,107,60,25,0,26.4,0.133,23,0 +1,91,54,25,100,25.2,0.234,23,0 +1,117,60,23,106,33.8,0.466,27,0 +5,123,74,40,77,34.1,0.269,28,0 +2,120,54,0,0,26.8,0.455,27,0 +1,106,70,28,135,34.2,0.142,22,0 +2,155,52,27,540,38.7,0.24,25,1 +2,101,58,35,90,21.8,0.155,22,0 +1,120,80,48,200,38.9,1.162,41,0 +11,127,106,0,0,39,0.19,51,0 +3,80,82,31,70,34.2,1.292,27,1 +10,162,84,0,0,27.7,0.182,54,0 +1,199,76,43,0,42.9,1.394,22,1 +8,167,106,46,231,37.6,0.165,43,1 +9,145,80,46,130,37.9,0.637,40,1 +6,115,60,39,0,33.7,0.245,40,1 +1,112,80,45,132,34.8,0.217,24,0 +4,145,82,18,0,32.5,0.235,70,1 +10,111,70,27,0,27.5,0.141,40,1 +6,98,58,33,190,34,0.43,43,0 +9,154,78,30,100,30.9,0.164,45,0 +6,165,68,26,168,33.6,0.631,49,0 +1,99,58,10,0,25.4,0.551,21,0 +10,68,106,23,49,35.5,0.285,47,0 +3,123,100,35,240,57.3,0.88,22,0 +8,91,82,0,0,35.6,0.587,68,0 +6,195,70,0,0,30.9,0.328,31,1 +9,156,86,0,0,24.8,0.23,53,1 +0,93,60,0,0,35.3,0.263,25,0 +3,121,52,0,0,36,0.127,25,1 +2,101,58,17,265,24.2,0.614,23,0 +2,56,56,28,45,24.2,0.332,22,0 +0,162,76,36,0,49.6,0.364,26,1 +0,95,64,39,105,44.6,0.366,22,0 +4,125,80,0,0,32.3,0.536,27,1 +5,136,82,0,0,0,0.64,69,0 +2,129,74,26,205,33.2,0.591,25,0 +3,130,64,0,0,23.1,0.314,22,0 +1,107,50,19,0,28.3,0.181,29,0 +1,140,74,26,180,24.1,0.828,23,0 +1,144,82,46,180,46.1,0.335,46,1 +8,107,80,0,0,24.6,0.856,34,0 +13,158,114,0,0,42.3,0.257,44,1 +2,121,70,32,95,39.1,0.886,23,0 +7,129,68,49,125,38.5,0.439,43,1 +2,90,60,0,0,23.5,0.191,25,0 +7,142,90,24,480,30.4,0.128,43,1 +3,169,74,19,125,29.9,0.268,31,1 +0,99,0,0,0,25,0.253,22,0 +4,127,88,11,155,34.5,0.598,28,0 +4,118,70,0,0,44.5,0.904,26,0 +2,122,76,27,200,35.9,0.483,26,0 +6,125,78,31,0,27.6,0.565,49,1 +1,168,88,29,0,35,0.905,52,1 +2,129,0,0,0,38.5,0.304,41,0 +4,110,76,20,100,28.4,0.118,27,0 +6,80,80,36,0,39.8,0.177,28,0 +10,115,0,0,0,0,0.261,30,1 +2,127,46,21,335,34.4,0.176,22,0 +9,164,78,0,0,32.8,0.148,45,1 +2,93,64,32,160,38,0.674,23,1 +3,158,64,13,387,31.2,0.295,24,0 +5,126,78,27,22,29.6,0.439,40,0 +10,129,62,36,0,41.2,0.441,38,1 +0,134,58,20,291,26.4,0.352,21,0 +3,102,74,0,0,29.5,0.121,32,0 +7,187,50,33,392,33.9,0.826,34,1 +3,173,78,39,185,33.8,0.97,31,1 +10,94,72,18,0,23.1,0.595,56,0 +1,108,60,46,178,35.5,0.415,24,0 +5,97,76,27,0,35.6,0.378,52,1 +4,83,86,19,0,29.3,0.317,34,0 +1,114,66,36,200,38.1,0.289,21,0 +1,149,68,29,127,29.3,0.349,42,1 +5,117,86,30,105,39.1,0.251,42,0 +1,111,94,0,0,32.8,0.265,45,0 +4,112,78,40,0,39.4,0.236,38,0 +1,116,78,29,180,36.1,0.496,25,0 +0,141,84,26,0,32.4,0.433,22,0 +2,175,88,0,0,22.9,0.326,22,0 +2,92,52,0,0,30.1,0.141,22,0 +3,130,78,23,79,28.4,0.323,34,1 +8,120,86,0,0,28.4,0.259,22,1 +2,174,88,37,120,44.5,0.646,24,1 +2,106,56,27,165,29,0.426,22,0 +2,105,75,0,0,23.3,0.56,53,0 +4,95,60,32,0,35.4,0.284,28,0 +0,126,86,27,120,27.4,0.515,21,0 +8,65,72,23,0,32,0.6,42,0 +2,99,60,17,160,36.6,0.453,21,0 +1,102,74,0,0,39.5,0.293,42,1 +11,120,80,37,150,42.3,0.785,48,1 +3,102,44,20,94,30.8,0.4,26,0 +1,109,58,18,116,28.5,0.219,22,0 +9,140,94,0,0,32.7,0.734,45,1 +13,153,88,37,140,40.6,1.174,39,0 +12,100,84,33,105,30,0.488,46,0 +1,147,94,41,0,49.3,0.358,27,1 +1,81,74,41,57,46.3,1.096,32,0 +3,187,70,22,200,36.4,0.408,36,1 +6,162,62,0,0,24.3,0.178,50,1 +4,136,70,0,0,31.2,1.182,22,1 +1,121,78,39,74,39,0.261,28,0 +3,108,62,24,0,26,0.223,25,0 +0,181,88,44,510,43.3,0.222,26,1 +8,154,78,32,0,32.4,0.443,45,1 +1,128,88,39,110,36.5,1.057,37,1 +7,137,90,41,0,32,0.391,39,0 +0,123,72,0,0,36.3,0.258,52,1 +1,106,76,0,0,37.5,0.197,26,0 +6,190,92,0,0,35.5,0.278,66,1 +2,88,58,26,16,28.4,0.766,22,0 +9,170,74,31,0,44,0.403,43,1 +9,89,62,0,0,22.5,0.142,33,0 +10,101,76,48,180,32.9,0.171,63,0 +2,122,70,27,0,36.8,0.34,27,0 +5,121,72,23,112,26.2,0.245,30,0 +1,126,60,0,0,30.1,0.349,47,1 +1,93,70,31,0,30.4,0.315,23,0 \ No newline at end of file diff --git a/examples/splitlearning_diabetes/docker-compose.override.dev.yaml b/examples/splitlearning_diabetes/docker-compose.override.dev.yaml new file mode 100644 index 000000000..bb1184b69 --- /dev/null +++ b/examples/splitlearning_diabetes/docker-compose.override.dev.yaml @@ -0,0 +1,22 @@ +# Compose schema version +version: '3.4' + +x-env: &defaults + GET_HOSTS_FROM: dns + FEDN_PACKAGE_EXTRACT_DIR: package + FEDN_NUM_DATA_SPLITS: 2 + +services: + combiner: + extends: + file: ${HOST_REPO_DIR:-.}/docker-compose.yaml + service: combiner + build: + args: + INSTALL_TORCH: "1" + environment: + <<: *defaults + FEDN_LABELS_PATH: /app/data/clients/labels.pt + volumes: + - ${HOST_REPO_DIR:-.}/fedn:/app/fedn + - ${HOST_REPO_DIR:-.}/examples/splitlearning_diabetes/client/data:/app/data diff --git a/examples/splitlearning_diabetes/docker-compose.override.yaml b/examples/splitlearning_diabetes/docker-compose.override.yaml new file mode 100644 index 000000000..0726df600 --- /dev/null +++ b/examples/splitlearning_diabetes/docker-compose.override.yaml @@ -0,0 +1,50 @@ +# Compose schema version +version: '3.4' + +x-env: &defaults + GET_HOSTS_FROM: dns + FEDN_PACKAGE_EXTRACT_DIR: package + FEDN_NUM_DATA_SPLITS: 2 + +services: + combiner: + extends: + file: ${HOST_REPO_DIR:-.}/docker-compose.yaml + service: combiner + build: + args: + INSTALL_TORCH: "1" + environment: + <<: *defaults + FEDN_LABELS_PATH: /app/data/clients/labels.pt + volumes: + - ${HOST_REPO_DIR:-.}/fedn:/app/fedn + - ${HOST_REPO_DIR:-.}/examples/splitlearning_diabetes/client/data:/app/data + + client1: + extends: + file: ${HOST_REPO_DIR:-.}/docker-compose.yaml + service: client + environment: + <<: *defaults + FEDN_DATA_PATH: /app/package/data/clients/1/diabetes.pt + USE_DOCKER_PATH: true + deploy: + replicas: 1 + volumes: + - ${HOST_REPO_DIR:-.}/fedn:/app/fedn + - ${HOST_REPO_DIR:-.}/examples/splitlearning_diabetes/data:/app/data + + client2: + extends: + file: ${HOST_REPO_DIR:-.}/docker-compose.yaml + service: client + environment: + <<: *defaults + FEDN_DATA_PATH: /app/package/data/clients/2/diabetes.pt + USE_DOCKER_PATH: true + deploy: + replicas: 1 + volumes: + - ${HOST_REPO_DIR:-.}/fedn:/app/fedn + - ${HOST_REPO_DIR:-.}/examples/splitlearning_diabetes/data:/app/data diff --git a/examples/splitlearning_diabetes/readme.rst b/examples/splitlearning_diabetes/readme.rst new file mode 100644 index 000000000..cff29f113 --- /dev/null +++ b/examples/splitlearning_diabetes/readme.rst @@ -0,0 +1,152 @@ +Vertical FL with Split Learning in FEDn +======================================= + +To run the commands, first git clone this repository and switch to the branch containing this code. +In order to be able to run this example, you need to install FEDn. +It is best if you create a virtual environment for this. + +.. code-block:: bash + + git clone https://github.com/scaleoutsystems/fedn.git + cd fedn + pip install fedn + +In your /etc/hosts file, add the following lines: + +.. code-block:: text + + 127.0.0.1 localhost + 127.0.0.1 combiner + 127.0.0.1 minio + 127.0.0.1 mongo + + +Data Preparation +---------------- + +Make sure the diabetes dataset is downloaded in the splitlearning_diabetes/data folder with the files "labels.csv", "train.csv" and "test.csv". +For convenience, the dataset is already provided. If necessary, it can be downloaded from https://www.kaggle.com/datasets/uciml/pima-indians-diabetes-database?resource=download. + +We split the vertically dataset between 2 clients. For this, locate yourself in the *examples/splitlearning_diabetes/client* folder + +.. code-block:: bash + + cd examples/splitlearning_diabetes/client + +and run the following command to generate the data for the two clients: + +.. code-block:: bash + + python3 data.py + +Compute Package +--------------- + +Then, locate yourself into the examples/splitlearning_diabetes folder: + +.. code-block:: bash + + cd .. + +To create the compute package, run: + +.. code-block:: bash + + fedn package create --path client + +Note: For split learning, we do not need a seed model in contrast to horizontal federated learning. + +Local Setup with FEDn +--------------------- + +To execute the split learning example on your local machine, run the following commands in different terminals (docker is required). + +To start mongo and minio: + +.. code-block:: bash + + docker compose up -d mongo minio + +We need to set some environment variables in order to let the system know where to find the data and labels. +In another terminal (make sure to be located in the examples/splitlearning_diabetes folder), set the compute package environment variable and start the controller. + +.. code-block:: bash + + export FEDN_COMPUTE_PACKAGE_DIR=. + fedn controller start + +Now, we set the path to the labels.pt file in the client folder and start the combiner (from another terminal, again from the examples/splitlearning_diabetes folder) + +.. code-block:: bash + + export FEDN_LABELS_PATH=./client/data/clients/labels.pt + fedn combiner start + +**NOTE** + +For convenience, you can run the following docker compose command to start controller, combiner, mongo and minio, together with the correct environment variables: + +.. code-block:: bash + + docker compose \ + -f ../../docker-compose.yaml \ + -f docker-compose.override.dev.yaml \ + up + +Now, we will connect 2 clients. Open 2 new terminals and locate yourself into the splitlearning_diabetes folder. As both clients should have access to their respective vertical dataset, +the datapath should be set to the different data folders that are generated by the data.py script. +To start the first client, run: + +.. code-block:: bash + + export FEDN_DATA_PATH=./data/clients/1/diabetes.pt + fedn client start --api-url http://localhost:8092 --local-package + +and to start the second client, run: + +.. code-block:: bash + + export FEDN_DATA_PATH=./data/clients/2/diabetes.pt + fedn client start --api-url http://localhost:8092 --local-package + + +**NOTE** + +Instead of setting up the clients manually, you can also run the following docker compose command. It will set up all server-side (controller, combiner, etc.) and the 2 clients automatically inside a docker container. +All environment variables are handled as well. + +.. code-block:: bash + + docker compose \ + -f ../../docker-compose.yaml \ + -f docker-compose.override.yaml \ + up + + +Starting the Split Learning Training +------------------------------------- + +We are going to start the training through the API Client. +Go to the *run_splitlearning.ipynb* file in the *splitlearning_diabetes* folder and execute the cells. +The splitlearning session will start running. + +Clean-up +--------- + +After the training is finished, run the following command for clean-up: + +.. code-block:: bash + + docker compose down -v + +Modifying the example +--------------------- + +In order to change the split learning model architecture, you need to modify two files: +The *model.py* file to change the client-side model, and the *splitlearningagg.py* file +in the *combiner* folder to change the server-side model. + +Update the *data.py* file if you want to change the dataset. + + + diff --git a/examples/splitlearning_diabetes/run_splitlearning.ipynb b/examples/splitlearning_diabetes/run_splitlearning.ipynb new file mode 100644 index 000000000..6a16e9e07 --- /dev/null +++ b/examples/splitlearning_diabetes/run_splitlearning.ipynb @@ -0,0 +1,132 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 45, + "metadata": {}, + "outputs": [], + "source": [ + "from fedn import APIClient" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": {}, + "outputs": [], + "source": [ + "DISCOVER_HOST = '127.0.0.1'\n", + "DISCOVER_PORT = 8092\n", + "client = APIClient(DISCOVER_HOST, DISCOVER_PORT)" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [], + "source": [ + "client.set_active_package('client/package.tgz', 'splitlearninghelper', 'package-name')" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'count': 2,\n", + " 'result': [{'client_id': '74e6e1c9-95db-4613-9b20-91706e680d79',\n", + " 'combiner': 'combiner',\n", + " 'combiner_preferred': None,\n", + " 'committed_at': 'Tue, 13 May 2025 12:27:23 GMT',\n", + " 'ip': '172.18.0.1',\n", + " 'last_seen': 'Tue, 13 May 2025 12:27:25 GMT',\n", + " 'name': 'client23c044d9',\n", + " 'package': 'local',\n", + " 'status': 'online',\n", + " 'updated_at': 'Tue, 13 May 2025 12:27:25 GMT'},\n", + " {'client_id': '213568c9-df82-42a2-a6c8-43809833e930',\n", + " 'combiner': 'combiner',\n", + " 'combiner_preferred': None,\n", + " 'committed_at': 'Tue, 13 May 2025 12:27:21 GMT',\n", + " 'ip': '172.18.0.1',\n", + " 'last_seen': 'Tue, 13 May 2025 12:27:23 GMT',\n", + " 'name': 'client307e271e',\n", + " 'package': 'local',\n", + " 'status': 'online',\n", + " 'updated_at': 'Tue, 13 May 2025 12:27:23 GMT'}]}" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "client.get_active_clients()" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'message': 'Splitlearning session started',\n", + " 'session_id': '2390fbb8-8c61-419a-bf07-01e1ef03c64b'}" + ] + }, + "execution_count": 49, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "session_config = {\n", + " \"name\": \"test_session\",\n", + " \"model_id\": \"1\",\n", + " \"helper\": \"splitlearninghelper\",\n", + " \"aggregator\": \"splitlearningagg\",\n", + " \"rounds\": 50,\n", + " \"round_timeout\": 60,\n", + " \"validate\": True\n", + "}\n", + "\n", + "client.start_session(**session_config)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/fedn/cli/combiner_cmd.py b/fedn/cli/combiner_cmd.py index f27e2d9a1..c86b841a8 100644 --- a/fedn/cli/combiner_cmd.py +++ b/fedn/cli/combiner_cmd.py @@ -67,7 +67,7 @@ def start_cmd(ctx, discoverhost, discoverport, token, name, host, port, fqdn, se network_id = get_network_config() # TODO: set storage_type ? - repository = Repository(modelstorage_config["storage_config"], init_buckets=False) + repository = Repository(modelstorage_config["storage_config"], storage_type=modelstorage_config["storage_type"], init_buckets=False) db = DatabaseConnection(statestore_config, network_id) diff --git a/fedn/cli/project_cmd.py b/fedn/cli/project_cmd.py index e7083c46c..6297e1703 100644 --- a/fedn/cli/project_cmd.py +++ b/fedn/cli/project_cmd.py @@ -4,7 +4,7 @@ import requests from .main import main -from .shared import HOME_DIR, STUDIO_DEFAULTS, get_api_url, get_context, get_response, get_token, print_response, set_context +from .shared import HOME_DIR, STUDIO_DEFAULTS, get_api_url, get_context, get_response, get_token, pretty_print_projects, print_response, set_context @main.group("project") @@ -51,14 +51,23 @@ def delete_project(ctx, id: str = None, protocol: str = None, host: str = None, @click.option("-n", "--name", required=False, default=None, help="Name of new project.") @click.option("-p", "--protocol", required=False, default=STUDIO_DEFAULTS["protocol"], help="Communication protocol of studio (api)") @click.option("-H", "--host", required=False, default=STUDIO_DEFAULTS["host"], help="Hostname of studio (api)") -@click.option("--no-interactive", is_flag=True, help="Run in non-interactive mode.") @click.option("--branch", required=False, default=None, help="Studio branch (default main). Requires admin in Studio") @click.option("--image", required=False, default=None, help="Container image. Requires admin in Studio") @click.option("--repository", required=False, default=None, help="Container image repository. Requires admin in Studio") +@click.option("--no-interactive", is_flag=True, help="Run in non-interactive mode.") +@click.option("--no-header", is_flag=True, help="Run in non-header mode.") @project_cmd.command("create") @click.pass_context def create_project( - ctx, name: str = None, protocol: str = None, host: str = None, no_interactive: bool = False, branch: str = None, image: str = None, repository: str = None + ctx, + name: str = None, + protocol: str = None, + host: str = None, + no_interactive: bool = False, + no_header: bool = False, + branch: str = None, + image: str = None, + repository: str = None, ): """Create project. :param ctx: @@ -75,21 +84,25 @@ def create_project( if no_interactive: click.secho("Project name is required.", fg="red") return - name = input("Please enter a project name: ") + name = input("Please enter a project name: ") if len(name) > 46: - click.secho("Project name or description too long.", fg="red") + click.secho("Project name too long.", fg="red") else: # Call the authentication API try: - requests.post(url, data={"name": name, "studio_branch": branch, "fedn_image": image, "fedn_repo": repository}, headers=headers) + response = requests.post(url, data={"name": name, "studio_branch": branch, "fedn_image": image, "fedn_repo": repository}, headers=headers) + response_message = response.json().get("message") + if response.status_code == 201: + click.secho(f"Project with name '{name}' created.", fg="green") + elif response.status_code == 400: + click.secho(f"Unexpected error: {response_message}", fg="red") except requests.exceptions.RequestException as e: click.secho(str(e), fg="red") - click.secho("Project created.", fg="green") @click.option("-p", "--protocol", required=False, default=STUDIO_DEFAULTS["protocol"], help="Communication protocol of studio (api)") @click.option("-H", "--host", required=False, default=STUDIO_DEFAULTS["host"], help="Hostname of studio (api)") -@click.option("-no-header", "--no-header", required=False, help="list projects without headers") +@click.option("--no-header", is_flag=True, help="Run in non-header mode.") @project_cmd.command("list") @click.pass_context def list_projects(ctx, protocol: str = None, host: str = None, no_header: bool = False): @@ -99,25 +112,13 @@ def list_projects(ctx, protocol: str = None, host: str = None, no_header: bool = """ studio_api = True - if no_header: - headers = {} - else: - headers = {} + headers = {} response = get_response(protocol=protocol, host=host, port=None, endpoint="projects", token=None, headers=headers, usr_api=studio_api, usr_token=True) if response.status_code == 200: response_json = response.json() if len(response_json) > 0: - context_path = os.path.join(HOME_DIR, ".fedn") - context_data = get_context(context_path) - active_project = context_data.get("Active project id") - - for i in response_json: - project_name = i.get("slug") - if project_name == active_project: - click.secho(f"{project_name} (active)", fg="green") - else: - click.secho(project_name) + pretty_print_projects(response_json, no_header) else: click.secho(f"Unexpected error: {response.status_code}", fg="red") diff --git a/fedn/cli/run_cmd.py b/fedn/cli/run_cmd.py index 80052149f..42c202b6f 100644 --- a/fedn/cli/run_cmd.py +++ b/fedn/cli/run_cmd.py @@ -7,7 +7,7 @@ from fedn.cli.main import main from fedn.cli.shared import apply_config -from fedn.common.config import get_modelstorage_config, get_network_config, get_statestore_config +from fedn.common.config import FEDN_OBJECT_STORAGE_TYPE, get_modelstorage_config, get_network_config, get_statestore_config from fedn.common.log_config import logger from fedn.network.storage.dbconnection import DatabaseConnection from fedn.network.storage.s3.repository import Repository @@ -225,8 +225,8 @@ def combiner_cmd(ctx, discoverhost, discoverport, token, name, host, port, fqdn, statestore_config = get_statestore_config() network_id = get_network_config() - # TODO: set storage_type ? - repository = Repository(modelstorage_config["storage_config"], init_buckets=False) + storage_type = modelstorage_config.get("storage_type", FEDN_OBJECT_STORAGE_TYPE) + repository = Repository(modelstorage_config["storage_config"], storage_type=storage_type, init_buckets=False) db = DatabaseConnection(statestore_config, network_id) diff --git a/fedn/cli/session_cmd.py b/fedn/cli/session_cmd.py index 61aa6e4d5..b4430490c 100644 --- a/fedn/cli/session_cmd.py +++ b/fedn/cli/session_cmd.py @@ -143,18 +143,21 @@ def start_session( if response.status_code == 201: session_id = response.json()["session_id"] url = get_api_url(protocol, host, port, "sessions/start", usr_api=False) - response = requests.post( - url, - json={ - "session_id": session_id, - "rounds": rounds, - "round_timeout": round_timeout, - }, - headers=headers, - verify=False, - ) - response_json = response.json() - response_json["session_id"] = session_id - click.secho(f"Session started successfully: {response_json}", fg="green") + try: + response = requests.post( + url, + json={ + "session_id": session_id, + "rounds": rounds, + "round_timeout": round_timeout, + }, + headers=headers, + verify=False, + ) + response_json = response.json() + response_json["session_id"] = session_id + click.secho(f"Session started successfully: {response_json}", fg="green") + except requests.exceptions.RequestException: + click.secho(f"Failed to start session: {response.json()}", fg="red") else: click.secho(f"Failed to start session: {response.json()}", fg="red") diff --git a/fedn/cli/shared.py b/fedn/cli/shared.py index 693b48137..ccc9fcead 100644 --- a/fedn/cli/shared.py +++ b/fedn/cli/shared.py @@ -131,6 +131,44 @@ def print_response(response, entity_name: str, so): click.echo(f"Error: {response.status_code}") +def pretty_print_projects(data, no_header): + """Prints the project information in tabular format. + :param data: + type: array + description: list of entities + return: None + """ + if not isinstance(data, list): + data = [data] + + headers = ["Name", "ID", "Owner", "Status", "Created At", "FEDn Version"] + + # Prepare rows + rows = [] + for i in data: + rows.append([i.get("name", ""), i.get("slug", ""), i.get("owner_username", ""), i.get("status", ""), i.get("created_at", ""), i.get("app_version", "")]) + + # Calculate column widths + + col_widths = [len(h) for h in headers] + for row in rows: + for idx, value in enumerate(row): + col_widths[idx] = max(col_widths[idx], len(str(value))) + + # Helper to format a row + def format_row(row): + return " | ".join(f"{str(val):<{col_widths[idx]}}" for idx, val in enumerate(row)) + + # Print header + if not no_header: + print(format_row(headers)) + print("-+-".join("-" * w for w in col_widths)) + + # Print rows + for row in rows: + click.secho(format_row(row)) + + def set_context(context_path, context_data): """Saves context data as yaml file in given path""" try: diff --git a/fedn/common/config.py b/fedn/common/config.py index 5cab273f5..8bb347910 100644 --- a/fedn/common/config.py +++ b/fedn/common/config.py @@ -17,9 +17,24 @@ FEDN_CONNECT_API_SECURE = os.environ.get("FEDN_CONNECT_API_SECURE", "true").lower() == "true" FEDN_PACKAGE_EXTRACT_DIR = os.environ.get("FEDN_PACKAGE_EXTRACT_DIR", "package") - FEDN_COMPUTE_PACKAGE_DIR = os.environ.get("FEDN_COMPUTE_PACKAGE_DIR", "/app/client/package/") +FEDN_OBJECT_STORAGE_TYPE = os.environ.get("FEDN_OBJECT_STORAGE_TYPE", "BOTO3").upper() +FEDN_OBJECT_MODEL_BUCKET = os.environ.get("FEDN_OBJECT_MODEL_BUCKET", "fedn-model") +FEDN_OBJECT_CONTEXT_BUCKET = os.environ.get("FEDN_OBJECT_CONTEXT_BUCKET", "fedn-context") +FEDN_OBJECT_PREDICTION_BUCKET = os.environ.get("FEDN_OBJECT_PREDICTION_BUCKET", "fedn-prediction") +FEDN_OBJECT_STORAGE_REGION = os.environ.get("FEDN_OBJECT_STORAGE_REGION", "eu-west-1") +FEDN_OBJECT_STORAGE_ENDPOINT = os.environ.get("FEDN_OBJECT_STORAGE_ENDPOINT", "http://minio:9000") +FEDN_OBJECT_STORAGE_ACCESS_KEY = os.environ.get("FEDN_OBJECT_STORAGE_ACCESS_KEY", "") +FEDN_OBJECT_STORAGE_SECRET_KEY = os.environ.get("FEDN_OBJECT_STORAGE_SECRET_KEY", "") +FEDN_OBJECT_STORAGE_SECURE_MODE = os.environ.get("FEDN_OBJECT_STORAGE_SECURE_MODE", "true").lower() == "true" +FEDN_OBJECT_STORAGE_VERIFY_SSL = os.environ.get("FEDN_OBJECT_STORAGE_VERIFY_SSL", "true").lower() == "true" +FEDN_OBJECT_STORAGE_BUCKETS = { + "model": os.environ.get("FEDN_OBJECT_MODEL_BUCKET", "fedn-model"), + "context": os.environ.get("FEDN_OBJECT_CONTEXT_BUCKET", "fedn-context"), + "prediction": os.environ.get("FEDN_OBJECT_PREDICTION_BUCKET", "fedn-prediction"), +} + def get_environment_config(): """Get the configuration from environment variables.""" diff --git a/fedn/common/log_config.py b/fedn/common/log_config.py index 0d3ddb96c..5b6b6b578 100644 --- a/fedn/common/log_config.py +++ b/fedn/common/log_config.py @@ -4,9 +4,25 @@ import requests import urllib3 - -log_levels = {"DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING, "ERROR": logging.ERROR, "CRITICAL": logging.CRITICAL} - +from opentelemetry import trace +from opentelemetry._logs import set_logger_provider +from opentelemetry.exporter.otlp.proto.grpc._log_exporter import \ + OTLPLogExporter +from opentelemetry.exporter.otlp.proto.http.trace_exporter import \ + OTLPSpanExporter +from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler +from opentelemetry.sdk._logs.export import BatchLogRecordProcessor +from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from opentelemetry.trace import NoOpTracerProvider, get_tracer + +log_levels = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, +} urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) logging.getLogger("urllib3").setLevel(logging.ERROR) @@ -33,7 +49,6 @@ def emit(self, record): "project": os.environ.get("PROJECT_ID"), "appinstance": os.environ.get("APP_ID"), } - # Setup headers headers = { "Content-type": "application/json", } @@ -42,12 +57,9 @@ def emit(self, record): headers["Authorization"] = f"{remote_token_protocol} {self.token}" if self.method.lower() == "post": requests.post(self.host + self.url, json=log_entry, headers=headers) - else: - # No other methods implemented. - return -# Remote logging can only be configured via environment variables for now. +# Remote logging configuration REMOTE_LOG_SERVER = os.environ.get("FEDN_REMOTE_LOG_SERVER", False) REMOTE_LOG_PATH = os.environ.get("FEDN_REMOTE_LOG_PATH", False) REMOTE_LOG_LEVEL = os.environ.get("FEDN_REMOTE_LOG_LEVEL", "INFO") @@ -58,13 +70,32 @@ def emit(self, record): http_handler = StudioHTTPHandler(host=REMOTE_LOG_SERVER, url=REMOTE_LOG_PATH, method="POST", token=remote_token) http_handler.setLevel(rloglevel) - logger.addHandler(http_handler) + #logger.addHandler(http_handler) + + +if os.environ.get("OTEL_SERVICE_NAME", None): + # OpenTelemetry Logging Configuration + logger_provider = LoggerProvider() + set_logger_provider(logger_provider) + exporter = OTLPLogExporter() + logger_provider.add_log_record_processor(BatchLogRecordProcessor(exporter)) + otel_handler = LoggingHandler(logger_provider=logger_provider) + #logger.addHandler(otel_handler) # for Transient Error relating to exporting logs to honeycomb + + # Set up trace provider and exporter + trace_provider = SDKTracerProvider() + trace.set_tracer_provider(trace_provider) + + trace_exporter = OTLPSpanExporter() + trace_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) + tracer = trace.get_tracer(__name__) +else: + trace.set_tracer_provider(NoOpTracerProvider()) + tracer = get_tracer(__name__) def set_log_level_from_string(level_str): - """Set the log level based on a string input. - """ - # Mapping of string representation to logging constants + """Set the log level based on a string input.""" level_mapping = { "CRITICAL": logging.CRITICAL, "ERROR": logging.ERROR, @@ -72,30 +103,23 @@ def set_log_level_from_string(level_str): "INFO": logging.INFO, "DEBUG": logging.DEBUG, } - - # Get the logging level from the mapping level = level_mapping.get(level_str.upper()) - if not level: raise ValueError(f"Invalid log level: {level_str}") - - # Set the log level logger.setLevel(level) def set_log_stream(log_file): - """Redirect the log stream to a specified file, if log_file is set. - """ + """Redirect the log stream to a specified file, if log_file is set.""" if not log_file: return - - # Remove existing handlers for h in logger.handlers[:]: logger.removeHandler(h) - - # Create a FileHandler file_handler = logging.FileHandler(log_file) file_handler.setFormatter(formatter) - - # Add the file handler to the logger logger.addHandler(file_handler) + + +def shutdown_logging(): + """Shutdown the logger provider to ensure logs are flushed before exit.""" + logger_provider.shutdown() diff --git a/fedn/common/settings-controller.yaml.template b/fedn/common/settings-controller.yaml.template index 3667eb189..2a0c3a405 100644 --- a/fedn/common/settings-controller.yaml.template +++ b/fedn/common/settings-controller.yaml.template @@ -18,12 +18,12 @@ statestore: port: 5432 storage: - storage_type: S3 + storage_type: BOTO3 storage_config: - storage_hostname: localhost - storage_port: 9000 + storage_endpoint_url: http://minio:9000 storage_access_key: fedn_admin storage_secret_key: password storage_bucket: fedn-models context_bucket: fedn-context - storage_secure_mode: False + storage_secure_mode: False + storage_verify_ssl: False diff --git a/fedn/network/api/client.py b/fedn/network/api/client.py index 1e6ca3c96..951a45c24 100644 --- a/fedn/network/api/client.py +++ b/fedn/network/api/client.py @@ -292,14 +292,15 @@ def get_model_trail(self, id: str = None, include_self: bool = True, reverse: bo """ if not id: model = self.get_active_model() - if "id" in model: - id = model["id"] + if "model_id" in model: + id = model["model_id"] else: return model _headers = self.headers.copy() _count: int = n_max if n_max else self.get_models_count() + _headers["X-Limit"] = str(_count) _headers["X-Reverse"] = "true" if reverse else "false" @@ -639,12 +640,18 @@ def start_session( :return: A dict with success or failure message and session config. :rtype: dict """ + is_splitlearning = aggregator == "splitlearningagg" + if is_splitlearning: + return self.start_splitlearning_session( + name, model_id, round_timeout, rounds, round_buffer_size, delete_models, validate, min_clients, requested_clients + ) + if model_id is None: - headers = self.headers.copy() - headers["X-Limit"] = "1" - headers["X-Sort-Key"] = "committed_at" - headers["X-Sort-Order"] = "desc" - response = requests.get(self._get_url_api_v1("models"), verify=self.verify, headers=headers) + _headers = self.headers.copy() + _headers["X-Limit"] = "1" + _headers["X-Sort-Key"] = "committed_at" + _headers["X-Sort-Order"] = "desc" + response = requests.get(self._get_url_api_v1("models/"), verify=self.verify, headers=_headers) if response.status_code == 200: json = response.json() if "result" in json and len(json["result"]) > 0: @@ -652,7 +659,7 @@ def start_session( else: return {"message": "No models found in the repository"} else: - return response.json() + return {"message": "No models found in the repository"} if helper is None: response = requests.get(self._get_url_api_v1("helpers/active"), verify=self.verify, headers=self.headers) @@ -698,6 +705,85 @@ def start_session( verify=self.verify, headers=self.headers, ) + # Try to parse JSON, but handle the case where it fails + try: + response_json = response.json() + response_json["session_id"] = session_id + return response_json + except requests.exceptions.JSONDecodeError: + # Handle invalid JSON response + return {"success": response.status_code < 400, "session_id": session_id, "message": f"Session started with status code {response.status_code}"} + + _json = response.json() + return _json + + def start_splitlearning_session( + self, + name: str = None, + model_id: str = None, + round_timeout: int = 180, + rounds: int = 5, + round_buffer_size: int = -1, + delete_models: bool = True, + validate: bool = False, + min_clients: int = 1, + requested_clients: int = 8, + ): + """Start a new splitlearning session. + + :param name: The name of the session + :type name: str + :param model_id: The id of the initial model. + :type model_id: str + :param round_timeout: The round timeout to use in seconds. + :type round_timeout: int + :param rounds: The number of rounds to perform. + :type rounds: int + :param round_buffer_size: The round buffer size to use. + :type round_buffer_size: int + :param delete_models: Whether to delete models after each round at combiner (save storage). + :type delete_models: bool + :param validate: Whether to validate the model after each round. + :type validate: bool + :param min_clients: The minimum number of clients required. + :type min_clients: int + :param requested_clients: The requested number of clients. + :type requested_clients: int + :return: A dict with success or failure message and session config. + :rtype: dict + """ + response = requests.post( + self._get_url_api_v1("sessions/"), + json={ + "name": name, + "session_config": { + "aggregator": "splitlearningagg", + "model_id": model_id, + "round_timeout": round_timeout, + "buffer_size": round_buffer_size, + "delete_models_storage": delete_models, + "clients_required": min_clients, + "requested_clients": requested_clients, + "validate": validate, + "helper_type": "splitlearninghelper", + }, + }, + verify=self.verify, + headers=self.headers, + ) + + if response.status_code == 201: + session_id = response.json()["session_id"] + response = requests.post( + self._get_url_api_v1("sessions/start_splitlearning_session"), + json={ + "session_id": session_id, + "rounds": rounds, + "round_timeout": round_timeout, + }, + verify=self.verify, + headers=self.headers, + ) response_json = response.json() response_json["session_id"] = session_id return response_json @@ -705,6 +791,47 @@ def start_session( _json = response.json() return _json + def continue_session(self, session_id: str, rounds: int = 5, round_timeout: int = 180): + """Continue a session. + + :param session_id: The id of the session to continue. + :type session_id: str + :param rounds: The number of rounds to perform. + :type rounds: int + :param round_timeout: The round timeout to use in seconds. + :type round_timeout: int + :return: A dict with success or failure message and session config. + :rtype: dict + """ + if not session_id: + return {"message": "No session id provided."} + if rounds is None or rounds <= 0: + return {"message": "Invalid number of rounds provided. Must be greater than 0."} + if round_timeout is None or round_timeout <= 0: + return {"message": "Invalid round timeout provided. Must be greater than 0."} + # Check if session exists + session = self.get_session(session_id) + if not session or "session_id" not in session: + return {"message": "Session not found."} + # Check if session is finished + if not self.session_is_finished(session_id): + return {"message": "Session is already running."} + + response = requests.post( + self._get_url_api_v1("sessions/start"), + json={ + "session_id": session_id, + "rounds": rounds, + "round_timeout": round_timeout, + }, + verify=self.verify, + headers=self.headers, + ) + + _json = response.json() + + return _json + # --- Statuses --- # def get_status(self, id: str): @@ -951,3 +1078,43 @@ def start_predictions(self, prediction_id: str = None, model_id: str = None): _json = response.json() return _json + + # --- Client --- # + + def get_current_attributes(self, client_list): + """Get the current attributes of the client. + + :param client_list: The list of clients to get the attributes for or a single client_id + :type client_list: list|str + :raises ValueError: If client_list is not a list or empty. + :return: The current attributes of the client. + :rtype: dict + """ + if not isinstance(client_list, list): + if isinstance(client_list, str): + client_list = [client_list] + else: + raise ValueError("client_list must be a list") + if len(client_list) == 0: + raise ValueError("client_list must not be empty") + json = {"client_ids": client_list} + response = requests.post(self._get_url_api_v1("attributes/current"), json=json, verify=self.verify, headers=self.headers) + _json = response.json() + return _json + + def add_attributes(self, attribute: dict) -> dict: + """Add or update client attributes via the controller API. + + :param attribute: A dict matching AttributeDTO.schema, e.g. + { + "key": "charging", + "value": "true", + "sender": {"name": "", "role": "", "client_id": "abc123"} + } + :return: Parsed JSON response from the server. + :rtype: dict + """ + url = self._get_url_api_v1("attributes/") + response = requests.post(url, json=attribute, headers=self.headers, verify=self.verify) + response.raise_for_status() + return response.json() diff --git a/fedn/network/api/gunicorn_app.py b/fedn/network/api/gunicorn_app.py index 8c5cb0c30..bd93ddeca 100644 --- a/fedn/network/api/gunicorn_app.py +++ b/fedn/network/api/gunicorn_app.py @@ -1,4 +1,10 @@ from gunicorn.app.base import BaseApplication +from fedn.network.controller.control import Control + + +from fedn.network.controller.control import Control + + class GunicornApp(BaseApplication): def __init__(self, app, options=None): self.options = options or {} @@ -6,18 +12,31 @@ def __init__(self, app, options=None): super().__init__() def load_config(self): - config = {key: value for key, value in self.options.items() - if key in self.cfg.settings and value is not None} + config = {key: value for key, value in self.options.items() if key in self.cfg.settings and value is not None} for key, value in config.items(): self.cfg.set(key.lower(), value) def load(self): return self.application -def run_gunicorn(app, host,port,workers=4): + +def post_fork(server, worker): + """Hook to be called after the worker has forked. + + This is where we can initialize the database connection for each worker. + """ + # Initialize the database connection + Control.instance().db.initialize_connection() + + +def run_gunicorn(app, host, port, workers=4): bind_address = f"{host}:{port}" options = { "bind": bind_address, # Specify the bind address and port here "workers": workers, + # After forking, initialize the database connection + "post_fork": post_fork, + "timeout": 120, + "post_fork": post_fork, } GunicornApp(app, options).run() diff --git a/fedn/network/api/server.py b/fedn/network/api/server.py index 31a051327..1f2bb634f 100644 --- a/fedn/network/api/server.py +++ b/fedn/network/api/server.py @@ -492,11 +492,13 @@ def start_server_api(): statestore_config = get_statestore_config() # TODO: Initialize database with config instead of reading it under the hood - db = DatabaseConnection(statestore_config, network_id) + db = DatabaseConnection(statestore_config, network_id, connect=False) repository = Repository(modelstorage_config["storage_config"], storage_type=modelstorage_config["storage_type"]) Control.create_instance(network_id, repository, db) if debug: + # Without gunicorn, we can initialize the database connection here + db.initialize_connection() app.run(debug=debug, port=port, host=host) else: workers = os.cpu_count() diff --git a/fedn/network/api/tests.py b/fedn/network/api/tests.py index 84df49e70..94df557d1 100644 --- a/fedn/network/api/tests.py +++ b/fedn/network/api/tests.py @@ -1,47 +1,31 @@ # Unittest for Flask API endpoints # # Run with: -# python -m unittest fedn.tests.network.api.tests -# -# or -# -# python3 -m unittest fedn.tests.network.api.tests -# -# or -# -# python3 -m unittest fedn.tests.network.api.tests.NetworkAPITests -# -# or -# -# python -m unittest fedn.tests.network.api.tests.NetworkAPITests -# -# or -# -# python -m unittest fedn.tests.network.api.tests.NetworkAPITests.test_get_model_trail -# -# or -# -# python3 -m unittest fedn.tests.network.api.tests.NetworkAPITests.test_get_model_trail -# +# python -m unittest fedn.network.api.tests + import unittest from unittest.mock import patch, MagicMock from fedn.network.controller.control import Control +from fedn.network.storage.statestore.stores.dto.attribute import AttributeDTO from fedn.network.storage.statestore.stores.dto.metric import MetricDTO from fedn.network.storage.statestore.stores.dto.client import ClientDTO from fedn.network.storage.statestore.stores.dto.combiner import CombinerDTO from fedn.network.storage.statestore.stores.dto.model import ModelDTO from fedn.network.storage.statestore.stores.dto.package import PackageDTO +from fedn.network.storage.statestore.stores.dto.prediction import PredictionDTO from fedn.network.storage.statestore.stores.dto.round import RoundDTO +from fedn.network.storage.statestore.stores.dto.run import RunDTO from fedn.network.storage.statestore.stores.dto.session import SessionDTO from fedn.network.storage.statestore.stores.dto.status import StatusDTO +from fedn.network.storage.statestore.stores.dto.telemetry import TelemetryDTO from fedn.network.storage.statestore.stores.dto.validation import ValidationDTO from fedn.network.storage.statestore.stores.shared import SortOrder -entitites = ['clients', 'combiners', 'models', 'packages', 'rounds', 'sessions', 'statuses', 'validations', 'metrics'] -keys = ['client_id', 'combiner_id', 'model_id', 'package_id', 'round_id', 'session_id', 'status_id', 'validation_id', 'metric_id'] +entities = ['clients', 'combiners', 'models', 'packages', 'rounds', 'sessions', 'statuses', 'validations', 'metrics', 'runs', 'predictions', 'telemetry', 'attributes'] +keys = ['client_id', 'combiner_id', 'model_id', 'package_id', 'round_id', 'session_id', 'status_id', 'validation_id', 'metric_id', 'run_id', 'prediction_id', 'telemetry_id', 'attribute_id'] class MockStore: """Mock store implementation.""" @@ -74,8 +58,11 @@ def __init__(self): self.package_store = MockStore() self.model_store = MockStore() self.session_store = MockStore() - self.analytic_store = MockStore() self.metric_store = MockStore() + self.run_store = MockStore() + self.prediction_store = MockStore() + self.telemetry_store = MockStore() + self.attribute_store = MockStore() class NetworkAPITests(unittest.TestCase): """ Unittests for the Network API. """ @@ -86,7 +73,6 @@ def setUp(self, mock_control): self.app = fedn.network.api.server.app.test_client() self.db = MockDB() - Control.create_instance("test_network", None, self.db) @@ -110,8 +96,35 @@ def test_get_controller_status(self): # Assert response self.assertEqual(response.status_code, 200) + def test_get_single_endpoints(self): + """ Test get single endpoints. """ + expected_return_id = "test" + self.db.client_store.get = MagicMock(side_effect=lambda id: ClientDTO(client_id="test") if id == "test" else None) + self.db.combiner_store.get = MagicMock(side_effect=lambda id: CombinerDTO(combiner_id="test") if id == "test" else None) + self.db.model_store.get = MagicMock(side_effect=lambda id: ModelDTO(model_id="test") if id == "test" else None) + self.db.package_store.get = MagicMock(side_effect=lambda id: PackageDTO(package_id="test") if id == "test" else None) + self.db.round_store.get = MagicMock(side_effect=lambda id: RoundDTO(round_id="test") if id == "test" else None) + self.db.session_store.get = MagicMock(side_effect=lambda id: SessionDTO(session_id="test") if id == "test" else None) + self.db.status_store.get = MagicMock(side_effect=lambda id: StatusDTO(status_id="test") if id == "test" else None) + self.db.validation_store.get = MagicMock(side_effect=lambda id: ValidationDTO(validation_id="test") if id == "test" else None) + self.db.metric_store.get = MagicMock(side_effect=lambda id: MetricDTO(metric_id="test") if id == "test" else None) + self.db.run_store.get = MagicMock(side_effect=lambda id: RunDTO(run_id="test") if id == "test" else None) + self.db.prediction_store.get = MagicMock(side_effect=lambda id: PredictionDTO(prediction_id="test") if id == "test" else None) + self.db.telemetry_store.get = MagicMock(side_effect=lambda id: TelemetryDTO(telemetry_id="test") if id == "test" else None) + self.db.attribute_store.get = MagicMock(side_effect=lambda id: AttributeDTO(attribute_id="test") if id == "test" else None) + + for key,entity in zip(keys, entities): + response = self.app.get(f'/api/v1/{entity}/test') + # Assert response + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json[key], expected_return_id) + response = self.app.get(f'/api/v1/{entity}/test2') # does not exist + # Assert response + self.assertEqual(response.status_code, 404) + self.assertEqual(response.json['message'], "Entity with id: test2 not found") + def test_get_endpoints(self): - """ Test allt get endpoints. """ + """ Test all get endpoints. """ excepted_return_count = 1 expected_return_id = "test" self.db.client_store.list = MagicMock(return_value=[ClientDTO(client_id="test")]) @@ -132,9 +145,17 @@ def test_get_endpoints(self): self.db.validation_store.count = MagicMock(return_value=1) self.db.metric_store.list = MagicMock(return_value=[MetricDTO(metric_id="test")]) self.db.metric_store.count = MagicMock(return_value=1) + self.db.run_store.list = MagicMock(return_value=[RunDTO(run_id="test")]) + self.db.run_store.count = MagicMock(return_value=1) + self.db.prediction_store.list = MagicMock(return_value=[PredictionDTO(prediction_id="test")]) + self.db.prediction_store.count = MagicMock(return_value=1) + self.db.telemetry_store.list = MagicMock(return_value=[TelemetryDTO(telemetry_id="test")]) + self.db.telemetry_store.count = MagicMock(return_value=1) + self.db.attribute_store.list = MagicMock(return_value=[AttributeDTO(attribute_id="test")]) + self.db.attribute_store.count = MagicMock(return_value=1) - for key,entity in zip(keys, entitites): + for key,entity in zip(keys, entities): response = self.app.get(f'/api/v1/{entity}/') # Assert response self.assertEqual(response.status_code, 200) @@ -165,8 +186,16 @@ def test_get_endpoints(self): self.db.validation_store.list.assert_called_once() self.db.metric_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) self.db.metric_store.list.assert_called_once() + self.db.run_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.run_store.list.assert_called_once() + self.db.prediction_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.prediction_store.list.assert_called_once() + self.db.telemetry_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.telemetry_store.list.assert_called_once() + self.db.attribute_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.attribute_store.list.assert_called_once() - for entity in entitites: + for entity in entities: headers = { "X-Limit": 10, "X-Skip": 10, @@ -186,6 +215,222 @@ def test_get_endpoints(self): self.db.status_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) self.db.validation_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) self.db.metric_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.run_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.prediction_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.telemetry_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.attribute_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + + for entity in entities: + + response = self.app.get(f'/api/v1/{entity}/?property1=value1&property2=value2') + # Assert response + self.assertEqual(response.status_code, 200) + + self.db.client_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.combiner_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.model_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.package_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.round_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.session_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.status_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.validation_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.metric_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.run_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.prediction_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.telemetry_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.attribute_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + + def test_list_endpoints(self): + """ Test all list endpoints. """ + expected_return_count = 1 + expected_return_id = "test" + self.db.client_store.list = MagicMock(return_value=[ClientDTO(client_id="test")]) + self.db.client_store.count = MagicMock(return_value=1) + self.db.combiner_store.list = MagicMock(return_value=[CombinerDTO(combiner_id="test")]) + self.db.combiner_store.count = MagicMock(return_value=1) + self.db.model_store.list = MagicMock(return_value=[ModelDTO(model_id="test")]) + self.db.model_store.count = MagicMock(return_value=1) + self.db.package_store.list = MagicMock(return_value=[PackageDTO(package_id="test")]) + self.db.package_store.count = MagicMock(return_value=1) + self.db.round_store.list = MagicMock(return_value=[RoundDTO(round_id="test")]) + self.db.round_store.count = MagicMock(return_value=1) + self.db.session_store.list = MagicMock(return_value=[SessionDTO(session_id="test")]) + self.db.session_store.count = MagicMock(return_value=1) + self.db.status_store.list = MagicMock(return_value=[StatusDTO(status_id="test")]) + self.db.status_store.count = MagicMock(return_value=1) + self.db.validation_store.list = MagicMock(return_value=[ValidationDTO(validation_id="test")]) + self.db.validation_store.count = MagicMock(return_value=1) + self.db.metric_store.list = MagicMock(return_value=[MetricDTO(metric_id="test")]) + self.db.metric_store.count = MagicMock(return_value=1) + self.db.run_store.list = MagicMock(return_value=[RunDTO(run_id="test")]) + self.db.run_store.count = MagicMock(return_value=1) + self.db.prediction_store.list = MagicMock(return_value=[PredictionDTO(prediction_id="test")]) + self.db.prediction_store.count = MagicMock(return_value=1) + self.db.telemetry_store.list = MagicMock(return_value=[TelemetryDTO(telemetry_id="test")]) + self.db.telemetry_store.count = MagicMock(return_value=1) + self.db.attribute_store.list = MagicMock(return_value=[AttributeDTO(attribute_id="test")]) + self.db.attribute_store.count = MagicMock(return_value=1) + + for key,entity in zip(keys, entities): + response = self.app.post(f'/api/v1/{entity}/list') + # Assert response + self.assertEqual(response.status_code, 200) + + count = response.json['count'] + + self.assertEqual(count, expected_return_count) + + id = response.json['result'][0][key] + + self.assertEqual(id, expected_return_id) + + self.db.client_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.client_store.list.assert_called_once() + self.db.combiner_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.combiner_store.list.assert_called_once() + self.db.model_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.model_store.list.assert_called_once() + self.db.package_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.package_store.list.assert_called_once() + self.db.round_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.round_store.list.assert_called_once() + self.db.session_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.session_store.list.assert_called_once() + self.db.status_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.status_store.list.assert_called_once() + self.db.validation_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.validation_store.list.assert_called_once() + self.db.metric_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.metric_store.list.assert_called_once() + self.db.run_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.run_store.list.assert_called_once() + self.db.prediction_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.prediction_store.list.assert_called_once() + self.db.telemetry_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.telemetry_store.list.assert_called_once() + self.db.attribute_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING) + self.db.attribute_store.list.assert_called_once() + + for entity in entities: + headers = { + "X-Limit": 10, + "X-Skip": 10, + "X-Sort-Key": "test", + "X-Sort-Order": "asc" + } + response = self.app.post(f'/api/v1/{entity}/list', headers=headers) + # Assert response + self.assertEqual(response.status_code, 200) + self.db.client_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.combiner_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.model_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.package_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.round_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.session_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.status_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.validation_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.metric_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.run_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.prediction_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.telemetry_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + self.db.attribute_store.list.assert_called_with(10, 10, "test", SortOrder.ASCENDING) + + for entity in entities: + response = self.app.post(f'/api/v1/{entity}/list', json={"property1": "value1", "property2": "value2"}) + # Assert response + self.assertEqual(response.status_code, 200) + + self.db.client_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.combiner_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.model_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.package_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.round_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.session_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.status_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.validation_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.metric_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.run_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.prediction_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.telemetry_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + self.db.attribute_store.list.assert_called_with(0, 0, None, SortOrder.DESCENDING, property1="value1", property2="value2") + + def test_count_endpoints(self): + """ Test all count endpoints. """ + expected_return_count = 1 + self.db.client_store.count = MagicMock(return_value=1) + self.db.combiner_store.count = MagicMock(return_value=1) + self.db.model_store.count = MagicMock(return_value=1) + self.db.package_store.count = MagicMock(return_value=1) + self.db.round_store.count = MagicMock(return_value=1) + self.db.session_store.count = MagicMock(return_value=1) + self.db.status_store.count = MagicMock(return_value=1) + self.db.validation_store.count = MagicMock(return_value=1) + self.db.metric_store.count = MagicMock(return_value=1) + self.db.run_store.count = MagicMock(return_value=1) + self.db.prediction_store.count = MagicMock(return_value=1) + self.db.telemetry_store.count = MagicMock(return_value=1) + self.db.attribute_store.count = MagicMock(return_value=1) + + for entity in entities: + response = self.app.get(f'/api/v1/{entity}/count') + # Assert response + self.assertEqual(response.status_code, 200) + + count = response.json + + self.assertEqual(count, expected_return_count) + + self.db.client_store.count.assert_called_with() + self.db.combiner_store.count.assert_called_with() + self.db.model_store.count.assert_called_with() + self.db.package_store.count.assert_called_with() + self.db.round_store.count.assert_called_with() + self.db.session_store.count.assert_called_with() + self.db.status_store.count.assert_called_with() + self.db.validation_store.count.assert_called_with() + self.db.metric_store.count.assert_called_with() + self.db.run_store.count.assert_called_with() + self.db.prediction_store.count.assert_called_with() + self.db.telemetry_store.count.assert_called_with() + self.db.attribute_store.count.assert_called_with() + + for entity in entities: + response = self.app.get(f'/api/v1/{entity}/count?property1=value1&property2=value2') + # Assert response + self.assertEqual(response.status_code, 200) + + self.db.client_store.count.assert_called_with(property1="value1", property2="value2") + self.db.combiner_store.count.assert_called_with(property1="value1", property2="value2") + self.db.model_store.count.assert_called_with(property1="value1", property2="value2") + self.db.package_store.count.assert_called_with(property1="value1", property2="value2") + self.db.round_store.count.assert_called_with(property1="value1", property2="value2") + self.db.session_store.count.assert_called_with(property1="value1", property2="value2") + self.db.status_store.count.assert_called_with(property1="value1", property2="value2") + self.db.validation_store.count.assert_called_with(property1="value1", property2="value2") + self.db.metric_store.count.assert_called_with(property1="value1", property2="value2") + self.db.run_store.count.assert_called_with(property1="value1", property2="value2") + self.db.prediction_store.count.assert_called_with(property1="value1", property2="value2") + self.db.telemetry_store.count.assert_called_with(property1="value1", property2="value2") + self.db.attribute_store.count.assert_called_with(property1="value1", property2="value2") + + for entity in entities: + response = self.app.post(f'/api/v1/{entity}/count', json={"property1": "value1", "property2": "value2"}) + # Assert response + self.assertEqual(response.status_code, 200) + + self.db.client_store.count.assert_called_with(property1="value1", property2="value2") + self.db.combiner_store.count.assert_called_with(property1="value1", property2="value2") + self.db.model_store.count.assert_called_with(property1="value1", property2="value2") + self.db.package_store.count.assert_called_with(property1="value1", property2="value2") + self.db.round_store.count.assert_called_with(property1="value1", property2="value2") + self.db.session_store.count.assert_called_with(property1="value1", property2="value2") + self.db.status_store.count.assert_called_with(property1="value1", property2="value2") + self.db.validation_store.count.assert_called_with(property1="value1", property2="value2") + self.db.metric_store.count.assert_called_with(property1="value1", property2="value2") + self.db.run_store.count.assert_called_with(property1="value1", property2="value2") + self.db.prediction_store.count.assert_called_with(property1="value1", property2="value2") + self.db.telemetry_store.count.assert_called_with(property1="value1", property2="value2") + self.db.attribute_store.count.assert_called_with(property1="value1", property2="value2") if __name__ == '__main__': diff --git a/fedn/network/api/v1/README.md b/fedn/network/api/v1/README.md new file mode 100644 index 000000000..d9cbf3ede --- /dev/null +++ b/fedn/network/api/v1/README.md @@ -0,0 +1,163 @@ +# API Documentation - `/v1` + +This directory contains the implementation of the version 1 (v1) API for the FEDn network. The API provides endpoints for managing and interacting with the network. + +## Overview + +The v1 API provides RESTful endpoints for retrieving, adding, and updating data records generated by the FEDn network. + +## Endpoints + +Below is a summary of the main endpoints: + +- **`Clients`**: Manage client entities. + - **`GET /clients`**: List all clients. + - **`POST /clients/list`**: List all clients. Use this when filtering for multiple values of the same field. + - **`GET /clients/count`**: Count all clients. + - **`POST /clients/count`**: Count all clients. Use this when filtering for multiple values of the same field. + - **`GET /clients/{id}`**: Retrieve details of a specific client. + - **`DELETE /clients/{id}`**: Remove a client from the network. + - **`POST /clients/add`**: Register a new client. + - **`PUT /clients/config`**: Get the configuration of a client. +- **`Combiners`**: Manage combiner entities. + - **`GET /combiners`**: List all combiners. + - **`POST /combiners/list`**: List all combiners. Use this when filtering for multiple values of the same field. + - **`GET /combiners/count`**: Count all combiners. + - **`POST /combiners/count`**: Count all combiners. Use this when filtering for multiple values of the same field. + - **`GET /combiners/{id}`**: Retrieve details of a specific combiner. + - **`DELETE /combiners/{id}`**: Remove a combiner from the network. + - **`POST /combiners/clients/count`**: Count all clients of a combiner. +- **`Helper`**: Set and get active helpers. + - **`GET /helpers/active`**: Get the active helper. + - **`PUT /helpers/active`**: Set the active helper. +- **`Metrics`**: Manage metrics entities. + - **`GET /metrics`**: List all metrics. + - **`POST /metrics/list`**: List all metrics. Use this when filtering for multiple values of the same field. + - **`GET /metrics/count`**: Count all metrics. + - **`POST /metrics/count`**: Count all metrics. Use this when filtering for multiple values of the same field. + - **`GET /metrics/{id}`**: Retrieve details of a specific metric. +- **`Models`**: Manage model entities. + - **`GET /model`**: List all models. + - **`POST /model/list`**: List all models. Use this when filtering for multiple values of the same field. + - **`GET /model/count`**: Count all models. + - **`POST /model/count`**: Count all models. Use this when filtering for multiple values of the same field. + - **`GET /model/{id}`**: Retrieve details of a specific model. + - **`PATCH /model/{id}`**: Update model information. Will only update the fields provided in the request body. + - **`PUT /model/{id}`**: Update model information. + - **`GET /model/{id}/descendants`**: Get all descendants of a model. + - **`GET /model/{id}/ancestors`**: Get all ancestors of a model. + - **`GET /model/{id}/parameters`**: Get the parameters of a model. + - **`GET /model/leaf-nodes`**: Get all leaf nodes of a model. Meaning all models that are not parents of any other model. + - **`GET /model/download`**: Download a model. + - **`POST /model`**: Upload a model. +- **`Packages`**: Manage packages (compute package) entities. + - **`GET /packages`**: List all packages. + - **`POST /packages/list`**: List all packages. Use this when filtering for multiple values of the same field. + - **`GET /packages/count`**: Count all packages. + - **`POST /packages/count`**: Count all packages. Use this when filtering for multiple values of the same field. + - **`GET /packages/{id}`**: Retrieve details of a specific package. + - **`POST /packages`**: Upload a new package. + - **`GET /packages/download`**: Download the active package. + - **`GET /packages/checksum`**: Get the checksum of the active package. + - **`GET /packages/active`**: Get the active package. + - **`PUT /packages/active`**: Set the active package. + - **`DELETE /packages/active`**: Delete the active package. +- **`Predictions`**: Manage predictions entities. + - **`GET /predictions`**: List all predictions. + - **`POST /predictions/list`**: List all predictions. Use this when filtering for multiple values of the same field. + - **`GET /predictions/count`**: Count all predictions. + - **`POST /predictions/count`**: Count all predictions. Use this when filtering for multiple values of the same field. + - **`GET /predictions/{id}`**: Retrieve details of a specific prediction. +- **`Rounds`**: Manage (session training) round entities. + - **`GET /rounds`**: List all rounds. + - **`POST /rounds/list`**: List all rounds. Use this when filtering for multiple values of the same field. + - **`GET /rounds/count`**: Count all rounds. + - **`POST /rounds/count`**: Count all rounds. Use this when filtering for multiple values of the same field. + - **`GET /rounds/{id}`**: Retrieve details of a specific round. +- **`Runs`**: Manage (session training) run entities. (Stored whenever training is started) + - **`GET /runs`**: List all runs. + - **`POST /runs/list`**: List all runs. Use this when filtering for multiple values of the same field. + - **`GET /runs/count`**: Count all runs. + - **`POST /runs/count`**: Count all runs. Use this when filtering for multiple values of the same field. + - **`GET /runs/{id}`**: Retrieve details of a specific run. +- **`Sessions`**: Manage session entities. + - **`GET /sessions`**: List all sessions. + - **`POST /sessions/list`**: List all sessions. Use this when filtering for multiple values of the same field. + - **`GET /sessions/count`**: Count all sessions. + - **`POST /sessions/count`**: Count all sessions. Use this when filtering for multiple values of the same field. + - **`GET /sessions/{id}`**: Retrieve details of a specific session. + - **`POST /sessions`**: Create a new session. + - **`POST /sessions/start`**: Start a new training round for a session. + - **`PATCH /sessions/{id}`**: Update session information. Will only update the fields provided in the request body. + - **`PUT /sessions/{id}`**: Update session information. +- **`Statuses`**: Manage status entities. (Events that are stored in the database) + - **`GET /statuses`**: List all statuses. + - **`POST /statuses/list`**: List all statuses. Use this when filtering for multiple values of the same field. + - **`GET /statuses/count`**: Count all statuses. + - **`POST /statuses/count`**: Count all statuses. Use this when filtering for multiple values of the same field. + - **`GET /statuses/{id}`**: Retrieve details of a specific status. +- **`Telemetry`**: Manage telemetry entities. + - **`GET /telemetry`**: List all telemetry data. + - **`POST /telemetry/list`**: List all telemetry data. Use this when filtering for multiple values of the same field. + - **`GET /telemetry/count`**: Count all telemetry data. + - **`POST /telemetry/count`**: Count all telemetry data. Use this when filtering for multiple values of the same field. + - **`GET /telemetry/{id}`**: Retrieve details of a specific telemetry record. + - **`POST /telemetry`**: Add new telemetry data. +- **`Validations`**: Manage validation entities. + - **`GET /validations`**: List all validations. + - **`POST /validations/list`**: List all validations. Use this when filtering for multiple values of the same field. + - **`GET /validations/count`**: Count all validations. + - **`POST /validations/count`**: Count all validations. Use this when filtering for multiple values of the same field. + - **`GET /validations/{id}`**: Retrieve details of a specific validation. + + +## Filtering +All get (many) endpoints support filtering. You can use query parameters to filter results based on specific fields. For example: + +``` +GET /clients?name=John +``` +This will return all clients with the name "John". +You can also use multiple filters: +``` +GET /clients?name=John&status=active +``` +To filter by multiple values of the same field, use the `list` endpoint: +``` +POST /clients/list +{ + "name": ["John", "Doe"] +} +``` +To filter for missing values, use `null` as value: +``` +GET /clients?name=null +``` +For value types like numbers and dates, you can use comparison operators: +``` +GET /clients?commit_time__gt=2023-01-01 +``` +Full list of comparison operators: +- `__gt`: Greater than +- `__lt`: Less than +- `__gte`: Greater than or equal to +- `__lte`: Less than or equal to +- `__ne`: Not equal +- `__eq`: Equal + +## Authentication + +The API requires authentication via API keys. Include the API key in the `Authorization` header of your requests: + +``` +Authorization: Bearer +``` + +## Error Handling + +The API returns standard HTTP status codes to indicate success or failure: +- `200 OK`: Request succeeded. +- `400 Bad Request`: Invalid input. +- `401 Unauthorized`: Missing or invalid API key. +- `404 Not Found`: Resource not found. +- `500 Internal Server Error`: Server encountered an error. \ No newline at end of file diff --git a/fedn/network/api/v1/__init__.py b/fedn/network/api/v1/__init__.py index 6bdcaf1de..dc4613df8 100644 --- a/fedn/network/api/v1/__init__.py +++ b/fedn/network/api/v1/__init__.py @@ -1,4 +1,4 @@ -from fedn.network.api.v1.analytic_routes import bp as analytic_bp +from fedn.network.api.v1.attribute_routes import bp as attribute_bp from fedn.network.api.v1.client_routes import bp as client_bp from fedn.network.api.v1.combiner_routes import bp as combiner_bp from fedn.network.api.v1.helper_routes import bp as helper_bp @@ -7,9 +7,10 @@ from fedn.network.api.v1.package_routes import bp as package_bp from fedn.network.api.v1.prediction_routes import bp as prediction_bp from fedn.network.api.v1.round_routes import bp as round_bp -from fedn.network.api.v1.run_routes import bp as training_run_bp +from fedn.network.api.v1.run_routes import bp as run_bp from fedn.network.api.v1.session_routes import bp as session_bp from fedn.network.api.v1.status_routes import bp as status_bp +from fedn.network.api.v1.telemetry_routes import bp as telemetry_bp from fedn.network.api.v1.validation_routes import bp as validation_bp _routes = [ @@ -23,7 +24,8 @@ validation_bp, prediction_bp, helper_bp, - analytic_bp, metric_bp, - training_run_bp, + run_bp, + telemetry_bp, + attribute_bp, ] diff --git a/fedn/network/api/v1/analytic_routes.py b/fedn/network/api/v1/analytic_routes.py deleted file mode 100644 index 6feb56d4c..000000000 --- a/fedn/network/api/v1/analytic_routes.py +++ /dev/null @@ -1,55 +0,0 @@ -from flask import Blueprint, jsonify, request - -from fedn.common.log_config import logger -from fedn.network.api.auth import jwt_auth_required -from fedn.network.api.v1.shared import api_version, get_typed_list_headers -from fedn.network.controller.control import Control -from fedn.network.storage.statestore.stores.dto.analytic import AnalyticDTO -from fedn.network.storage.statestore.stores.shared import MissingFieldError, ValidationError - -bp = Blueprint("analytic", __name__, url_prefix=f"/api/{api_version}/analytics") - - -@bp.route("/", methods=["GET"]) -@jwt_auth_required(role="admin") -def get_analytics(): - try: - db = Control.instance().db - limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) - kwargs = request.args.to_dict() - - analytics = db.analytic_store.list(limit, skip, sort_key, sort_order, **kwargs) - count = db.analytic_store.count(**kwargs) - - response = {"count": count, "result": [analytic.to_dict() for analytic in analytics]} - return jsonify(response), 200 - except Exception as e: - logger.error(f"An unexpected error occurred: {e}") - return jsonify({"message": "An unexpected error occurred"}), 500 - - -@bp.route("/", methods=["POST"]) -@jwt_auth_required(role="client") -def add_analytics(): - try: - db = Control.instance().db - data = request.json if request.headers["Content-Type"] == "application/json" else request.form.to_dict() - - analytic = AnalyticDTO().patch_with(data) - result = db.analytic_store.add(analytic) - response = result.to_dict() - status_code: int = 201 - - return jsonify(response), status_code - except ValidationError as e: - logger.error(f"Validation error: {e}") - return jsonify({"message": e.user_message()}), 400 - except MissingFieldError as e: - logger.error(f"Missing field error: {e}") - return jsonify({"message": e.user_message()}), 400 - except ValueError as e: - logger.error(f"ValueError occured: {e}") - return jsonify({"message": "Invalid object"}), 400 - except Exception as e: - logger.error(f"An unexpected error occurred: {e}") - return jsonify({"message": "An unexpected error occurred"}), 500 diff --git a/fedn/network/api/v1/attribute_routes.py b/fedn/network/api/v1/attribute_routes.py new file mode 100644 index 000000000..f77095b53 --- /dev/null +++ b/fedn/network/api/v1/attribute_routes.py @@ -0,0 +1,182 @@ +from flask import Blueprint, jsonify, request + +from fedn.common.log_config import logger +from fedn.network.api.auth import jwt_auth_required +from fedn.network.api.v1.shared import api_version, get_post_data_to_kwargs, get_typed_list_headers +from fedn.network.controller.control import Control +from fedn.network.storage.statestore.stores.dto.attribute import AttributeDTO +from fedn.network.storage.statestore.stores.shared import MissingFieldError, ValidationError + +bp = Blueprint("attribute", __name__, url_prefix=f"/api/{api_version}/attributes") + + +@bp.route("/", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_attributes(): + try: + db = Control.instance().db + limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) + kwargs = request.args.to_dict() + + attributes = db.attribute_store.list(limit, skip, sort_key, sort_order, **kwargs) + count = db.attribute_store.count(**kwargs) + + response = {"count": count, "result": [attribute.to_dict() for attribute in attributes]} + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/list", methods=["POST"]) +@jwt_auth_required(role="admin") +def list_attributes(): + try: + db = Control.instance().db + limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) + kwargs = get_post_data_to_kwargs(request) + + attributes = db.attribute_store.list(limit, skip, sort_key, sort_order, **kwargs) + count = db.attribute_store.count(**kwargs) + + response = {"count": count, "result": [attribute.to_dict() for attribute in attributes]} + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_attributes_count(): + try: + db = Control.instance().db + kwargs = request.args.to_dict() + count = db.attribute_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["POST"]) +@jwt_auth_required(role="admin") +def attributes_count(): + try: + db = Control.instance().db + kwargs = request.json if request.headers["Content-Type"] == "application/json" else request.form.to_dict() + count = db.attribute_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_attribute(id: str): + try: + db = Control.instance().db + attribute = db.attribute_store.get(id) + if attribute is None: + return jsonify({"message": f"Entity with id: {id} not found"}), 404 + + response = attribute.to_dict() + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/", methods=["POST"]) +@jwt_auth_required(role="admin") +def add_attributes(): + try: + db = Control.instance().db + data = request.json if request.headers["Content-Type"] == "application/json" else request.form.to_dict() + + attribute = AttributeDTO().patch_with(data) + result = db.attribute_store.add(attribute) + response = result.to_dict() + status_code: int = 201 + + return jsonify(response), status_code + except ValidationError as e: + logger.error(f"Validation error: {e}") + return jsonify({"message": e.user_message()}), 400 + except MissingFieldError as e: + logger.error(f"Missing field error: {e}") + return jsonify({"message": e.user_message()}), 400 + except ValueError as e: + logger.error(f"ValueError occured: {e}") + return jsonify({"message": "Invalid object"}), 400 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/current", methods=["POST"]) +@jwt_auth_required(role="admin") +def get_client_current_attributes(): + """Get current attributes for clients + --- + tags: + - Clients + parameters: + - name: client_ids + in: body + required: true + type: array + items: + type: string + description: List of client IDs to retrieve attributes for + responses: + 200: + description: A dict of clients and their attributes + schema: + type: object + properties: + client_id: + type: object + additionalProperties: + type: string + 400: + description: Missing required field + schema: + type: object + properties: + message: + type: string + 500: + description: An error occurred + schema: + type: object + properties: + message: + type: string + """ + try: + db = Control.instance().db + json_data = request.get_json() + client_ids = json_data.get("client_ids") + if not client_ids: + return jsonify({"message": "Missing required field: client_ids"}), 400 + + response = {} + for client_id in client_ids: + client = db.client_store.get(client_id) + if client is None: + response[client_id] = f"Entity with client_id: {client_id} not found" + continue + attributes = db.attribute_store.get_current_attributes_for_client(client.client_id) + response[client.client_id] = {} + for attribute in attributes: + response[client.client_id][attribute.key] = attribute.value + + return jsonify(response), 200 + + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 diff --git a/fedn/network/api/v1/client_routes.py b/fedn/network/api/v1/client_routes.py index 3e20f417e..8dcc149f7 100644 --- a/fedn/network/api/v1/client_routes.py +++ b/fedn/network/api/v1/client_routes.py @@ -599,3 +599,61 @@ def get_client_config(): except Exception as e: logger.error(f"An unexpected error occurred: {e}") return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("//attributes", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_client_attributes(id): + """Get client attributes + Retrieves the attributes of a client based on the provided id. + --- + tags: + - Clients + parameters: + - name: id + in: path + required: true + type: string + description: The id of the client + responses: + 200: + description: A list of attributes for the client + schema: + type: array + items: + type: object + properties: + key: + type: string + value: + type: string + 404: + description: The client was not found + schema: + type: object + properties: + message: + type: string + 500: + description: An error occurred + schema: + type: object + properties: + message: + type: string + """ + try: + db = Control.instance().db + + client = db.client_store.get(id) + if client is None: + return jsonify({"message": f"Entity with id: {id} not found"}), 404 + + attributes = db.attribute_store.get_current_attributes_for_client(client.client_id) + response = {} + for attribute in attributes: + response[attribute.key] = attribute.value + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 diff --git a/fedn/network/api/v1/metric_routes.py b/fedn/network/api/v1/metric_routes.py index bc229ab47..5854f41cc 100644 --- a/fedn/network/api/v1/metric_routes.py +++ b/fedn/network/api/v1/metric_routes.py @@ -232,3 +232,225 @@ def list_metrics(): except Exception as e: logger.error(f"An unexpected error occurred: {e}") return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_metric(id: str): + """Get metric + Retrieves a metric based on the provided id. + --- + tags: + - Metrics + parameters: + - name: id + in: path + required: true + type: string + description: The id of the metric + responses: + 200: + description: The metric + schema: + $ref: '#/definitions/Metric' + 404: + description: The metric was not found + schema: + type: object + properties: + error: + type: string + 500: + description: An error occurred + schema: + type: object + properties: + message: + type: string + """ + try: + db = Control.instance().db + response = db.metric_store.get(id) + if response is None: + return jsonify({"message": f"Entity with id: {id} not found"}), 404 + return jsonify(response.to_dict()), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_metrics_count(): + """Metrics count + Retrieves the count of metrics based on the provided parameters. + By specifying a parameter in the url, you can filter the metrics based on that parameter, + and the response will contain only the count of metrics that match the filter. + --- + tags: + - Metrics + parameters: + - name: sender.name + in: query + required: false + type: string + description: Name of the sender + - name: sender.role + in: query + required: false + type: string + description: Role of the sender + - name: model_id + in: query + required: false + type: string + description: Model ID associated with the metric + - name: model_step + in: query + required: false + type: integer + description: Model step associated with the metric + - name: round_id + in: query + required: false + type: string + description: Round ID associated with the metric + - name: session_id + in: query + required: false + type: string + description: Session ID associated with the metric + - name: X-Limit + in: header + required: false + type: integer + description: The maximum number of metrics to retrieve + - name: X-Skip + in: header + required: false + type: integer + description: The number of metrics to skip + - name: X-Sort-Key + in: header + required: false + type: string + description: The key to sort the metrics by + - name: X-Sort-Order + in: header + required: false + type: string + description: The order to sort the metrics in ('asc' or 'desc') + responses: + 200: + description: The count of metrics + schema: + type: integer + 500: + description: An error occurred + schema: + type: object + properties: + message: + type: string + """ + try: + db = Control.instance().db + kwargs = request.args.to_dict() + count = db.metric_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["POST"]) +@jwt_auth_required(role="admin") +def metrics_count(): + """Metrics count + Retrieves the count of metrics based on the provided parameters. + Works much like the GET /metrics/count endpoint, but allows for more complex queries. + By specifying a parameter in the request body, you can filter the metrics based on that parameter, + if the parameter value contains a comma, the filter will be an "in" query, meaning that the metrics + will be returned if the specified field contains any of the values in the parameter. + --- + tags: + - Metrics + parameters: + - name: limit + in: header + required: false + type: integer + description: The maximum number of metrics to retrieve + - name: skip + in: header + required: false + type: integer + description: The number of metrics to skip + - name: sort_key + in: header + required: false + type: string + description: The key to sort the metrics by + - name: sort_order + in: header + required: false + type: string + description: The order to sort the metrics in ('asc' or 'desc') + - name: filters + in: body + required: false + schema: + type: object + additionalProperties: + type: string + description: Additional filters for querying metrics + definitions: + Metric: + type: object + properties: + metric_id: + type: string + key: + type: string + value: + type: number + timestamp: + type: string + format: date-time + sender: + type: object + properties: + name: + type: string + role: + type: string + model_id: + type: string + model_step: + type: integer + round_id: + type: string + session_id: + type: string + 200: + description: The count of metrics + schema: + type: integer + 500: + description: An error occurred + schema: + type: object + properties: + message: + type: string + """ + try: + db = Control.instance().db + kwargs = get_post_data_to_kwargs(request) + count = db.metric_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 diff --git a/fedn/network/api/v1/package_routes.py b/fedn/network/api/v1/package_routes.py index 20d3ce8f6..c1c22fa75 100644 --- a/fedn/network/api/v1/package_routes.py +++ b/fedn/network/api/v1/package_routes.py @@ -5,13 +5,15 @@ from werkzeug.security import safe_join from fedn.common.config import FEDN_COMPUTE_PACKAGE_DIR -from fedn.common.log_config import logger +from fedn.common.log_config import logger, tracer from fedn.network.api.auth import jwt_auth_required from fedn.network.api.shared import get_checksum as _get_checksum -from fedn.network.api.v1.shared import api_version, get_post_data_to_kwargs, get_typed_list_headers +from fedn.network.api.v1.shared import (api_version, get_post_data_to_kwargs, + get_typed_list_headers) from fedn.network.controller.control import Control from fedn.network.storage.statestore.stores.dto.package import PackageDTO -from fedn.network.storage.statestore.stores.shared import MissingFieldError, ValidationError +from fedn.network.storage.statestore.stores.shared import (MissingFieldError, + ValidationError) bp = Blueprint("package", __name__, url_prefix=f"/api/{api_version}/packages") @@ -585,7 +587,8 @@ def upload_package(): if not os.path.exists(FEDN_COMPUTE_PACKAGE_DIR): os.makedirs(FEDN_COMPUTE_PACKAGE_DIR, exist_ok=True) file.save(file_path) - repository.set_compute_package(storage_file_name, file_path) + with tracer.start_as_current_span("set-compute-package"): + repository.set_compute_package(storage_file_name, file_path) except Exception as e: logger.error(f"An unexpected error occurred: {e}") db.package_store.delete(package.package_id) diff --git a/fedn/network/api/v1/prediction_routes.py b/fedn/network/api/v1/prediction_routes.py index ca7171fa3..893aba786 100644 --- a/fedn/network/api/v1/prediction_routes.py +++ b/fedn/network/api/v1/prediction_routes.py @@ -23,7 +23,7 @@ def start_session(): db = Control.instance().db control = Control.instance() - data = request.json if request.headers["Content-Type"] == "application/json" else request.form.to_dict() + data = request.get_json(silent=True) if request.is_json else request.form.to_dict() prediction_id: str = data.get("prediction_id") if not prediction_id or prediction_id == "": @@ -279,3 +279,161 @@ def list_predictions(): except Exception as e: logger.error(f"An unexpected error occurred: {e}") return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_predictions_count(): + """Get the count of predictions + --- + tags: + - Predictions + parameters: + - name: prediction_id + in: path + required: true + type: string + description: The id of the prediction to retrieve + responses: + 200: + description: The count of predictions with the specified id. + schema: + type: object + properties: + count: + type: integer + 500: + description: An error occurred + schema: + type: object + properties: + message: + type: string + """ + try: + db = Control.instance().db + kwargs = request.args.to_dict() + count = db.prediction_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["POST"]) +@jwt_auth_required(role="admin") +def predictions_count(): + """Prediction count + Retrieves the count of predictions based on the provided parameters. + Much like the GET /predictions/count endpoint, but allows for more complex queries. + By specifying a parameter in the body, you can filter the predictions based on that parameter, + and the response will contain only the count of predictions that match the filter. If the parameter value contains a comma, + the filter will be an "in" query, meaning that the predictions will be returned if the specified field contains any of the values in the parameter. + --- + tags: + - Predictions + parameters: + - name: prediction + in: body + required: false + type: object + description: Object containing the prediction filter + schema: + type: object + properties: + sender.name: + type: string + description: Name of the sender + sender.role: + type: string + description: Role of the sender + receiver.name: + type: string + description: Name of the receiver + receiver.role: + type: string + description: Role of the receiver + prediction_id: + type: string + description: The id of the prediction + model_id: + type: string + description: The id of the model + correlation_id: + type: string + description: Correlation id of the prediction + responses: + 200: + description: The count of predictions matching the filter. + schema: + type: object + properties: + count: + type: integer + 500: + description: An error occurred + schema: + type: object + properties: + error: + type: string + """ + try: + db = Control.instance().db + kwargs = get_post_data_to_kwargs(request) + count = db.prediction_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_prediction(id: str): + """Get a prediction by id + --- + tags: + - Predictions + parameters: + - name: id + in: path + required: true + type: string + description: The id of the prediction to retrieve + responses: + 200: + description: The prediction with the specified id. + schema: + type: object + properties: + prediction: + $ref: '#/definitions/Prediction' + 404: + description: The prediction with the specified id was not found. + schema: + type: object + properties: + message: + type: string + 500: + description: An error occurred + schema: + type: object + properties: + message: + type: string + """ + try: + db = Control.instance().db + + prediction = db.prediction_store.get(id) + if prediction is None: + return jsonify({"message": f"Entity with id: {id} not found"}), 404 + + return jsonify(prediction.to_dict()), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 diff --git a/fedn/network/api/v1/run_routes.py b/fedn/network/api/v1/run_routes.py index fc0a6d128..f861d5709 100644 --- a/fedn/network/api/v1/run_routes.py +++ b/fedn/network/api/v1/run_routes.py @@ -2,7 +2,7 @@ from fedn.common.log_config import logger from fedn.network.api.auth import jwt_auth_required -from fedn.network.api.v1.shared import api_version, get_typed_list_headers +from fedn.network.api.v1.shared import api_version, get_post_data_to_kwargs, get_typed_list_headers from fedn.network.controller.control import Control bp = Blueprint("run", __name__, url_prefix=f"/api/{api_version}/runs") @@ -16,11 +16,71 @@ def get_runs(): limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) kwargs = request.args.to_dict() - analytics = db.training_run_store.list(limit, skip, sort_key, sort_order, **kwargs) - count = db.training_run_store.count(**kwargs) + runs = db.run_store.list(limit, skip, sort_key, sort_order, **kwargs) + count = db.run_store.count(**kwargs) - response = {"count": count, "result": [analytic.to_dict() for analytic in analytics]} + response = {"count": count, "result": [run.to_dict() for run in runs]} return jsonify(response), 200 except Exception as e: logger.error(f"An unexpected error occurred: {e}") return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/list", methods=["POST"]) +@jwt_auth_required(role="admin") +def list_runs(): + try: + db = Control.instance().db + limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) + kwargs = get_post_data_to_kwargs(request) + + result = db.run_store.list(limit, skip, sort_key, sort_order, **kwargs) + count = db.run_store.count(**kwargs) + response = {"count": count, "result": [run.to_dict() for run in result]} + + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_runs_count(): + try: + db = Control.instance().db + kwargs = request.args.to_dict() + count = db.run_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["POST"]) +@jwt_auth_required(role="admin") +def runs_count(): + try: + db = Control.instance().db + kwargs = get_post_data_to_kwargs(request) + count = db.run_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_run(id: str): + try: + db = Control.instance().db + response = db.run_store.get(id) + if response is None: + return jsonify({"message": f"Entity with id: {id} not found"}), 404 + return jsonify(response.to_dict()), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 diff --git a/fedn/network/api/v1/session_routes.py b/fedn/network/api/v1/session_routes.py index 27d941864..122f764b9 100644 --- a/fedn/network/api/v1/session_routes.py +++ b/fedn/network/api/v1/session_routes.py @@ -1,4 +1,6 @@ import threading +from typing import Optional + from flask import Blueprint, jsonify, request @@ -354,7 +356,7 @@ def post(): """ try: db = Control.instance().db - data = request.json if request.headers["Content-Type"] == "application/json" else request.form.to_dict() + data = request.get_json(silent=True) if request.is_json else request.form.to_dict() session_config = SessionConfigDTO() session_config.populate_with(data.pop("session_config")) @@ -380,23 +382,32 @@ def post(): logger.error(f"ValueError occurred: {e}") return jsonify({"message": "Invalid object"}), 400 except Exception as e: + logger.error("error when creating a session") logger.error(f"An unexpected error occurred: {e}") return jsonify({"message": "An unexpected error occurred"}), 500 -def _get_number_of_available_clients(): +def _get_number_of_available_clients(client_ids: Optional[list[str]] = None): control = Control.instance() + result = 0 + active_clients = None for combiner in control.network.get_combiners(): try: - nr_active_clients = len(combiner.list_active_clients()) - result = result + int(nr_active_clients) + active_clients = combiner.list_active_clients() + if active_clients is not None: + if client_ids is not None: + filtered = [item for item in active_clients if item.client_id in client_ids] + result += len(filtered) + else: + result += len(active_clients) except CombinerUnavailableError: return 0 return result + @bp.route("/start", methods=["POST"]) @jwt_auth_required(role="admin") def start_session(): @@ -409,7 +420,9 @@ def start_session(): try: db = Control.instance().db control = Control.instance() - data = request.json if request.headers["Content-Type"] == "application/json" else request.form.to_dict() + + data = request.get_json(silent=True) if request.is_json else request.form.to_dict() + session_id: str = data.get("session_id") rounds: int = data.get("rounds", "") round_timeout: int = data.get("round_timeout", None) @@ -449,6 +462,47 @@ def start_session(): return jsonify({"message": "An unexpected error occurred"}), 500 +@bp.route("/start_splitlearning_session", methods=["POST"]) +@jwt_auth_required(role="admin") +def start_splitlearning_session(): + """Starts a new split learning session.""" + try: + db = Control.instance().db + control = Control.instance() + data = request.json if request.headers["Content-Type"] == "application/json" else request.form.to_dict() + session_id: str = data.get("session_id") + rounds: int = data.get("rounds", "") + round_timeout: int = data.get("round_timeout", None) + model_name_prefix: str = data.get("model_name_prefix", None) + + if model_name_prefix is None or not isinstance(model_name_prefix, str) or len(model_name_prefix) == 0: + model_name_prefix = None + + if not session_id or session_id == "": + return jsonify({"message": "Session ID is required"}), 400 + + session = db.session_store.get(session_id) + session_config = session.session_config + min_clients = session_config.clients_required + + if control.state() == ReducerState.monitoring: + return jsonify({"message": "A session is already running!"}), 400 + + if not rounds or not isinstance(rounds, int): + rounds = session_config.rounds + nr_available_clients = _get_number_of_available_clients() + + if nr_available_clients < min_clients: + return jsonify({"message": f"Number of available clients is lower than the required minimum of {min_clients}"}), 400 + + threading.Thread(target=control.splitlearning_session, args=(session_id, rounds, round_timeout)).start() + + return jsonify({"message": "Splitlearning session started"}), 200 + except Exception as e: + logger.error(f"An unexpected error occurred in split learning session: {e}") + return jsonify({"message": "An unexpected error occurred when starting split learning session"}), 500 + + @bp.route("/", methods=["PATCH"]) @jwt_auth_required(role="admin") def patch_session(id: str): diff --git a/fedn/network/api/v1/status_routes.py b/fedn/network/api/v1/status_routes.py index 3f27350df..70846fcd4 100644 --- a/fedn/network/api/v1/status_routes.py +++ b/fedn/network/api/v1/status_routes.py @@ -7,6 +7,7 @@ bp = Blueprint("status", __name__, url_prefix=f"/api/{api_version}/statuses") +MAX_STATUSES = 200 # for Async Gunicorn worker timeout error @bp.route("/", methods=["GET"]) @jwt_auth_required(role="admin") @@ -123,6 +124,10 @@ def get_statuses(): limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) kwargs = request.args.to_dict() + # for async gunicorn worker timeout + if not limit or limit > MAX_STATUSES: + limit = MAX_STATUSES + result = db.status_store.list(limit, skip, sort_key, sort_order, **kwargs) count = db.status_store.count(**kwargs) response = {"count": count, "result": [item.to_dict() for item in result]} @@ -218,6 +223,10 @@ def list_statuses(): limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) kwargs = get_post_data_to_kwargs(request) + # for async gunicorn worker timeout + if not limit or limit > MAX_STATUSES: + limit = MAX_STATUSES + result = db.status_store.list(limit, skip, sort_key, sort_order, **kwargs) count = db.status_store.count(**kwargs) response = {"count": count, "result": [item.to_dict() for item in result]} diff --git a/fedn/network/api/v1/telemetry_routes.py b/fedn/network/api/v1/telemetry_routes.py new file mode 100644 index 000000000..2a2993ba7 --- /dev/null +++ b/fedn/network/api/v1/telemetry_routes.py @@ -0,0 +1,119 @@ +from flask import Blueprint, jsonify, request + +from fedn.common.log_config import logger +from fedn.network.api.auth import jwt_auth_required +from fedn.network.api.v1.shared import api_version, get_post_data_to_kwargs, get_typed_list_headers +from fedn.network.controller.control import Control +from fedn.network.storage.statestore.stores.dto.telemetry import TelemetryDTO +from fedn.network.storage.statestore.stores.shared import MissingFieldError, ValidationError + +bp = Blueprint("telemetry", __name__, url_prefix=f"/api/{api_version}/telemetry") + + +@bp.route("/", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_telemetries(): + try: + db = Control.instance().db + limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) + kwargs = request.args.to_dict() + + telemetries = db.telemetry_store.list(limit, skip, sort_key, sort_order, **kwargs) + count = db.telemetry_store.count(**kwargs) + + response = {"count": count, "result": [telemetry.to_dict() for telemetry in telemetries]} + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/list", methods=["POST"]) +@jwt_auth_required(role="admin") +def list_telemetries(): + try: + db = Control.instance().db + limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) + kwargs = get_post_data_to_kwargs(request) + + telemetries = db.telemetry_store.list(limit, skip, sort_key, sort_order, **kwargs) + count = db.telemetry_store.count(**kwargs) + + response = {"count": count, "result": [telemetry.to_dict() for telemetry in telemetries]} + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_telemetries_count(): + try: + db = Control.instance().db + kwargs = request.args.to_dict() + count = db.telemetry_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/count", methods=["POST"]) +@jwt_auth_required(role="admin") +def telemetries_count(): + try: + db = Control.instance().db + + kwargs = request.get_json(silent=True) if request.is_json else request.form.to_dict() + + count = db.telemetry_store.count(**kwargs) + response = count + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/", methods=["GET"]) +@jwt_auth_required(role="admin") +def get_telemetry(id: str): + try: + db = Control.instance().db + telemetry = db.telemetry_store.get(id) + if telemetry is None: + return jsonify({"message": f"Entity with id: {id} not found"}), 404 + + response = telemetry.to_dict() + return jsonify(response), 200 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 + + +@bp.route("/", methods=["POST"]) +@jwt_auth_required(role="admin") +def add_telemetries(): + try: + db = Control.instance().db + data = request.get_json(silent=True) if request.is_json else request.form.to_dict() + + telemetry = TelemetryDTO().patch_with(data) + result = db.telemetry_store.add(telemetry) + response = result.to_dict() + status_code: int = 201 + + return jsonify(response), status_code + except ValidationError as e: + logger.error(f"Validation error: {e}") + return jsonify({"message": e.user_message()}), 400 + except MissingFieldError as e: + logger.error(f"Missing field error: {e}") + return jsonify({"message": e.user_message()}), 400 + except ValueError as e: + logger.error(f"ValueError occured: {e}") + return jsonify({"message": "Invalid object"}), 400 + except Exception as e: + logger.error(f"An unexpected error occurred: {e}") + return jsonify({"message": "An unexpected error occurred"}), 500 diff --git a/fedn/network/api/v1/training_run_routes.py b/fedn/network/api/v1/training_run_routes.py deleted file mode 100644 index 7141ac701..000000000 --- a/fedn/network/api/v1/training_run_routes.py +++ /dev/null @@ -1,26 +0,0 @@ -from flask import Blueprint, jsonify, request - -from fedn.common.log_config import logger -from fedn.network.api.auth import jwt_auth_required -from fedn.network.api.v1.shared import api_version, get_typed_list_headers -from fedn.network.controller.control import Control - -bp = Blueprint("training_run", __name__, url_prefix=f"/api/{api_version}/training-runs") - - -@bp.route("/", methods=["GET"]) -@jwt_auth_required(role="admin") -def get_training_runs(): - try: - db = Control.instance().db - limit, skip, sort_key, sort_order = get_typed_list_headers(request.headers) - kwargs = request.args.to_dict() - - training_runs = db.training_run_store.list(limit, skip, sort_key, sort_order, **kwargs) - count = db.training_run_store.count(**kwargs) - - response = {"count": count, "result": [training_run.to_dict() for training_run in training_runs]} - return jsonify(response), 200 - except Exception as e: - logger.error(f"An unexpected error occurred: {e}") - return jsonify({"message": "An unexpected error occurred"}), 500 diff --git a/fedn/network/clients/client_v2.py b/fedn/network/clients/client_v2.py index a644310b1..8deb0f62b 100644 --- a/fedn/network/clients/client_v2.py +++ b/fedn/network/clients/client_v2.py @@ -119,6 +119,8 @@ def start(self) -> None: self.fedn_client.set_train_callback(self.on_train) self.fedn_client.set_validate_callback(self.on_validation) + self.fedn_client.set_forward_callback(self.on_forward) + self.fedn_client.set_backward_callback(self.on_backward) self.fedn_client.set_predict_callback(self._process_prediction_request) self.fedn_client.set_name(self.client_obj.name) @@ -141,6 +143,14 @@ def on_validation(self, in_model: BytesIO) -> Optional[dict]: """Handle the validation callback.""" return self._process_validation_request(in_model) + def on_forward(self, client_id, is_sl_inference): + out_embeddings, meta = self._process_forward_request(client_id, is_sl_inference) + return out_embeddings, meta + + def on_backward(self, in_gradients, client_id): + meta = self._process_backward_request(in_gradients, client_id) + return meta + def _process_training_request(self, in_model: BytesIO, client_settings: dict) -> Tuple[Optional[BytesIO], dict]: """Process a training (model update) request.""" try: @@ -222,3 +232,73 @@ def _process_prediction_request(self, in_model: BytesIO) -> Optional[dict]: metrics = None return metrics + + def _process_forward_request(self, client_id, is_sl_inference) -> Tuple[BytesIO, dict]: + """Process a forward request. Param is_sl_inference determines whether the forward pass is used for gradient calculation or validation. + + :param client_id: The client ID. + :type client_id: str + :param is_sl_inference: Whether the request is for splitlearning inference or not. + :type is_sl_inference: str + :return: The embeddings, or None if forward failed. + :rtype: tuple + """ + try: + out_embedding_path = get_tmp_path() + + tic = time.time() + self.fedn_client.dispatcher.run_cmd(f"forward {client_id} {out_embedding_path} {is_sl_inference}") + + meta = {} + embeddings = None + + with open(out_embedding_path, "rb") as fr: + embeddings = io.BytesIO(fr.read()) + + meta["exec_training"] = time.time() - tic + + # Read the metadata file + with open(out_embedding_path + "-metadata", "r") as fh: + training_metadata = json.loads(fh.read()) + + logger.debug("SETTING Forward metadata: {}".format(training_metadata)) + meta["training_metadata"] = training_metadata + + os.unlink(out_embedding_path) + os.unlink(out_embedding_path + "-metadata") + + except Exception as e: + logger.warning("Forward failed with exception {}".format(e)) + embeddings = None + meta = {"status": "failed", "error": str(e)} + + return embeddings, meta + + def _process_backward_request(self, in_gradients: BytesIO, client_id: str) -> dict: + """Process a backward request. + + :param in_gradients: The gradients to be processed. + :type in_gradients: BytesIO + :return: Metadata, or None if backward failed. + :rtype: dict + """ + try: + meta = {} + inpath = get_tmp_path() + + # load gradients + with open(inpath, "wb") as fh: + fh.write(in_gradients.getbuffer()) + + tic = time.time() + + self.fedn_client.dispatcher.run_cmd(f"backward {inpath} {client_id}") + meta["exec_training"] = time.time() - tic + + os.unlink(inpath) + + except Exception as e: + logger.error("Backward failed with exception {}".format(e)) + meta = {"status": "failed", "error": str(e)} + + return meta diff --git a/fedn/network/clients/connect.py b/fedn/network/clients/connect.py index d6b747b9a..ddf156676 100644 --- a/fedn/network/clients/connect.py +++ b/fedn/network/clients/connect.py @@ -25,7 +25,7 @@ HTTP_STATUS_UNAUTHORIZED = 401 # Default timeout for requests -REQUEST_TIMEOUT = 10 # seconds +REQUEST_TIMEOUT = 60 # seconds class Status(enum.Enum): diff --git a/fedn/network/clients/fedn_client.py b/fedn/network/clients/fedn_client.py index 3fb38353a..b28707c5d 100644 --- a/fedn/network/clients/fedn_client.py +++ b/fedn/network/clients/fedn_client.py @@ -10,12 +10,13 @@ from io import BytesIO from typing import Any, Optional, Tuple, Union +import psutil import requests import fedn.network.grpc.fedn_pb2 as fedn from fedn.common.config import FEDN_AUTH_SCHEME, FEDN_CONNECT_API_SECURE, FEDN_PACKAGE_EXTRACT_DIR from fedn.common.log_config import logger -from fedn.network.clients.grpc_handler import GrpcHandler +from fedn.network.clients.grpc_handler import GrpcHandler, RetryException from fedn.network.clients.package_runtime import PackageRuntime from fedn.utils.dispatcher import Dispatcher @@ -28,7 +29,7 @@ HTTP_STATUS_PACKAGE_MISSING = 203 # Default timeout for requests -REQUEST_TIMEOUT = 10 # seconds +REQUEST_TIMEOUT = 60 # seconds class GrpcConnectionOptions: @@ -137,6 +138,12 @@ def set_predict_callback(self, callback: callable) -> None: """Set the predict callback.""" self.predict_callback = callback + def set_forward_callback(self, callback: callable): + self.forward_callback = callback + + def set_backward_callback(self, callback: callable): + self.backward_callback = callback + def connect_to_api(self, url: str, token: str, json: dict) -> Tuple[ConnectToApiResult, Any]: """Connect to the FEDn API.""" url_endpoint = f"{url}api/v1/clients/add" @@ -251,7 +258,7 @@ def init_grpchandler(self, config: GrpcConnectionOptions, client_name: str, toke logger.error(f"Could not initialize GRPC connection: {e}") return False - def send_heartbeats(self, client_name: str, client_id: str, update_frequency: float = 2.0) -> None: + def send_heartbeats(self, client_name: str, client_id: str, update_frequency: float = 20.0) -> None: # Updated update frequency to 20 seconds """Send heartbeats to the server.""" self.grpc_handler.send_heartbeats(client_name=client_name, client_id=client_id, update_frequency=update_frequency) @@ -259,6 +266,21 @@ def listen_to_task_stream(self, client_name: str, client_id: str) -> None: """Listen to the task stream.""" self.grpc_handler.listen_to_task_stream(client_name=client_name, client_id=client_id, callback=self._task_stream_callback) + def default_telemetry_loop(self, update_frequency: float = 5.0) -> None: + """Send default telemetry data.""" + send_telemetry = True + while send_telemetry: + memory_usage = psutil.virtual_memory().percent + cpu_usage = psutil.cpu_percent() + try: + success = self.log_telemetry(telemetry={"memory_usage": memory_usage, "cpu_usage": cpu_usage}) + except RetryException as e: + logger.error(f"Sending telemetry failed: {e}") + if not success: + logger.error("Telemetry failed.") + send_telemetry = False + time.sleep(update_frequency) + @contextmanager def logging_context(self, context: LoggingContext): """Set the logging context.""" @@ -277,6 +299,10 @@ def _task_stream_callback(self, request: fedn.TaskRequest) -> None: self.validate_global_model(request) elif request.type == fedn.StatusType.MODEL_PREDICTION: self.predict_global_model(request) + elif request.type == fedn.StatusType.FORWARD: + self.forward_embeddings(request) + elif request.type == fedn.StatusType.BACKWARD: + self.backward_gradients(request) def update_local_model(self, request: fedn.TaskRequest) -> None: """Update the local model.""" @@ -300,7 +326,7 @@ def update_local_model(self, request: fedn.TaskRequest) -> None: self.send_status( f"\t Starting processing of training request for model_id {model_id}", - sesssion_id=request.session_id, + session_id=request.session_id, sender_name=self.name, log_level=fedn.LogLevel.INFO, type=fedn.StatusType.MODEL_UPDATE, @@ -329,7 +355,7 @@ def update_local_model(self, request: fedn.TaskRequest) -> None: log_level=fedn.LogLevel.AUDIT, type=fedn.StatusType.MODEL_UPDATE, request=update, - sesssion_id=request.session_id, + session_id=request.session_id, sender_name=self.name, ) @@ -340,7 +366,7 @@ def validate_global_model(self, request: fedn.TaskRequest) -> None: self.send_status( f"Processing validate request for model_id {model_id}", - sesssion_id=request.session_id, + session_id=request.session_id, sender_name=self.name, log_level=fedn.LogLevel.INFO, type=fedn.StatusType.MODEL_VALIDATION, @@ -356,7 +382,7 @@ def validate_global_model(self, request: fedn.TaskRequest) -> None: logger.error("No validate callback set") return - logger.info(f"Running validate callback with model ID: {model_id}") + logger.debug(f"Running validate callback with model ID: {model_id}") metrics = self.validate_callback(in_model) if metrics is not None: @@ -371,7 +397,7 @@ def validate_global_model(self, request: fedn.TaskRequest) -> None: log_level=fedn.LogLevel.AUDIT, type=fedn.StatusType.MODEL_VALIDATION, request=validation, - sesssion_id=request.session_id, + session_id=request.session_id, sender_name=self.name, ) else: @@ -379,7 +405,7 @@ def validate_global_model(self, request: fedn.TaskRequest) -> None: f"Client {self.name} failed to complete model validation.", log_level=fedn.LogLevel.WARNING, request=request, - sesssion_id=request.session_id, + session_id=request.session_id, sender_name=self.name, ) @@ -444,6 +470,97 @@ def log_metric(self, metrics: dict, step: int = None, commit: bool = True) -> bo return self.grpc_handler.send_model_metric(message) + def forward_embeddings(self, request): + """Forward pass for split learning gradient calculation or inference.""" + model_id = request.model_id + is_sl_inference = json.loads(request.data).get("is_sl_inference", False) + + embedding_update_id = str(uuid.uuid4()) + + if not self.forward_callback: + logger.error("No forward callback set") + return + + self.send_status(f"\t Starting processing of forward request for model_id {model_id}", session_id=request.session_id, sender_name=self.name) + + logger.info(f"Running forward callback with model ID: {model_id}") + tic = time.time() + out_embeddings, meta = self.forward_callback(self.client_id, is_sl_inference) + meta["processing_time"] = time.time() - tic + + tic = time.time() + self.send_model_to_combiner(model=out_embeddings, id=embedding_update_id) + meta["upload_model"] = time.time() - tic + + meta["config"] = request.data + + update = self.create_update_message(model_id=model_id, model_update_id=embedding_update_id, meta=meta, request=request) + + self.send_model_update(update) + + self.send_status( + "Forward pass completed.", + log_level=fedn.LogLevel.AUDIT, + type=fedn.StatusType.MODEL_UPDATE, + request=update, + session_id=request.session_id, + sender_name=self.name, + ) + + def backward_gradients(self, request): + """Split learning backward pass to update the local client models.""" + model_id = request.model_id + + try: + tic = time.time() + in_gradients = self.get_model_from_combiner(id=model_id, client_id=self.client_id) # gets gradients + + if in_gradients is None: + logger.error("Could not retrieve gradients from combiner. Aborting backward request.") + return + + fetch_model_time = time.time() - tic + + if not self.backward_callback: + logger.error("No backward callback set") + return + + self.send_status(f"\t Starting processing of backward request for gradient_id {model_id}", session_id=request.session_id, sender_name=self.name) + + logger.info(f"Running backward callback with gradient ID: {model_id}") + tic = time.time() + meta = self.backward_callback(in_gradients, self.client_id) + meta["processing_time"] = time.time() - tic + + meta["fetch_model"] = fetch_model_time + meta["config"] = request.data + meta["status"] = "success" + + logger.info("Creating and sending backward completion to combiner.") + completion = self.create_backward_completion_message(gradient_id=model_id, meta=meta, request=request) + self.grpc_handler.send_backward_completion(completion) + + self.send_status( + "Backward pass completed. Status: finished_backward", + log_level=fedn.LogLevel.AUDIT, + type=fedn.StatusType.BACKWARD, + session_id=request.session_id, + sender_name=self.name, + ) + except Exception as e: + logger.error(f"Error in backward pass: {str(e)}") + + def create_backward_completion_message(self, gradient_id: str, meta: dict, request: fedn.TaskRequest): + """Create a backward completion message.""" + return self.grpc_handler.create_backward_completion_message( + sender_name=self.name, + receiver_name=request.sender.name, + receiver_role=request.sender.role, + gradient_id=gradient_id, + session_id=request.session_id, + meta=meta, + ) + def log_attributes(self, attributes: dict) -> bool: """Log the attributes to the server. @@ -465,6 +582,27 @@ def log_attributes(self, attributes: dict) -> bool: return self.grpc_handler.send_attributes(message) + def log_telemetry(self, telemetry: dict) -> bool: + """Log the telemetry data to the server. + + Args: + telemetry (dict): The telemetry data to log. + + Returns: + bool: True if the telemetry data was logged successfully, False otherwise. + + """ + message = fedn.TelemetryMessage() + message.sender.name = self.name + message.sender.client_id = self.client_id + message.sender.role = fedn.Role.CLIENT + message.timestamp.GetCurrentTime() + + for key, value in telemetry.items(): + message.telemetries.add(key=key, value=value) + + return self.grpc_handler.send_telemetry(message) + def create_update_message(self, model_id: str, model_update_id: str, meta: dict, request: fedn.TaskRequest) -> fedn.ModelUpdate: """Create an update message.""" return self.grpc_handler.create_update_message( @@ -511,9 +649,12 @@ def set_client_id(self, client_id: str) -> None: logger.info(f"Setting client ID to: {client_id}") self.client_id = client_id - def run(self) -> None: + def run(self, with_telemetry=True, with_heartbeat=True) -> None: """Run the client.""" - threading.Thread(target=self.send_heartbeats, kwargs={"client_name": self.name, "client_id": self.client_id}, daemon=True).start() + if with_heartbeat: + threading.Thread(target=self.send_heartbeats, args=(self.name, self.client_id), daemon=True).start() + if with_telemetry: + threading.Thread(target=self.default_telemetry_loop, daemon=True).start() try: self.listen_to_task_stream(client_name=self.name, client_id=self.client_id) except KeyboardInterrupt: @@ -533,11 +674,11 @@ def send_status( log_level: fedn.LogLevel = fedn.LogLevel.INFO, type: Optional[str] = None, request: Optional[Union[fedn.ModelUpdate, fedn.ModelValidation, fedn.TaskRequest]] = None, - sesssion_id: Optional[str] = None, + session_id: Optional[str] = None, sender_name: Optional[str] = None, ) -> None: """Send the status.""" - self.grpc_handler.send_status(msg, log_level, type, request, sesssion_id, sender_name) + self.grpc_handler.send_status(msg, log_level, type, request, session_id, sender_name) def send_model_update(self, update: fedn.ModelUpdate) -> bool: """Send the model update.""" diff --git a/fedn/network/clients/grpc_handler.py b/fedn/network/clients/grpc_handler.py index c6f6015e4..f0060babc 100644 --- a/fedn/network/clients/grpc_handler.py +++ b/fedn/network/clients/grpc_handler.py @@ -2,13 +2,14 @@ import json import os +import random import time from datetime import datetime, timezone +from functools import wraps from io import BytesIO from typing import Any, Callable, Optional, Union import grpc -import psutil from google.protobuf.json_format import MessageToJson import fedn.network.grpc.fedn_pb2 as fedn @@ -18,24 +19,23 @@ from fedn.network.combiner.modelservice import upload_request_generator # Keepalive settings: these help keep the connection open for long-lived clients -KEEPALIVE_TIME_MS = 1 * 1000 # send keepalive ping every 60 seconds +KEEPALIVE_TIME_MS = 60 * 1000 # Updated, using Benjamins code, send keepalive ping every 60 seconds # wait 20 seconds for keepalive ping ack before considering connection dead -KEEPALIVE_TIMEOUT_MS = 30 * 1000 +KEEPALIVE_TIMEOUT_MS = 30 * 1000 # Updated: Match server's timeout # allow keepalive pings even when there are no RPCs KEEPALIVE_PERMIT_WITHOUT_CALLS = True -MAX_CONNECTION_IDLE_MS = 30000 -MAX_CONNECTION_AGE_GRACE_MS = "INT_MAX" # keep connection open indefinitely -CLIENT_IDLE_TIMEOUT_MS = 30000 GRPC_OPTIONS = [ ("grpc.keepalive_time_ms", KEEPALIVE_TIME_MS), ("grpc.keepalive_timeout_ms", KEEPALIVE_TIMEOUT_MS), ("grpc.keepalive_permit_without_calls", KEEPALIVE_PERMIT_WITHOUT_CALLS), - ("grpc.http2.max_pings_without_data", 0), # unlimited pings without data - ("grpc.max_connection_idle_ms", MAX_CONNECTION_IDLE_MS), - ("grpc.max_connection_age_grace_ms", MAX_CONNECTION_AGE_GRACE_MS), - ("grpc.client_idle_timeout_ms", CLIENT_IDLE_TIMEOUT_MS), -] + ("grpc.http2.max_pings_without_data", 5), # Updated: limit pings without data to 5 + # ("grpc.max_connection_idle_ms", MAX_CONNECTION_IDLE_MS), + # ("grpc.max_connection_age_grace_ms", MAX_CONNECTION_AGE_GRACE_MS), + # ("grpc.client_idle_timeout_ms", CLIENT_IDLE_TIMEOUT_MS), + ("grpc.http2.min_time_between_pings_ms", 10000), # Added line: minimum 10 seconds between pings + ("grpc.http2.min_ping_interval_without_data_ms", 15000), # Added line: minimum 15 seconds between pings when idle + ] GRPC_SECURE_PORT = 443 @@ -52,6 +52,86 @@ def __call__(self, context: grpc.AuthMetadataContext, callback: grpc.AuthMetadat callback((("authorization", f"{FEDN_AUTH_SCHEME} {self._key}"),), None) +class RetryException(Exception): + pass + + +def grpc_retry( + max_retries: int = 3, + retry_interval: float = 5, + backoff: float = 2, +) -> Callable: + """GRPC retry decorator. + + + :param max_retries: The maximum number of retries. -1 means infinite retries. + :type max_retries: int + :param retry_interval: The interval between retries in seconds. + :type retry_interval: float + :return: The decorated function. + :rtype: Callable + """ + + def decorator(func: Callable) -> Callable: + @wraps(func) + def wrapper(self: "GrpcHandler", *args, **kwargs): + """Wrapper function for retrying GRPC calls.""" + retries = 0 + last_try = time.time() + backoff_factor = 1.0 + while max_retries > retries or max_retries == -1: + retries += 1 + backoff_factor *= backoff + + # Reset backoff factor if the last try was more than 16 times the retry interval ago + # This is to prevent the backoff factor from growing too large + # if the server is down for a long time and then comes back up + this_try = time.time() + if this_try - last_try > 16 * retry_interval: + backoff_factor = 1.0 + last_try = this_try + + try: + return func(self, *args, **kwargs) + except grpc.RpcError as e: + status_code = e.code() + if status_code == grpc.StatusCode.UNAVAILABLE: + logger.warning(f"GRPC ({func.__name__}): Server unavailable. Retrying in approx {retry_interval * backoff_factor} seconds.") + logger.debug(f"GRPC ({func.__name__}): Error details: {e.details()}") + self._reconnect() + time.sleep(retry_interval * backoff_factor + random.uniform(-0.5, 0.5)) + continue + if status_code == grpc.StatusCode.CANCELLED: + logger.warning(f"GRPC ({func.__name__}): Connection cancelled. Retrying in approx {retry_interval * backoff_factor} seconds.") + logger.debug(f"GRPC ({func.__name__}): Error details: {e.details()}") + time.sleep(retry_interval * backoff_factor + random.uniform(-0.5, 0.5)) + continue + if status_code == grpc.StatusCode.UNKNOWN: + details = e.details() + if details == "Stream removed": + logger.warning(f"GRPC ({func.__name__}): Stream removed. Retrying in approx {retry_interval * backoff_factor} seconds.") + self._reconnect() + time.sleep(retry_interval * backoff_factor + random.uniform(-0.5, 0.5)) + continue + raise e + raise e + except Exception as e: + logger.warning(f"GRPC ({func.__name__}): An unknown error occurred: {e}.") + if isinstance(e, ValueError): + logger.warning(f"GRPC ({func.__name__}): Retrying in approx {retry_interval * backoff_factor} seconds.") + self._reconnect() + time.sleep(retry_interval * backoff_factor + random.uniform(-0.5, 0.5)) + continue + raise e + + logger.error(f"GRPC ({func.__name__}): Max retries exceeded.") + raise RetryException("Max retries exceeded") + + return wrapper + + return decorator + + class GrpcHandler: """Handler for GRPC connections and operations.""" @@ -90,7 +170,11 @@ def _init_secure_channel(self, host: str, port: int, token: str) -> None: logger.info("Using root certificate from environment variable for GRPC channel.") with open(os.environ["FEDN_GRPC_ROOT_CERT_PATH"], "rb") as f: credentials = grpc.ssl_channel_credentials(f.read()) - self.channel = grpc.secure_channel(f"{host}:{port}", credentials) + self.channel = grpc.secure_channel( + f"{host}:{port}", + credentials, + options=GRPC_OPTIONS, + ) return credentials = grpc.ssl_channel_credentials() @@ -110,48 +194,32 @@ def _init_insecure_channel(self, host: str, port: int) -> None: options=GRPC_OPTIONS, ) - def heartbeat(self, client_name: str, client_id: str, memory_utilisation: float, cpu_utilisation: float) -> fedn.Response: + def heartbeat(self, client_name: str, client_id: str, memory_utilisation: float = None, cpu_utilisation: float = None) -> fedn.Response: """Send a heartbeat to the combiner. :return: Response from the combiner. :rtype: fedn.Response """ - heartbeat = fedn.Heartbeat( - sender=fedn.Client(name=client_name, role=fedn.CLIENT, client_id=client_id), - memory_utilisation=memory_utilisation, - cpu_utilisation=cpu_utilisation, - ) + heartbeat = fedn.Heartbeat(sender=fedn.Client(name=client_name, role=fedn.CLIENT, client_id=client_id)) + + response = self.connectorStub.SendHeartbeat(heartbeat, metadata=self.metadata) - try: - response = self.connectorStub.SendHeartbeat(heartbeat, metadata=self.metadata) - except grpc.RpcError as e: - logger.error(f"GRPC (SendHeartbeat): An error occurred: {e}") - raise e - except Exception as e: - logger.error(f"GRPC (SendHeartbeat): An error occurred: {e}") - raise e return response + @grpc_retry(max_retries=-1, retry_interval=5) def send_heartbeats(self, client_name: str, client_id: str, update_frequency: float = 2.0) -> None: """Send heartbeats to the combiner at regular intervals.""" send_heartbeat = True while send_heartbeat: - try: - memory_usage = psutil.virtual_memory().percent - cpu_usage = psutil.cpu_percent(interval=update_frequency) - response = self.heartbeat(client_name, client_id, memory_usage, cpu_usage) - except grpc.RpcError as e: - self._handle_grpc_error(e, "SendHeartbeat", lambda: self.send_heartbeats(client_name, client_id, update_frequency)) - return - except Exception as e: - self._handle_unknown_error(e, "SendHeartbeat", lambda: self.send_heartbeats(client_name, client_id, update_frequency)) - return + response = self.heartbeat(client_name, client_id) + time.sleep(update_frequency) if isinstance(response, fedn.Response): pass else: logger.error("Heartbeat failed.") send_heartbeat = False + @grpc_retry(max_retries=-1, retry_interval=5) def listen_to_task_stream(self, client_name: str, client_id: str, callback: Callable[[Any], None]) -> None: """Subscribe to the model update request stream.""" r = fedn.ClientAvailableMessage() @@ -159,37 +227,29 @@ def listen_to_task_stream(self, client_name: str, client_id: str, callback: Call r.sender.role = fedn.CLIENT r.sender.client_id = client_id - try: - logger.info("Listening to task stream.") - for request in self.combinerStub.TaskStream(r, metadata=self.metadata): - if request.sender.role == fedn.COMBINER: - self.send_status( - "Received request from combiner.", - log_level=fedn.LogLevel.AUDIT, - type=request.type, - request=request, - sesssion_id=request.session_id, - sender_name=client_name, - ) - - logger.info(f"Received task request of type {request.type} for model_id {request.model_id}") - - callback(request) - - except grpc.RpcError as e: - logger.error(f"GRPC (TaskStream): An error occurred: {e}") - self._handle_grpc_error(e, "TaskStream", lambda: self.listen_to_task_stream(client_name, client_id, callback)) - except Exception as e: - logger.error(f"GRPC (TaskStream): An error occurred: {e}") - self._handle_unknown_error(e, "TaskStream", lambda: self.listen_to_task_stream(client_name, client_id, callback)) - + logger.info("Listening to task stream.") + for request in self.combinerStub.TaskStream(r, metadata=self.metadata): + if request.sender.role == fedn.COMBINER: + self.send_status( + "Received request from combiner.", + log_level=fedn.LogLevel.AUDIT, + type=request.type, + request=request, + session_id=request.session_id, + sender_name=client_name, + ) + + logger.info(f"Received task request of type {request.type} for model_id {request.model_id}") + callback(request) + + @grpc_retry(max_retries=5, retry_interval=5) def send_status( self, msg: str, log_level: fedn.LogLevel = fedn.LogLevel.INFO, type: Optional[str] = None, request: Optional[Union[fedn.ModelUpdate, fedn.ModelValidation, fedn.TaskRequest]] = None, - sesssion_id: Optional[str] = None, + session_id: Optional[str] = None, sender_name: Optional[str] = None, ) -> None: """Send status message. @@ -209,7 +269,7 @@ def send_status( status.sender.role = fedn.CLIENT status.log_level = log_level status.status = str(msg) - status.session_id = sesssion_id + status.session_id = session_id if type is not None: status.type = type @@ -217,43 +277,31 @@ def send_status( if request is not None: status.data = MessageToJson(request) - try: - logger.info("Sending status message to combiner.") - _ = self.connectorStub.SendStatus(status, metadata=self.metadata) - except grpc.RpcError as e: - self._handle_grpc_error(e, "SendStatus", lambda: self.send_status(msg, log_level, type, request, sesssion_id, sender_name)) - except Exception as e: - logger.error(f"GRPC (SendStatus): An error occurred: {e}") - self._handle_unknown_error(e, "SendStatus", lambda: self.send_status(msg, log_level, type, request, sesssion_id, sender_name)) + logger.info("Sending status message to combiner.") + _ = self.connectorStub.SendStatus(status, metadata=self.metadata) + @grpc_retry(max_retries=5, retry_interval=5) def send_model_metric(self, metric: fedn.ModelMetric) -> bool: """Send a model metric to the combiner.""" - try: - logger.info("Sending model metric to combiner.") - _ = self.combinerStub.SendModelMetric(metric, metadata=self.metadata) - except grpc.RpcError as e: - self._handle_grpc_error(e, "SendModelMetric", lambda: self.send_model_metric(metric)) - return False - except Exception as e: - logger.error(f"GRPC (SendModelMetric): An error occurred: {e}") - self._handle_unknown_error(e, "SendModelMetric", lambda: self.send_model_metric(metric)) - return False + logger.info("Sending model metric to combiner.") + _ = self.combinerStub.SendModelMetric(metric, metadata=self.metadata) return True + @grpc_retry(max_retries=5, retry_interval=5) def send_attributes(self, attribute: fedn.AttributeMessage) -> bool: """Send a attribute message to the combiner.""" - try: - logger.info("Sending attributes to combiner.") - _ = self.combinerStub.SendAttributeMessage(attribute, metadata=self.metadata) - except grpc.RpcError as e: - self._handle_grpc_error(e, "SendAttributeMessage", lambda: self.send_attributes(attribute)) - return False - except Exception as e: - logger.error(f"GRPC (SendAttributeMessage): An error occurred: {e}") - self._handle_unknown_error(e, "SendAttributeMessage", lambda: self.send_attributes(attribute)) - return False + logger.debug("Sending attributes to combiner.") + _ = self.combinerStub.SendAttributeMessage(attribute, metadata=self.metadata) return True + @grpc_retry(max_retries=5, retry_interval=5) + def send_telemetry(self, telemetry: fedn.TelemetryMessage) -> bool: + """Send a telemetry message to the combiner.""" + logger.debug("Sending telemetry to combiner.") + _ = self.combinerStub.SendTelemetryMessage(telemetry, metadata=self.metadata) + return True + + @grpc_retry(max_retries=-1, retry_interval=5) def get_model_from_combiner(self, id: str, client_id: str, timeout: int = 20) -> Optional[BytesIO]: """Fetch a model from the assigned combiner. @@ -274,29 +322,24 @@ def get_model_from_combiner(self, id: str, client_id: str, timeout: int = 20) -> request.sender.client_id = client_id request.sender.role = fedn.CLIENT - try: - logger.info("Downloading model from combiner.") - for part in self.modelStub.Download(request, metadata=self.metadata): - if part.status == fedn.ModelStatus.IN_PROGRESS: - data.write(part.data) + logger.info("Downloading model from combiner.") + for part in self.modelStub.Download(request, metadata=self.metadata): + if part.status == fedn.ModelStatus.IN_PROGRESS: + data.write(part.data) - if part.status == fedn.ModelStatus.OK: - return data + if part.status == fedn.ModelStatus.OK: + return data - if part.status == fedn.ModelStatus.FAILED: - return None + if part.status == fedn.ModelStatus.FAILED: + return None - if part.status == fedn.ModelStatus.UNKNOWN: - if time.time() - time_start >= timeout: - return None - continue - except grpc.RpcError as e: - return self._handle_grpc_error(e, "Download", lambda: self.get_model_from_combiner(id, client_id, timeout)) - except Exception as e: - logger.error(f"GRPC (Download): An error occurred: {e}") - self._handle_unknown_error(e, "Download", lambda: self.get_model_from_combiner(id, client_id, timeout)) + if part.status == fedn.ModelStatus.UNKNOWN: + if time.time() - time_start >= timeout: + return None + continue return data + @grpc_retry(max_retries=-1, retry_interval=5) def send_model_to_combiner(self, model: BytesIO, id: str) -> Optional[BytesIO]: """Send a model update to the assigned combiner. @@ -319,14 +362,8 @@ def send_model_to_combiner(self, model: BytesIO, id: str) -> Optional[BytesIO]: bt.seek(0, 0) - try: - logger.info("Uploading model to combiner.") - result = self.modelStub.Upload(upload_request_generator(bt, id), metadata=self.metadata) - except grpc.RpcError as e: - return self._handle_grpc_error(e, "Upload", lambda: self.send_model_to_combiner(model, id)) - except Exception as e: - logger.error(f"GRPC (Upload): An error occurred: {e}") - self._handle_unknown_error(e, "Upload", lambda: self.send_model_to_combiner(model, id)) + logger.info("Uploading model to combiner.") + result = self.modelStub.Upload(upload_request_generator(bt, id), metadata=self.metadata) return result def create_update_message( @@ -402,6 +439,39 @@ def create_prediction_message( return prediction + def create_backward_completion_message( + self, + sender_name: str, + receiver_name: str, + receiver_role: fedn.Role, + gradient_id: str, + session_id: str, + meta: dict, + ): + completion = fedn.BackwardCompletion() + completion.sender.name = sender_name + completion.sender.role = fedn.CLIENT + completion.sender.client_id = self.metadata[0][1] + completion.receiver.name = receiver_name + completion.receiver.role = receiver_role + completion.gradient_id = gradient_id + completion.timestamp.GetCurrentTime() + completion.meta = json.dumps(meta) + completion.session_id = session_id + return completion + + def send_backward_completion(self, update: fedn.BackwardCompletion): + """Send a backward completion message to the combiner.""" + try: + logger.info("Sending backward completion to combiner.") + _ = self.combinerStub.SendBackwardCompletion(update, metadata=self.metadata) + except grpc.RpcError as e: + return self._handle_grpc_error(e, "SendBackwardCompletion", lambda: self.send_backward_completion(update)) + except Exception as e: + logger.error(f"GRPC (SendBackwardCompletion): An error occurred: {e}") + self._handle_unknown_error(e, "SendBackwardCompletion", lambda: self.send_backward_completion(update)) + return True + def create_metric_message( self, sender_name: str, sender_client_id: str, metrics: dict, step: int, model_id: str, session_id: str, round_id: str ) -> fedn.ModelMetric: @@ -420,94 +490,35 @@ def create_metric_message( metric.metrics.add(key=key, value=value) return metric + @grpc_retry(max_retries=-1, retry_interval=5) def send_model_update(self, update: fedn.ModelUpdate) -> bool: """Send a model update to the combiner.""" - try: - logger.info("Sending model update to combiner.") - _ = self.combinerStub.SendModelUpdate(update, metadata=self.metadata) - except grpc.RpcError as e: - return self._handle_grpc_error(e, "SendModelUpdate", lambda: self.send_model_update(update)) - except Exception as e: - logger.error(f"GRPC (SendModelUpdate): An error occurred: {e}") - self._handle_unknown_error(e, "SendModelUpdate", lambda: self.send_model_update(update)) + logger.info("Sending model update to combiner.") + _ = self.combinerStub.SendModelUpdate(update, metadata=self.metadata) return True + @grpc_retry(max_retries=-1, retry_interval=5) def send_model_validation(self, validation: fedn.ModelValidation) -> bool: """Send a model validation to the combiner.""" - try: - logger.info("Sending model validation to combiner.") - _ = self.combinerStub.SendModelValidation(validation, metadata=self.metadata) - except grpc.RpcError as e: - return self._handle_grpc_error( - e, - "SendModelValidation", - lambda: self.send_model_validation(validation), - ) - except Exception as e: - logger.error(f"GRPC (SendModelValidation): An error occurred: {e}") - self._handle_unknown_error(e, "SendModelValidation", lambda: self.send_model_validation(validation)) + logger.info("Sending model validation to combiner.") + _ = self.combinerStub.SendModelValidation(validation, metadata=self.metadata) return True + @grpc_retry(max_retries=-1, retry_interval=5) def send_model_prediction(self, prediction: fedn.ModelPrediction) -> bool: """Send a model prediction to the combiner.""" - try: - logger.info("Sending model prediction to combiner.") - _ = self.combinerStub.SendModelPrediction(prediction, metadata=self.metadata) - except grpc.RpcError as e: - return self._handle_grpc_error( - e, - "SendModelPrediction", - lambda: self.send_model_prediction(prediction), - ) - except Exception as e: - logger.error(f"GRPC (SendModelPrediction): An error occurred: {e}") - self._handle_unknown_error(e, "SendModelPrediction", lambda: self.send_model_prediction(prediction)) + logger.info("Sending model prediction to combiner.") + _ = self.combinerStub.SendModelPrediction(prediction, metadata=self.metadata) return True - def _handle_grpc_error(self, e: grpc.RpcError, method_name: str, sender_function: Callable) -> Optional[Callable]: - """Handle GRPC errors.""" - status_code = e.code() - if status_code == grpc.StatusCode.UNAVAILABLE: - logger.warning(f"GRPC ({method_name}): server unavailable. Retrying in 5 seconds.") - time.sleep(5) - return sender_function() - if status_code == grpc.StatusCode.CANCELLED: - logger.warning(f"GRPC ({method_name}): connection cancelled. Retrying in 5 seconds.") - time.sleep(5) - return sender_function() - if status_code == grpc.StatusCode.UNAUTHENTICATED: - details = e.details() - if details == "Token expired": - logger.warning(f"GRPC ({method_name}): Token expired.") - raise e - if status_code == grpc.StatusCode.UNKNOWN: - logger.warning(f"GRPC ({method_name}): An unknown error occurred: {e}.") - details = e.details() - if details == "Stream removed": - logger.warning(f"GRPC ({method_name}): Stream removed. Reconnecting") - self._disconnect() - self._init_channel(self.host, self.port, self.token) - self._init_stubs() - return sender_function() - raise e - self._disconnect() - logger.error(f"GRPC ({method_name}): An error occurred: {e}") - raise e - - def _handle_unknown_error(self, e: Exception, method_name: str, sender_function: Callable) -> Optional[Callable]: - """Handle unknown errors.""" - logger.warning(f"GRPC ({method_name}): An unknown error occurred: {e}.") - if isinstance(e, ValueError): - # ValueError is raised when the channel is closed - self._disconnect() - logger.warning(f"GRPC ({method_name}): Reconnecting to channel.") - # recreate the channel - self._init_channel(self.host, self.port, self.token) - self._init_stubs() - return sender_function() - raise e - def _disconnect(self) -> None: """Disconnect from the combiner.""" self.channel.close() logger.info("GRPC channel closed.") + + def _reconnect(self) -> None: + """Reconnect to the combiner.""" + self._disconnect() + self._init_channel(self.host, self.port, self.token) + self._init_stubs() + logger.debug("GRPC channel reconnected.") diff --git a/fedn/network/clients/package_runtime.py b/fedn/network/clients/package_runtime.py index a8d8f96d8..15c046d66 100644 --- a/fedn/network/clients/package_runtime.py +++ b/fedn/network/clients/package_runtime.py @@ -17,7 +17,7 @@ HTTP_STATUS_NO_CONTENT = 204 # Default timeout for requests -REQUEST_TIMEOUT = 10 # seconds +REQUEST_TIMEOUT = 60 # seconds class PackageRuntime: diff --git a/fedn/network/combiner/aggregators/fedavg.py b/fedn/network/combiner/aggregators/fedavg.py index 82a64b5fa..8801404e6 100644 --- a/fedn/network/combiner/aggregators/fedavg.py +++ b/fedn/network/combiner/aggregators/fedavg.py @@ -53,7 +53,13 @@ def combine_models(self, helper=None, delete_models=True, parameters=None): logger.info("AGGREGATOR({}): Loading model metadata {}.".format(self.name, model_update.model_update_id)) tic = time.time() - model_next, metadata = self.update_handler.load_model_update(model_update, helper) + t0 = time.monotonic() + try: + model_next, metadata = self.update_handler.load_model_update(model_update, helper) + except Exception as e: + logger.error(f"AGGREGATOR({self.name}): Error loading model update: {e}") + continue + logger.info("Time taken to load model update: {:.2f} seconds".format(time.monotonic() - t0)) data["time_model_load"] += time.time() - tic logger.info("AGGREGATOR({}): Processing model update {}, metadata: {} ".format(self.name, model_update.model_update_id, metadata)) diff --git a/fedn/network/combiner/aggregators/splitlearningagg.py b/fedn/network/combiner/aggregators/splitlearningagg.py new file mode 100644 index 000000000..3e2d67ce9 --- /dev/null +++ b/fedn/network/combiner/aggregators/splitlearningagg.py @@ -0,0 +1,217 @@ +import os +import traceback + +import torch +from torch import nn + +from fedn.common.log_config import logger +from fedn.network.combiner.aggregators.aggregatorbase import AggregatorBase +from fedn.utils.helpers.helpers import get_helper + +HELPER_MODULE = "splitlearninghelper" +helper = get_helper(HELPER_MODULE) + +seed = 42 +torch.manual_seed(seed) + + +class ServerModel(nn.Module): + """Server side neural network model for Split Learning.""" + + def __init__(self, input_features): + super(ServerModel, self).__init__() + self.fc = nn.Linear(input_features, 1) + self.sigmoid = nn.Sigmoid() + + def forward(self, x): + x = self.sigmoid(self.fc(x)) + return x + + +class Aggregator(AggregatorBase): + """Local SGD / Federated Averaging (FedAvg) aggregator. Computes a weighted mean + of parameter updates. + + :param id: A reference to id of :class: `fedn.network.combiner.Combiner` + :type id: str + :param storage: Model repository for :class: `fedn.network.combiner.Combiner` + :type storage: class: `fedn.common.storage.s3.s3repo.S3ModelRepository` + :param server: A handle to the Combiner class :class: `fedn.network.combiner.Combiner` + :type server: class: `fedn.network.combiner.Combiner` + :param modelservice: A handle to the model service :class: `fedn.network.combiner.modelservice.ModelService` + :type modelservice: class: `fedn.network.combiner.modelservice.ModelService` + :param control: A handle to the :class: `fedn.network.combiner.roundhandler.RoundHandler` + :type control: class: `fedn.network.combiner.roundhandler.RoundHandler` + + """ + + def __init__(self, update_handler): + """Constructor method""" + super().__init__(update_handler) + + self.name = "splitlearningagg" + self.model = None + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + + def combine_models(self, helper=None, delete_models=True, is_sl_inference=False): + """Concatenates client embeddings in the queue by aggregating them. + + After all embeddings are received, the embeddings need to be sorted + (consistently) by client ID. + + :param helper: An instance of :class: `fedn.utils.helpers.helpers.HelperBase`, ML framework specific helper, defaults to None + :type helper: class: `fedn.utils.helpers.helpers.HelperBase`, optional + :param delete_models: Delete models from storage after aggregation, defaults to True + :type delete_models: bool, optional + :param is_sl_inference: Whether it is a splitlearning inference session (no gradient calculation) or not + :type is_sl_inference: bool + :return: The gradients and metadata + :rtype: tuple + """ + data = {} + data["time_model_load"] = 0.0 + data["time_model_aggregation"] = 0.0 + + embeddings = None + nr_aggregated_embeddings = 0 + + logger.info("AGGREGATOR({}): Aggregating client embeddings... ".format(self.name)) + + while not self.update_handler.model_updates.empty(): + try: + logger.info("AGGREGATOR({}): Getting next embedding from queue.".format(self.name)) + new_embedding = self.update_handler.next_model_update() # returns in format {client_id: embedding} + + # Load model parameters and metadata + logger.info("AGGREGATOR({}): Loading embedding metadata.".format(self.name)) + embedding_next, metadata = self.update_handler.load_model_update(new_embedding, helper) + + logger.info("AGGREGATOR({}): Processing embedding metadata: {} ".format(self.name, metadata)) + + if nr_aggregated_embeddings == 0: + embeddings = embedding_next + else: + embeddings = helper.increment_average(embeddings, embedding_next) + + nr_aggregated_embeddings += 1 + # Delete model from storage + if delete_models: + self.update_handler.delete_model(new_embedding) + except Exception as e: + tb = traceback.format_exc() + logger.error(f"AGGREGATOR({self.name}): Error encoutered while processing embedding update: {e}") + logger.error(tb) + + logger.info("splitlearning aggregator: Embeddings have been aggregated.") + + result = {"gradients": None, "validation_data": None, "data": None} + + # order embeddings and change to tensor + client_order = sorted(embeddings.keys()) + ordered_embeddings = [] + for client_id in client_order: + embedding = torch.tensor(embeddings[client_id], requires_grad=True) + ordered_embeddings.append(embedding) + + concatenated_embeddings = torch.cat(ordered_embeddings, dim=1) # to 1d tensor + + # instantiate server model + if self.model is None: + self.input_features = concatenated_embeddings.shape[1] + self.model = ServerModel(self.input_features) + self.model.to(self.device) + + # check if concatenated_embeddings matches the input features of the server model + if concatenated_embeddings.shape[1] != self.input_features: + logger.error( + f"Server-side input feature mismatch: Received {concatenated_embeddings.shape[1]} input features, but expected {self.input_features}. \ + This is likely because one of the clients dropped out." + ) + raise ValueError + + if is_sl_inference == "False": + # split learning forward pass with gradient calculation + logger.info("Split Learning Aggregator: Executing forward training pass") + + gradients = self.calculate_gradients(concatenated_embeddings, client_order, ordered_embeddings) + + result["gradients"] = gradients + result["data"] = data + + logger.info("AGGREGATOR({}): Gradients are calculated.".format(self.name)) + + return result + else: + # split learning forward pass for inference, no gradient calculation (used for validation) + logger.info("Split Learning Aggregator: Executing forward inference pass") + + validation_data = self.calculate_validation_metrics(concatenated_embeddings) + + result["validation_data"] = validation_data + result["data"] = data + + logger.info("AGGREGATOR({}): Test Loss: {}, Test Accuracy: {}".format(self.name, validation_data["test_loss"], validation_data["test_accuracy"])) + + return result + + def calculate_gradients(self, concatenated_embeddings, client_order, ordered_embeddings): + self.model.train() + criterion = nn.BCELoss() + optimizer = torch.optim.Adam(self.model.parameters(), lr=0.01) + optimizer.zero_grad() + + output = self.model(concatenated_embeddings) + targets = self.load_targets(is_train=True) + targets = targets.to(self.device) + + loss = criterion(output, targets) + logger.info("AGGREGATOR({}): Train Loss: {}".format(self.name, loss)) + + loss.backward() + + optimizer.step() + + # Split gradients by client + gradients = {} + for client_id, embedding in zip(client_order, ordered_embeddings): + gradients[str(client_id)] = embedding.grad.numpy() + + return gradients + + def calculate_validation_metrics(self, concatenated_embeddings): + self.model.eval() + with torch.no_grad(): + criterion = nn.BCELoss() + output = self.model(concatenated_embeddings) + targets = self.load_targets(is_train=False) + targets = targets.to(self.device) + # metric calculation + test_loss = criterion(output, targets) + + predictions = (output > 0.5).float() + correct = (predictions == targets).sum().item() + total = targets.numel() # Total number of predictions + test_accuracy = correct / total + + validation_data = {"test_loss": test_loss, "test_accuracy": test_accuracy} + + return validation_data + + def load_targets(self, is_train=True): + """Load target labels for split learning.""" + try: + data_path = os.environ.get("FEDN_LABELS_PATH") + except Exception as e: + logger.error(f"FEDN_LABELS_PATH environment variable is not set. Set via export FEDN_LABELS_PATH='path/to/labels.pt', {e}") + raise + + try: + data = torch.load(data_path, weights_only=True) + if is_train: + targets = data["y_train"] + else: + targets = data["y_test"] + return targets.reshape(-1, 1) # Reshape to match model output shape + except Exception as e: + logger.error(f"Error loading labels from {data_path}: {str(e)}") + raise diff --git a/fedn/network/combiner/combiner.py b/fedn/network/combiner/combiner.py index 482efe5a5..323c2b8bf 100644 --- a/fedn/network/combiner/combiner.py +++ b/fedn/network/combiner/combiner.py @@ -21,12 +21,12 @@ from fedn.network.storage.dbconnection import DatabaseConnection from fedn.network.storage.s3.repository import Repository from fedn.network.storage.statestore.stores.dto import ClientDTO -from fedn.network.storage.statestore.stores.dto.analytic import AnalyticDTO from fedn.network.storage.statestore.stores.dto.attribute import AttributeDTO from fedn.network.storage.statestore.stores.dto.combiner import CombinerDTO from fedn.network.storage.statestore.stores.dto.metric import MetricDTO from fedn.network.storage.statestore.stores.dto.prediction import PredictionDTO from fedn.network.storage.statestore.stores.dto.status import StatusDTO +from fedn.network.storage.statestore.stores.dto.telemetry import TelemetryDTO from fedn.network.storage.statestore.stores.dto.validation import ValidationDTO from fedn.network.storage.statestore.stores.shared import SortOrder @@ -256,7 +256,38 @@ def request_model_prediction(self, prediction_id: str, model_id: str, clients: l else: logger.info("Sent model prediction request for model {} to {} clients".format(model_id, len(clients))) - def _send_request_type(self, request_type, session_id, model_id, config=None, clients=[]): + def request_forward_pass(self, session_id: str, model_id: str, config: dict, clients=[]) -> None: + """Ask clients to perform forward pass. + + :param config: the model configuration to send to clients + :type config: dict + :param clients: the clients to send the request to + :type clients: list + + """ + clients = self._send_request_type(fedn.StatusType.FORWARD, session_id, model_id, config, clients) + + if len(clients) < 20: + logger.info("Sent forward request to clients {}".format(clients)) + else: + logger.info("Sent forward request to {} clients".format(len(clients))) + + def request_backward_pass(self, session_id: str, gradient_id: str, config: dict, clients=[]) -> None: + """Ask clients to perform backward pass. + + :param config: the model configuration to send to clients + :type config: dict + :param clients: the clients to send the request to + :type clients: list + """ + clients = self._send_request_type(fedn.StatusType.BACKWARD, session_id, gradient_id, config, clients) + + if len(clients) < 20: + logger.info("Sent backward request for gradients {} to clients {}".format(gradient_id, clients)) + else: + logger.info("Sent backward request for gradients {} to {} clients".format(gradient_id, len(clients))) + + def _send_request_type(self, request_type, session_id, model_id=None, config=None, clients=[]): """Send a request of a specific type to clients. :param request_type: the type of request @@ -273,13 +304,14 @@ def _send_request_type(self, request_type, session_id, model_id, config=None, cl :rtype: list """ if len(clients) == 0: - if request_type == fedn.StatusType.MODEL_UPDATE: + if request_type in [fedn.StatusType.MODEL_UPDATE, fedn.StatusType.FORWARD, fedn.StatusType.BACKWARD]: clients = self.get_active_trainers() elif request_type == fedn.StatusType.MODEL_VALIDATION: clients = self.get_active_validators() elif request_type == fedn.StatusType.MODEL_PREDICTION: # TODO: add prediction clients type clients = self.get_active_validators() + for client in clients: request = fedn.TaskRequest() request.model_id = model_id @@ -292,12 +324,13 @@ def _send_request_type(self, request_type, session_id, model_id, config=None, cl request.sender.role = fedn.COMBINER request.receiver.client_id = client request.receiver.role = fedn.CLIENT + # Set the request data, not used in validation if request_type == fedn.StatusType.MODEL_PREDICTION: presigned_url = self.repository.presigned_put_url(self.repository.prediction_bucket, f"{client}/{session_id}") # TODO: in prediction, request.data should also contain user-defined data/parameters request.data = json.dumps({"presigned_url": presigned_url}) - elif request_type == fedn.StatusType.MODEL_UPDATE: + elif request_type in [fedn.StatusType.MODEL_UPDATE, fedn.StatusType.FORWARD, fedn.StatusType.BACKWARD]: request.data = json.dumps(config) self._put_request_to_client_queue(request, fedn.Queue.TASK_QUEUE) return clients @@ -397,7 +430,9 @@ def _list_active_clients(self, channel): "update_active_clients": [], "update_offline_clients": [], } + for client in self._list_subscribed_clients(channel): + status = self.clients[client]["status"] now = datetime.now() then = self.clients[client]["last_seen"] @@ -410,12 +445,14 @@ def _list_active_clients(self, channel): elif status != "offline": self.clients[client]["status"] = "offline" clients["update_offline_clients"].append(client) + # Update statestore with client status if len(clients["update_active_clients"]) > 0: for client in clients["update_active_clients"]: client_to_update = self.db.client_store.get(client) client_to_update.status = "online" self.db.client_store.update(client_to_update) + if len(clients["update_offline_clients"]) > 0: for client in clients["update_offline_clients"]: client_to_update = self.db.client_store.get(client) @@ -621,6 +658,7 @@ def ListActiveClients(self, request: fedn.ListClientsRequest, context): nr_active_clients = len(active_clients) if nr_active_clients < 20: logger.info("grpc.Combiner.ListActiveClients: Active clients: {}".format(active_clients)) + logger.info("grpc.Combiner.ListActiveClients: Number active clients: {}".format(nr_active_clients)) else: logger.info("grpc.Combiner.ListActiveClients: Number active clients: {}".format(nr_active_clients)) @@ -675,20 +713,6 @@ def SendHeartbeat(self, heartbeat: fedn.Heartbeat, context): self.__join_client(client) self.clients[client.client_id]["last_seen"] = datetime.now() - if heartbeat.cpu_utilisation is not None or heartbeat.memory_utilisation is not None: - analytic = AnalyticDTO().patch_with( - { - "sender_id": client.client_id, - "sender_role": "client", - "cpu_utilisation": heartbeat.cpu_utilisation, - "memory_utilisation": heartbeat.memory_utilisation, - } - ) - try: - self.db.analytic_store.add(analytic) - except Exception as e: - logger.error(f"GRPC: SendHeartbeat error: {e}") - response = fedn.Response() response.sender.name = heartbeat.sender.name response.sender.role = heartbeat.sender.role @@ -832,6 +856,41 @@ def SendModelPrediction(self, request, context): response.response = "RECEIVED ModelPrediction {} from client {}".format(response, response.sender.name) return response + def SendBackwardCompletion(self, request, context): + """Send a backward completion response. + + :param request: the request + :type request: :class:`fedn.network.grpc.fedn_pb2.BackwardCompletion` + :param context: the context + :type context: :class:`grpc._server._Context` + :return: the response + :rtype: :class:`fedn.network.grpc.fedn_pb2.Response` + """ + logger.info("Received BackwardCompletion from {}".format(request.sender.name)) + + # Add completion to the queue + self.round_handler.update_handler.backward_completions.put(request) + + # Create and send status message for backward completion + status = fedn.Status() + status.timestamp.GetCurrentTime() + status.sender.name = request.sender.name + status.sender.role = request.sender.role + status.sender.client_id = request.sender.client_id + status.status = "finished_backward" + status.type = fedn.StatusType.BACKWARD + status.session_id = request.session_id + + logger.info(f"Creating status message with session_id: {request.session_id}") + self._send_status(status) + logger.info("Status message sent to MongoDB") + + ########### + + response = fedn.Response() + response.response = "RECEIVED BackwardCompletion from client {}".format(request.sender.name) + return response + def SendModelMetric(self, request, context): """Send a model metric response. @@ -842,7 +901,7 @@ def SendModelMetric(self, request, context): :return: the response :rtype: :class:`fedn.network.grpc.fedn_pb2.Response` """ - logger.info("Received ModelMetric from {}".format(request.sender.name)) + logger.debug("Received ModelMetric from {}".format(request.sender.name)) metric_msg = MessageToDict(request, preserving_proto_field_name=True) metrics = metric_msg.pop("metrics") @@ -861,7 +920,7 @@ def SendAttributeMessage(self, request, context): :param request: the request :type request: :class:`fedn.network.grpc.fedn_pb2.AttributeMessage` """ - logger.info("Received Attributes from {}".format(request.sender.name)) + logger.debug("Received Attributes from {}".format(request.sender.name)) attribute_msg = MessageToDict(request, preserving_proto_field_name=True) attributes = attribute_msg.pop("attributes") @@ -874,6 +933,25 @@ def SendAttributeMessage(self, request, context): return fedn.Response() + def SendTelemetryMessage(self, request, context): + """Send a telemetry message. + + :param request: the request + :type request: :class:`fedn.network.grpc.fedn_pb2.TelemetryMessage` + """ + logger.debug("Received Telemetry from {}".format(request.sender.name)) + telemetry_msg = MessageToDict(request, preserving_proto_field_name=True) + telemetries = telemetry_msg.pop("telemetries") + for telemetry in telemetries: + telemetry = {"value": 0.0, **telemetry} + new_telemetry = TelemetryDTO(**telemetry, **telemetry_msg) + try: + self.db.telemetry_store.add(new_telemetry) + except Exception as e: + logger.error(f"Failed to register telemetry: {e}") + + return fedn.Response() + #################################################################################################################### def run(self): diff --git a/fedn/network/combiner/hooks/allowed_import.py b/fedn/network/combiner/hooks/allowed_import.py index 1de7eee2e..05cb2da65 100644 --- a/fedn/network/combiner/hooks/allowed_import.py +++ b/fedn/network/combiner/hooks/allowed_import.py @@ -1,9 +1,20 @@ +import os import random # noqa: F401 from typing import Dict, List, Tuple # noqa: F401 import numpy as np # noqa: F401 +from fedn import APIClient from fedn.common.log_config import logger # noqa: F401 from fedn.network.combiner.hooks.serverfunctionsbase import ServerFunctionsBase # noqa: F401 +if os.getenv("REDUCER_SERVICE_HOST") and os.getenv("REDUCER_SERVICE_PORT"): + host = f"{os.getenv('REDUCER_SERVICE_HOST')}:{os.getenv('REDUCER_SERVICE_PORT')}/internal" + port = None +else: + host = "api-server" + port = 8092 + +api_client = APIClient(host=host, port=port) + print = logger.info diff --git a/fedn/network/combiner/hooks/hooks.py b/fedn/network/combiner/hooks/hooks.py index 54e79333c..5f65e3193 100644 --- a/fedn/network/combiner/hooks/hooks.py +++ b/fedn/network/combiner/hooks/hooks.py @@ -1,3 +1,4 @@ +import ast import json from concurrent import futures @@ -6,9 +7,9 @@ import fedn.network.grpc.fedn_pb2 as fedn import fedn.network.grpc.fedn_pb2_grpc as rpc from fedn.common.log_config import logger -from fedn.network.combiner.hooks.allowed_import import * # noqa: F403 -# imports for user code +# imports for user defined code +from fedn.network.combiner.hooks.allowed_import import * # noqa: F403 from fedn.network.combiner.hooks.allowed_import import ServerFunctionsBase from fedn.network.combiner.modelservice import bytesIO_request_generator, model_as_bytesIO, unpack_model from fedn.utils.helpers.plugins.numpyhelper import Helper @@ -32,6 +33,7 @@ def __init__(self) -> None: self.server_functions_code: str = None self.client_updates = {} self.implemented_functions = {} + logger.info("Server Functions initialized.") def HandleClientConfig(self, request_iterator: fedn.ClientConfigRequest, context): """Distribute client configs to clients from user defined code. @@ -50,7 +52,7 @@ def HandleClientConfig(self, request_iterator: fedn.ClientConfigRequest, context logger.info(f"Client config response: {client_settings}") return fedn.ClientConfigResponse(client_settings=json.dumps(client_settings)) except Exception as e: - logger.exception("Error handling client config request: %s", e) + logger.error(f"Error handling client config request: {e}") def HandleClientSelection(self, request: fedn.ClientSelectionRequest, context): """Handle client selection from user defined code. @@ -69,7 +71,7 @@ def HandleClientSelection(self, request: fedn.ClientSelectionRequest, context): logger.info(f"Clients selected: {client_ids}") return fedn.ClientSelectionResponse(client_ids=json.dumps(client_ids)) except Exception as e: - logger.exception("Error handling client selection request: %s", e) + logger.error(f"Error handling client selection request: {e}") def HandleMetadata(self, request: fedn.ClientMetaRequest, context): """Store client metadata from a request. @@ -90,7 +92,7 @@ def HandleMetadata(self, request: fedn.ClientMetaRequest, context): self.check_incremental_aggregate(client_id) return fedn.ClientMetaResponse(status="Metadata stored") except Exception as e: - logger.exception("Error handling store metadata request: %s", e) + logger.error(f"Error handling store metadata request: {e}") def HandleStoreModel(self, request_iterator, context): try: @@ -106,7 +108,7 @@ def HandleStoreModel(self, request_iterator, context): self.check_incremental_aggregate(client_id) return fedn.StoreModelResponse(status=f"Received model originating from {client_id}") except Exception as e: - logger.exception("Error handling store model request: %s", e) + logger.error(f"Error handling store model request: {e}") def check_incremental_aggregate(self, client_id): # incremental aggregation (memory secure) @@ -144,7 +146,7 @@ def HandleAggregation(self, request, context): for response in response_generator: yield response except Exception as e: - logger.exception("Error handling aggregation request: %s", e) + logger.error(f"Error handling aggregation request: {e}") def HandleProvidedFunctions(self, request: fedn.ProvidedFunctionsResponse, context): """Handles the 'provided_functions' request. Sends back which functions are available. @@ -169,17 +171,24 @@ def HandleProvidedFunctions(self, request: fedn.ProvidedFunctionsResponse, conte self.implemented_functions = {} self._instansiate_server_functions_code() functions = ["client_selection", "client_settings", "aggregate", "incremental_aggregate"] + # parse the entire code string into an AST + tree = ast.parse(server_functions_code) + + # collect all real function names + defined_funcs = {node.name for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)} + + # check each target function for func in functions: - if func in self.server_functions_code: - logger.info(f"String {func} found in server functions code, assuming function is implemented.") + if func in defined_funcs: + print(f"Function '{func}' found—assuming it´s implemented.") self.implemented_functions[func] = True else: - logger.info(f"No {func} found in server functions code.") + print(f"Function '{func}' not found.") self.implemented_functions[func] = False logger.info(f"Provided function: {self.implemented_functions}") return fedn.ProvidedFunctionsResponse(available_functions=self.implemented_functions) except Exception as e: - logger.exception("Error handling provided functions request: %s", e) + logger.error(f"Error handling provided functions request: {e}") def _instansiate_server_functions_code(self): # this will create a new user defined instance of the ServerFunctions class. @@ -200,7 +209,7 @@ def serve(): MAX_CONNECTION_IDLE_MS = 5 * 60 * 1000 # max idle time before server terminates the connection (5 minutes) MAX_MESSAGE_LENGTH = 1 * 1024 * 1024 * 1024 # 1 GB in bytes server = grpc.server( - futures.ThreadPoolExecutor(max_workers=100), # Increase based on expected load + futures.ThreadPoolExecutor(max_workers=10000), # Increase based on expected load options=[ ("grpc.keepalive_time_ms", KEEPALIVE_TIME_MS), ("grpc.keepalive_timeout_ms", KEEPALIVE_TIMEOUT_MS), diff --git a/fedn/network/combiner/interfaces.py b/fedn/network/combiner/interfaces.py index 2dc485754..f9192b051 100644 --- a/fedn/network/combiner/interfaces.py +++ b/fedn/network/combiner/interfaces.py @@ -1,6 +1,7 @@ import base64 import copy import json +import time import grpc @@ -216,32 +217,46 @@ def set_server_functions(self, server_functions): else: raise - def submit(self, config: RoundConfig): - """Submit a compute plan to the combiner. - + def submit(self, config: RoundConfig, max_retries: int = 10, retry_delay: float = 3.0): + """Submit a compute plan to the combiner, with retry on UNAVAILABLE errors. + :param config: The job configuration. - :type config: dict + :type config: RoundConfig + :param max_retries: How many times to retry if gRPC returns UNAVAILABLE. + :type max_retries: int + :param retry_delay: Seconds to wait before retrying. + :type retry_delay: float :return: Server ControlResponse object. :rtype: :class:`fedn.network.grpc.fedn_pb2.ControlResponse` """ - channel = Channel(self.address, self.port, self.certificate).get_channel() - control = rpc.ControlStub(channel) - request = fedn.ControlRequest() - request.command = fedn.Command.START - for k, v in config.items(): - p = request.parameter.add() - p.key = str(k) - p.value = str(v) - - try: - response = control.Start(request) - except grpc.RpcError as e: - if e.code() == grpc.StatusCode.UNAVAILABLE: - raise CombinerUnavailableError - else: - raise - - return response + for attempt in range(max_retries): + channel = Channel(self.address, self.port, self.certificate).get_channel() + control = rpc.ControlStub(channel) + request = fedn.ControlRequest() + request.command = fedn.Command.START + + for k, v in config.items(): + p = request.parameter.add() + p.key = str(k) + p.value = str(v) + + try: + response = control.Start(request) + return response # If call succeeds, return immediately + + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.UNAVAILABLE: + # Final attempt, Raise CombinerUnavailableError + if attempt == max_retries - 1: + raise CombinerUnavailableError + else: + # Sleep before trying again + time.sleep(retry_delay) + else: + # For any other error re-raise + raise + # as a backup + raise CombinerUnavailableError("Exceeded max retries in submit()") def allowing_clients(self): """Check if the combiner is allowing additional client connections. @@ -269,24 +284,39 @@ def allowing_clients(self): return False - def list_active_clients(self, queue=1): - """List active clients. + def list_active_clients(self, queue=1, max_retries=3, retry_delay=1.0): + """List active clients with retry logic. :param queue: The channel (queue) to use (optional). Default is 1 = MODEL_UPDATE_REQUESTS channel. - see :class:`fedn.network.grpc.fedn_pb2.Channel` - :type channel: int + :type queue: int + :param max_retries: How many times to retry if gRPC returns UNAVAILABLE. + :type max_retries: int + :param retry_delay: Seconds to wait before retrying. + :type retry_delay: float :return: A list of active clients. - :rtype: json + :rtype: list """ - channel = Channel(self.address, self.port, self.certificate).get_channel() - control = rpc.ConnectorStub(channel) - request = fedn.ListClientsRequest() - request.channel = queue - try: - response = control.ListActiveClients(request) - except grpc.RpcError as e: - if e.code() == grpc.StatusCode.UNAVAILABLE: - raise CombinerUnavailableError - else: - raise - return response.client + for attempt in range(max_retries): + channel = Channel(self.address, self.port, self.certificate).get_channel() + control = rpc.ConnectorStub(channel) + request = fedn.ListClientsRequest() + request.channel = queue + + try: + response = control.ListActiveClients(request) + # If successful, return immediately + return response.client + + except grpc.RpcError as e: + if e.code() == grpc.StatusCode.UNAVAILABLE: + # last attempt, raise CombinerUnavailableError + if attempt == max_retries - 1: + raise CombinerUnavailableError + else: + # wait and retry + time.sleep(retry_delay) + else: + # other error re-raise + raise + # as a backup + raise CombinerUnavailableError("Exceeded max retries for list_active_clients") \ No newline at end of file diff --git a/fedn/network/combiner/roundhandler.py b/fedn/network/combiner/roundhandler.py index a6575712d..a87bd8bb1 100644 --- a/fedn/network/combiner/roundhandler.py +++ b/fedn/network/combiner/roundhandler.py @@ -5,6 +5,7 @@ import time import uuid from typing import TYPE_CHECKING, TypedDict +from pymongo import ReturnDocument from fedn.common.log_config import logger from fedn.network.combiner.aggregators.aggregatorbase import get_aggregator @@ -134,7 +135,14 @@ def _training_round(self, config: dict, clients: list, provided_functions: dict) :type clients: list :return: an aggregated model and associated metadata :rtype: model, dict + """ + # Ensure an aggregator is configured + if not hasattr(self, "aggregator"): + default_aggr = config.get("aggregator", "fedavg") + logger.warning("Aggregator not set; defaulting to %s", default_aggr) + self.set_aggregator(default_aggr) + logger.info("ROUNDHANDLER: Initiating training round, participating clients: {}".format(clients)) meta = {} @@ -145,6 +153,7 @@ def _training_round(self, config: dict, clients: list, provided_functions: dict) session_id = config["session_id"] model_id = config["model_id"] + if provided_functions.get("client_settings", False): global_model_bytes = self.modelservice.temp_model_storage.get(model_id) client_settings = self.hook_interface.client_settings(global_model_bytes) @@ -153,17 +162,20 @@ def _training_round(self, config: dict, clients: list, provided_functions: dict) # Request model updates from all active clients. self.server.request_model_update(session_id=session_id, model_id=model_id, config=config, clients=clients) - # If buffer_size is -1 (default), the round terminates when/if all clients have completed. + # If buffer_size is -1 (default), the round terminates when/if all clients have completed if int(config["buffer_size"]) == -1: buffer_size = len(clients) else: buffer_size = int(config["buffer_size"]) # Wait / block until the round termination policy has been met. + t0 = time.perf_counter() self.update_handler.waitforit(config, buffer_size=buffer_size) + logger.info("self.update_handler.waitforit took: {:.3f} s".format(time.perf_counter() - t0)) tic = time.time() model = None data = None + t0 = time.perf_counter() try: helper = get_helper(config["helper_type"]) logger.info("Config delete_models_storage: {}".format(config["delete_models_storage"])) @@ -177,6 +189,7 @@ def _training_round(self, config: dict, clients: list, provided_functions: dict) parameters = Parameters(dict_parameters) else: parameters = None + logger.info("aggregator_kwargs was not in config.keys") if provided_functions.get("aggregate", False) or provided_functions.get("incremental_aggregate", False): previous_model_bytes = self.modelservice.temp_model_storage.get(model_id) model, data = self.hook_interface.aggregate(previous_model_bytes, self.update_handler, helper, delete_models=delete_models) @@ -185,6 +198,8 @@ def _training_round(self, config: dict, clients: list, provided_functions: dict) except Exception as e: logger.warning("AGGREGATION FAILED AT COMBINER! {}".format(e)) raise + logger.info("Combine models, delete models took: {:.3f} s".format(time.perf_counter() - t0)) + meta["time_combination"] = time.time() - tic meta["aggregation_time"] = data @@ -214,7 +229,95 @@ def _prediction_round(self, prediction_id: str, model_id: str, clients: list): """ self.server.request_model_prediction(prediction_id, model_id, clients=clients) - def stage_model(self, model_id, timeout_retry=3, retry=2): + def _forward_pass(self, config: dict, clients: list): + """Send model forward pass requests to clients. + + :param config: The round config object (passed to the client). + :type config: dict + :param clients: clients to participate in the training round + :type clients: list + :return: aggregated embeddings and associated metadata + :rtype: model, dict + """ + # Ensure an aggregator is configured + if not hasattr(self, "aggregator"): + default_aggr = config.get("aggregator", "fedavg") + logger.warning("Aggregator not set; defaulting to %s", default_aggr) + self.set_aggregator(default_aggr) + + logger.info("ROUNDHANDLER: Initiating forward pass, participating clients: {}".format(clients)) + + meta = {} + meta["nr_expected_updates"] = len(clients) + meta["nr_required_updates"] = int(config["clients_required"]) + meta["timeout"] = float(config["round_timeout"]) + + session_id = config["session_id"] + model_id = config["model_id"] + is_sl_inference = config[ + "is_sl_inference" + ] # determines whether forward pass calculates gradients ("training"), or is used for inference (e.g., for validation) + # Request forward pass from all active clients. + self.server.request_forward_pass(session_id=session_id, model_id=model_id, config=config, clients=clients) + + # the round should terminate when all clients have completed + buffer_size = len(clients) + + # Wait / block until the round termination policy has been met. + self.update_handler.waitforit(config, buffer_size=buffer_size) + + tic = time.time() + output = None + try: + helper = get_helper(config["helper_type"]) + logger.info("Config delete_models_storage: {}".format(config["delete_models_storage"])) + if config["delete_models_storage"] == "True": + delete_models = True + else: + delete_models = False + + output = self.aggregator.combine_models(helper=helper, delete_models=delete_models, is_sl_inference=is_sl_inference) + + except Exception as e: + logger.warning("EMBEDDING CONCATENATION in FORWARD PASS FAILED AT COMBINER! {}".format(e)) + + meta["time_combination"] = time.time() - tic + meta["aggregation_time"] = output["data"] + return output, meta + + def _backward_pass(self, config: dict, clients: list): + """Send backward pass requests to clients. + + :param config: The round config object (passed to the client). + :type config: dict + :param clients: clients to participate in the training round + :type clients: list + :return: associated metadata + :rtype: dict + """ + logger.info("ROUNDHANDLER: Initiating backward pass, participating clients: {}".format(clients)) + + meta = {} + meta["nr_expected_updates"] = len(clients) + meta["nr_required_updates"] = int(config["clients_required"]) + meta["timeout"] = float(config["round_timeout"]) + + # Clear previous backward completions queue + self.update_handler.clear_backward_completions() + + # Request backward pass from all active clients. + logger.info("ROUNDHANDLER: Requesting backward pass, gradient_id: {}".format(config["model_id"])) + + self.server.request_backward_pass(session_id=config["session_id"], gradient_id=config["model_id"], config=config, clients=clients) + + # the round should terminate when all clients have completed + buffer_size = len(clients) + + self.update_handler.waitforbackwardcompletion(config, required_backward_completions=buffer_size) + + return meta + + def stage_model(self, model_id, timeout_retry=0.5, retry=1): """Download a model from persistent storage and set in modelservice. :param model_id: ID of the model update object to stage. @@ -333,10 +436,21 @@ def execute_training_round(self, config): provided_functions = self.hook_interface.provided_functions(self.server_functions) if provided_functions.get("client_selection", False): - clients = self.hook_interface.client_selection(clients=self.server.get_active_trainers()) + selected = 0 + while not selected: + clients = self.hook_interface.client_selection(clients=self.server.get_active_trainers()) + selected = len(clients) + if not selected: + logger.info("No clients selected based on custom client selection implementation. Trying again in 15 seconds.") + time.sleep(15) + else: clients = self._assign_round_clients(self.server.max_clients) + t0 = time.perf_counter() model, meta = self._training_round(config, clients, provided_functions) + logger.info("self._training_round took: {:.3f} s".format(time.perf_counter() - t0)) + + data["data"] = meta if model is None: @@ -355,6 +469,75 @@ def execute_training_round(self, config): self.modelservice.temp_model_storage.delete(config["model_id"]) return data + def execute_forward_pass(self, config): + """Coordinates clients to execute forward pass. + + :param config: The round config object. + :type config: dict + :return: metadata about the training round. + :rtype: dict + """ + logger.info("Processing forward pass, job_id {}".format(config["_job_id"])) + + data = {} + data["config"] = config + data["round_id"] = config["round_id"] + + data["model_id"] = None + + clients = self._assign_round_clients(self.server.max_clients) + output, meta = self._forward_pass(config, clients) + + data["data"] = meta + + if output["gradients"] is None and output["validation_data"] is None: + logger.warning("\t Forward pass failed in round {0}!".format(config["round_id"])) + elif output["validation_data"] is not None: # in forward validation pass, no gradients are calculated. Skip in this case. + logger.info("FORWARD VALIDATION PASS COMPLETED. Job id: {}".format(config["_job_id"])) + return data + elif output["gradients"] is not None: + gradients = output["gradients"] + helper = get_helper(config["helper_type"]) + a = serialize_model_to_BytesIO(gradients, helper) + gradient_id = self.storage.set_model(a.read(), is_file=False) # uploads gradients to storage + a.close() + data["model_id"] = gradient_id # intended + + logger.info("FORWARD PASS COMPLETED. Aggregated model id: {}, Job id: {}".format(gradient_id, config["_job_id"])) + + return data + + def execute_backward_pass(self, config): + """Coordinates clients to execute backward pass. + + :param config: The round config object. + :type config: dict + :return: metadata about the training round. + :rtype: dict + """ + logger.info("Processing backward pass, job_id {}".format(config["_job_id"])) + + data = {} + data["config"] = config + data["round_id"] = config["round_id"] + + logger.info("roundhandler execute_backward_pass: downloading gradients with id: {}".format(config["model_id"])) + + # Download gradients and set in temp storage. + self.stage_model(config["model_id"]) # Download a model from persistent storage and set in modelservice + + clients = self._assign_round_clients(self.server.max_clients) + meta = self._backward_pass(config, clients) + data["data"] = meta + + if meta is None: + logger.warning("\t Failed to run backward pass in round {0}!".format(config["round_id"])) + + # Delete temp model + self.modelservice.temp_model_storage.delete(config["model_id"]) + return data + + def run(self, polling_interval=1.0): """Main control loop. Execute rounds based on round config on the queue. @@ -375,18 +558,30 @@ def run(self, polling_interval=1.0): session_id = round_config["session_id"] model_id = round_config["model_id"] tic = time.time() + t0 = time.perf_counter() round_meta = self.execute_training_round(round_config) + logger.info("execute_training round took: {:.3f} s".format(time.perf_counter() - t0)) round_meta["time_exec_training"] = time.time() - tic round_meta["status"] = "Success" round_meta["name"] = self.server.id + t0 = time.perf_counter() active_round = self.server.db.round_store.get(round_meta["round_id"]) - + logger.info("self.server.db.round_store.get took: {:.3f} s".format(time.perf_counter() - t0)) active_round.combiners.append(round_meta) + t0 = time.perf_counter() try: - self.server.db.round_store.update(active_round) + # self.server.db.round_store.update(active_round) + # for multiple combiners, we need to make sure that we don't overwrite any existing entries. + self.server.db.round_store.update_one( + {"round_id": round_meta["round_id"]}, + {"$push": {"combiners": round_meta}}, + upsert=True + ) except Exception as e: logger.error("Failed to update round data in round store. {}".format(e)) raise Exception("Failed to update round data in round store.") + logger.info("self.server.db.round_store.update_one took: {:.3f} s".format(time.perf_counter() - t0)) + elif round_config["task"] == "validation": session_id = round_config["session_id"] model_id = round_config["model_id"] @@ -395,6 +590,53 @@ def run(self, polling_interval=1.0): prediction_id = round_config["prediction_id"] model_id = round_config["model_id"] self.execute_prediction_round(prediction_id, model_id) + + elif round_config["task"] == "forward": + session_id = round_config["session_id"] + model_id = round_config["model_id"] + tic = time.time() + round_meta = self.execute_forward_pass(round_config) + round_meta["time_exec_training"] = time.time() - tic + round_meta["status"] = "Success" + round_meta["name"] = self.server.id + active_round = self.server.db.round_store.get(round_meta["round_id"]) # if "combiners" not in active_round: + # active_round["combiners"] = [] + # active_round["combiners"].append(round_meta) + # updated = self.server.round_store.update(active_round["id"], active_round) + + active_round.combiners.append(round_meta) + try: + #self.server.db.round_store.update(active_round) + # for multiple combiners, we need to make sure that we don't overwrite any existing entries. + self.server.db.round_store.update_one( + {"round_id": round_meta["round_id"]}, + {"$push": {"combiners": round_meta}}, + upsert=True + ) + except Exception as e: + logger.error("Forward pass: Failed to update round data in round store. {}".format(e)) + raise Exception("Forward passFailed to update round data in round store.") + + elif round_config["task"] == "backward": + tic = time.time() + round_meta = self.execute_backward_pass(round_config) + round_meta["time_exec_training"] = time.time() - tic + round_meta["status"] = "Success" + round_meta["name"] = self.server.id + active_round = self.server.db.round_store.get(round_meta["round_id"]) + # updated = self.server.round_store.update(active_round["id"], active_round) + active_round.combiners.append(round_meta) + try: + self.server.db.round_store.update(active_round) + # for multiple combiners, we need to make sure that we don't overwrite any existing entries. + self.server.db.round_store.update_one( + {"round_id": round_meta["round_id"]}, + {"$push": {"combiners": round_meta}}, + upsert=True + ) + except Exception as e: + logger.error("Backward pass: Failed to update round data in round store. {}".format(e)) + raise Exception("Backward pass: Failed to update round data in round store.") else: logger.warning("config contains unkown task type.") else: diff --git a/fedn/network/combiner/updatehandler.py b/fedn/network/combiner/updatehandler.py index 517595d13..c762229af 100644 --- a/fedn/network/combiner/updatehandler.py +++ b/fedn/network/combiner/updatehandler.py @@ -23,6 +23,7 @@ class UpdateHandler: def __init__(self, modelservice: ModelService) -> None: self.model_updates = queue.Queue() + self.backward_completions = queue.Queue() self.modelservice = modelservice self.model_id_to_model_data = {} @@ -53,8 +54,6 @@ def on_model_update(self, model_update): :type model_id: str """ try: - logger.info("UPDATE HANDLER: callback received model update {}".format(model_update.model_update_id)) - # Validate the update and metadata valid_update = self._validate_model_update(model_update) if valid_update: @@ -110,7 +109,8 @@ def load_model_update(self, model_update, helper): # Used in C++ client config = json.loads(model_update.config) training_metadata = metadata["training_metadata"] - training_metadata["round_id"] = config["round_id"] + if "round_id" in config: + training_metadata["round_id"] = config["round_id"] return model, training_metadata @@ -136,7 +136,8 @@ def load_model_update_byte(self, model_update): # Used in C++ client config = json.loads(model_update.config) training_metadata = metadata["training_metadata"] - training_metadata["round_id"] = config["round_id"] + if "round_id" in config: + training_metadata["round_id"] = config["round_id"] return model, training_metadata @@ -159,7 +160,7 @@ def load_model(self, helper, model_id): return model - def load_model_update_bytesIO(self, model_id, retry=3): + def load_model_update_bytesIO(self, model_id, retry=1): """Load model update object and return it as BytesIO. :param model_id: The ID of the model @@ -180,7 +181,7 @@ def load_model_update_bytesIO(self, model_id, retry=3): tries += 1 if not model_str or sys.getsizeof(model_str) == 80: logger.warning("Model download failed. retrying") - time.sleep(1) + time.sleep(0.5) # sleep longer model_str = self.modelservice.get_model(model_id) return model_str @@ -201,10 +202,41 @@ def waitforit(self, config, buffer_size=100, polling_interval=0.1): """ time_window = float(config["round_timeout"]) + + start_time = time.monotonic() + deadline = start_time + time_window + + while True: + if self.model_updates.qsize() >= buffer_size: + break + + remaining = deadline - time.monotonic() + if remaining <= 0: + break + + time.sleep(min(polling_interval, remaining)) + + + def waitforbackwardcompletion(self, config, required_backward_completions=-1, polling_interval=0.1): + """Wait for backward completion messages. + + :param config: The round config object + :param required_backward_completions: Number of required backward completions + """ + time_window = float(config["round_timeout"]) tt = 0.0 + while tt < time_window: - if self.model_updates.qsize() >= buffer_size: + if self.backward_completions.qsize() >= required_backward_completions: break time.sleep(polling_interval) tt += polling_interval + + def clear_backward_completions(self): + """Clear the backward completions queue.""" + while not self.backward_completions.empty(): + try: + self.backward_completions.get_nowait() + except queue.Empty: + break diff --git a/fedn/network/controller/control.py b/fedn/network/controller/control.py index 2f35bf62d..c750e5b47 100644 --- a/fedn/network/controller/control.py +++ b/fedn/network/controller/control.py @@ -197,7 +197,7 @@ def start_session(self, session_id: str, rounds: int, round_timeout: int, model_ training_run_obj.round_timeout = session_config.round_timeout training_run_obj.rounds = rounds - training_run_obj = self.db.training_run_store.add(training_run_obj) + training_run_obj = self.db.run_store.add(training_run_obj) count_models_of_session = 0 @@ -216,7 +216,7 @@ def start_session(self, session_id: str, rounds: int, round_timeout: int, model_ logger.info("Session terminated.") training_run_obj.completed_at = datetime.datetime.now() training_run_obj.completed_at_model_id = self._get_active_model_id(session_id) - self.db.training_run_store.update(training_run_obj) + self.db.run_store.update(training_run_obj) break _, round_data = self.round( session_config=session_config, @@ -235,7 +235,7 @@ def start_session(self, session_id: str, rounds: int, round_timeout: int, model_ self.set_session_status(session_id, "Finished") training_run_obj.completed_at = datetime.datetime.now() training_run_obj.completed_at_model_id = self._get_active_model_id(session_id) - self.db.training_run_store.update(training_run_obj) + self.db.run_store.update(training_run_obj) logger.info("Session finished.") self._state = ReducerState.idle @@ -275,6 +275,75 @@ def prediction_session(self, config: RoundConfig) -> None: combiner.submit(config) logger.info("Prediction round submitted to combiner {}".format(combiner)) + def splitlearning_session(self, session_id: str, rounds: int, round_timeout: int) -> None: + """Execute a split learning session. + + :param session_id: The session id. + :type session_id: str + :param rounds: The number of rounds. + :type rounds: int + :param round_timeout: The round timeout. + :type round_timeout: int + """ + logger.info("Starting split learning session.") + + if self._state == ReducerState.instructing: + logger.info("Controller already in INSTRUCTING state. A session is in progress.") + return + + self._state = ReducerState.instructing + + session = self.db.session_store.get(session_id) + + if not session: + logger.error("Session not found.") + return + + session_config = session.session_config + + if not session_config: + logger.error("Splitlearning session not properly configured.") + return + + if round_timeout is not None: + session_config.round_timeout = round_timeout + + self._state = ReducerState.monitoring + + last_round = self.get_latest_round_id() + + for combiner in self.network.get_combiners(): + combiner.set_aggregator(session_config.aggregator) + + session_config.session_id = session_id + + self.set_session_status(session_id, "Started") + + # Execute the rounds in this session + for round in range(1, rounds + 1): + if last_round: + current_round = last_round + round + else: + current_round = round + + try: + if self.get_session_status(session_config.session_id) == "Terminated": + logger.info("Session terminated.") + break + _, round_obj = self.splitlearning_round(session_config, str(current_round), session_id) + if round_obj: + logger.info("Split learning round completed with status {}".format(round_obj.status)) + else: + logger.error("Split learning round failed - no round data returned") + except TypeError as e: + logger.error("Failed to execute split learning round: {0}".format(e)) + + if self.get_session_status(session_config.session_id) == "Started": + self.set_session_status(session_config.session_id, "Finished") + self._state = ReducerState.idle + + self.set_session_config(session_id, session_config.to_dict()) + def round(self, session_config: SessionConfigDTO, round_id: str, session_id: str, model_name: Optional[str] = None) -> tuple: """Execute one global round. @@ -320,7 +389,7 @@ def round(self, session_config: SessionConfigDTO, round_id: str, session_id: str # Wait until participating combiners have produced an updated global model, # or round times out. def do_if_round_times_out(result): - logger.warning("Round timed out!") + logger.warning("Round timed out, participating combiners have not yet produced a global model") return True @retry( @@ -336,11 +405,11 @@ def combiners_done(): self.set_round_status(round_id, "Terminated") return False if len(round.combiners) < 1: - logger.info("Waiting for combiners to update model...") + logger.info("Waiting for combiners to update model... 0 so far") raise CombinersNotDoneException("Combiners have not yet reported.") - if len(round.combiners) < len(participating_combiners): - logger.info("Waiting for combiners to update model...") + if 0 < len(round.combiners) and len(round.combiners) < len(participating_combiners): + logger.info(f"Waiting for combiners to update model... {len(round.combiners)} have reported.") raise CombinersNotDoneException("All combiners have not yet reported.") return True @@ -348,7 +417,11 @@ def combiners_done(): combiners_are_done = combiners_done() if not combiners_are_done: return None, self.db.round_store.get(round_id) + + + # This could cause an infinite loop over retries if if len(round.combiners) != len(participating_combiners) + # ------------------------------ # Due to the distributed nature of the computation, there might be a # delay before combiners have reported the round data to the db, # so we need some robustness here. @@ -356,9 +429,11 @@ def combiners_done(): def check_combiners_done_reporting(): round = self.db.round_store.get(round_id) if len(round.combiners) != len(participating_combiners): + logger.info(f"Waiting for combiners to update model... {len(round.combiners)} have reported.") raise KeyError("Combiners have not yet reported.") check_combiners_done_reporting() + # ------------------------------- round = self.db.round_store.get(round_id) round_valid = self.evaluate_round_validity_policy(round.to_dict()) @@ -427,6 +502,147 @@ def check_combiners_done_reporting(): self.set_round_status(round_id, "Finished") return model_id, self.db.round_store.get(round_id) + def splitlearning_round(self, session_config: SessionConfigDTO, round_id: str, session_id: str): + """Execute one global split learning round + + :param session_config: The session config + :type session_config: SessionConfigDTO + :param round_id: The round id + :type round_id: str + :param session_id: The session id + :type session_id: str + """ + # session_id = session_config.session_id + self.create_round({"round_id": round_id, "status": "Pending"}) + + if len(self.network.get_combiners()) < 1: + logger.warning("Round cannot start, no combiners connected!") + self.set_round_status(round_id, "Failed") + return None, self.db.round_store.get(round_id) + + # 1) FORWARD PASS - specified through "task": "forward" + forward_config = session_config.to_dict() + forward_config.update({"rounds": 1, "round_id": round_id, "task": "forward", "is_sl_inference": False, "session_id": session_id}) + + self.set_round_config(round_id, forward_config) + + participating_combiners = self.get_participating_combiners(forward_config) + + if not self.evaluate_round_start_policy(participating_combiners): + logger.warning("Round start policy not met, skipping round!") + self.set_round_status(round_id, "Failed") + return None, self.db.round_store.get(round_id) + + logger.info("CONTROLLER: Requesting forward pass") + # Request forward pass using existing method + _ = self.request_model_updates(participating_combiners) + + # Wait until participating combiners have produced an updated global model, + # or round times out. + def do_if_round_times_out(result): + logger.warning("Round timed out!") + return True + + @retry( + wait=wait_random(min=1.0, max=2.0), + stop=stop_after_delay(session_config.round_timeout), + retry_error_callback=do_if_round_times_out, + retry=retry_if_exception_type(CombinersNotDoneException), + ) + def combiners_done(): + round = self.db.round_store.get(round_id) + session_status = self.get_session_status(session_id) + if session_status == "Terminated": + self.set_round_status(round_id, "Terminated") + return False + if len(round.combiners) < 1: + logger.info("Waiting for combiners to update model...") + raise CombinersNotDoneException("Combiners have not yet reported.") + + if len(round.combiners) < len(participating_combiners): + logger.info("Waiting for combiners to update model...") + raise CombinersNotDoneException("All combiners have not yet reported.") + + return True + + combiners_are_done = combiners_done() + if not combiners_are_done: + return None, self.db.round_store.get(round_id) + + # Due to the distributed nature of the computation, there might be a + # delay before combiners have reported the round data to the db, + # so we need some robustness here. + @retry(wait=wait_random(min=0.1, max=1.0), retry=retry_if_exception_type(KeyError)) + def check_combiners_done_reporting(): + round = self.db.round_store.get(round_id) + if len(round.combiners) != len(participating_combiners): + raise KeyError("Combiners have not yet reported.") + + check_combiners_done_reporting() + + logger.info("CONTROLLER: Forward pass completed.") + + # NOTE: Only works for one combiner + # get model id and send it to backward pass + round = self.db.round_store.get(round_id) + round = round.to_dict() + for combiner in round["combiners"]: + try: + model_id = combiner["model_id"] + except KeyError: + logger.error("Forward pass failed - no model_id in combiner response") + self.set_round_status(round_id, "Failed") + return None, self.db.round_store.get(round_id) + + if model_id is None: + logger.error("Forward pass failed - no model_id in combiner response") + self.set_round_status(round_id, "Failed") + return None, self.db.round_store.get(round_id) + + logger.info("CONTROLLER: starting backward pass with model/gradient id: {}".format(model_id)) + + # 2) BACKWARD PASS + try: + backward_config = session_config.to_dict() + backward_config.update({"rounds": 1, "round_id": round_id, "task": "backward", "session_id": session_id, "model_id": model_id}) + + participating_combiners = [(combiner, backward_config) for combiner, _ in participating_combiners] + result = self.request_model_updates(participating_combiners) + + if not result: + logger.error("Backward pass failed - no result from model updates") + self.set_round_status(round_id, "Failed") + return None, self.db.round_store.get(round_id) + + logger.info("CONTROLLER: Backward pass completed successfully") + self.set_round_status(round_id, "Success") + + except Exception as e: + logger.error(f"Backward pass failed with error: {e}") + self.set_round_status(round_id, "Failed") + return None, self.db.round_store.get(round_id) + + # 3) Validation + validate = session_config.validate + if validate: + logger.info("CONTROLLER: Starting Split Learning Validation round") + validate_config = session_config.to_dict() + validate_config.update({"rounds": 1, "round_id": round_id, "task": "forward", "is_sl_inference": True, "session_id": session_id}) + validating_combiners = [(combiner, validate_config) for combiner, _ in participating_combiners] + + # Submit validation requests + for combiner, config in validating_combiners: + try: + logger.info("Submitting validation for split learning to combiner {}".format(combiner)) + combiner.submit(config) + except CombinerUnavailableError: + self._handle_unavailable_combiner(combiner) + pass + logger.info("Controller: Split Learning Validation completed") + + self.set_round_status(round_id, "Finished") + return None, self.db.round_store.get(round_id) + def reduce(self, combiners): """Combine updated models from Combiner nodes into one global model. @@ -469,8 +685,10 @@ def reduce(self, combiners): model = load_model_from_bytes(data, helper) meta["time_aggregate_model"] += time.time() - tic i = i + 1 - + try: self.repository.delete_model(model_id) + except Exception as e: + logger.error(f"Failed to delete model {model_id} from repository") return model, meta diff --git a/fedn/network/controller/controlbase.py b/fedn/network/controller/controlbase.py index b5fb33534..77edc7952 100644 --- a/fedn/network/controller/controlbase.py +++ b/fedn/network/controller/controlbase.py @@ -1,6 +1,7 @@ import os from abc import ABC, abstractmethod from typing import Any, Dict, List, Tuple +import time import fedn.utils.helpers.helpers from fedn.common.log_config import logger @@ -57,6 +58,11 @@ def __init__( self.db = db self._state = ReducerState.idle + + # For async combinerunavailableerror + self._active_clients_cache = {} + + self.COMBINER_CACHE_COOLDOWN = 20.0 # 20 seconds @abstractmethod def round(self, config, round_number): @@ -238,7 +244,7 @@ def commit(self, model: dict = None, session_id: str = None, name: str = None) - if model is not None: outfile_name = helper.save(model) logger.info("Saving model file temporarily to {}".format(outfile_name)) - logger.info("CONTROL: Uploading model to Minio...") + logger.info("CONTROL: Uploading model to object store...") model_id = self.repository.set_model(outfile_name, is_file=True) logger.info("CONTROL: Deleting temporary model file...") @@ -276,23 +282,50 @@ def get_combiner(self, name): return None def get_participating_combiners(self, combiner_round_config) -> List[Tuple[CombinerInterface, Dict]]: - """Assemble a list of combiners able to participate in a round as - descibed by combiner_round_config. + """Assemble a list of combiners able to participate in a round + according to combiner_round_config. """ combiners = [] for combiner in self.network.get_combiners(): - try: - # Current gRPC endpoint only returns active clients (both trainers and validators) - nr_active_clients = len(combiner.list_active_clients()) - except CombinerUnavailableError: - self._handle_unavailable_combiner(combiner) + nr_active_clients = self._get_nr_active_clients_throttled(combiner) + if nr_active_clients is None: + logger.warning(f"Combiner {combiner.name} returned nr_active_clients: None.") continue clients_required = int(combiner_round_config["clients_required"]) is_participating = self.evaluate_round_participation_policy(clients_required, nr_active_clients) if is_participating: combiners.append((combiner, combiner_round_config)) + return combiners + + def _get_nr_active_clients_throttled(self, combiner: CombinerInterface) -> int: + """Return the number of active clients for a combiner, but avoid + calling `list_active_clients()` if we recently did so. + """ + now = time.time() + cache_entry = self._active_clients_cache.get(combiner.name) + + # if we have a cache entry and the time between last call is less than COMBINER_CACHE_COOLDOWN, reuse it. + if cache_entry: + last_timestamp, nr_active = cache_entry + if (now - last_timestamp) < self.COMBINER_CACHE_COOLDOWN: + return nr_active + + # otherwise, do a fresh call + try: + nr_active_clients = len(combiner.list_active_clients()) + except CombinerUnavailableError: + logger.warning(f"Combiner {combiner.name} is unavailable.") + return None + + # update the cache + self._active_clients_cache[combiner.name] = (time.time(), nr_active_clients) + return nr_active_clients + + + def _handle_unavailable_combiner(self, combiner): + logger.warning(f"Ignoring unavailable combiner {combiner.name}.") def evaluate_round_participation_policy(self, clients_required: int, nr_active_clients: int) -> bool: """Evaluate policy for combiner round-participation. diff --git a/fedn/network/grpc/fedn.proto b/fedn/network/grpc/fedn.proto index 53ef44bfd..25a8c7490 100644 --- a/fedn/network/grpc/fedn.proto +++ b/fedn/network/grpc/fedn.proto @@ -18,6 +18,10 @@ enum StatusType { MODEL_VALIDATION = 4; MODEL_PREDICTION = 5; NETWORK = 6; + FORWARD_REQUEST = 7; + FORWARD = 8; + BACKWARD_REQUEST = 9; + BACKWARD = 10; } enum LogLevel { @@ -93,6 +97,16 @@ message ModelPrediction { string prediction_id = 8; } +message BackwardCompletion { + Client sender = 1; + Client receiver = 2; + string gradient_id = 3; + string correlation_id = 4; + string session_id = 5; + google.protobuf.Timestamp timestamp = 6; + string meta = 7; +} + message ModelMetric { Client sender = 1; repeated MetricElem metrics = 2; @@ -119,6 +133,17 @@ message AttributeElem { string value = 2; } +message TelemetryMessage { + Client sender = 1; + repeated TelemetryElem telemetries = 2; + google.protobuf.Timestamp timestamp = 3; +} + +message TelemetryElem { + string key = 1; + float value = 2; +} + enum ModelStatus { OK = 0; IN_PROGRESS = 1; @@ -283,9 +308,10 @@ service Combiner { rpc SendModelUpdate (ModelUpdate) returns (Response); rpc SendModelValidation (ModelValidation) returns (Response); rpc SendModelPrediction (ModelPrediction) returns (Response); - + rpc SendBackwardCompletion (BackwardCompletion) returns (Response); rpc SendModelMetric(ModelMetric) returns (Response); rpc SendAttributeMessage(AttributeMessage) returns (Response); + rpc SendTelemetryMessage(TelemetryMessage) returns (Response); } message ProvidedFunctionsRequest { diff --git a/fedn/network/grpc/fedn_pb2.py b/fedn/network/grpc/fedn_pb2.py index 3736b67e7..f3fb3b3ef 100644 --- a/fedn/network/grpc/fedn_pb2.py +++ b/fedn/network/grpc/fedn_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # NO CHECKED-IN PROTOBUF GENCODE -# source: network/grpc/fedn.proto +# source: fedn.proto # Protobuf Python Version: 5.28.1 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor @@ -15,7 +15,7 @@ 28, 1, '', - 'network/grpc/fedn.proto' + 'fedn.proto' ) # @@protoc_insertion_point(imports) @@ -26,117 +26,123 @@ from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17network/grpc/fedn.proto\x12\x04\x66\x65\x64n\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\":\n\x08Response\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08response\x18\x02 \x01(\t\"\xf1\x01\n\x06Status\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x0e\n\x06status\x18\x02 \x01(\t\x12!\n\tlog_level\x18\x03 \x01(\x0e\x32\x0e.fedn.LogLevel\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12-\n\ttimestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1e\n\x04type\x18\x07 \x01(\x0e\x32\x10.fedn.StatusType\x12\r\n\x05\x65xtra\x18\x08 \x01(\t\x12\x12\n\nsession_id\x18\t \x01(\t\"\xd8\x01\n\x0bTaskRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\t\x12\x0c\n\x04meta\x18\x07 \x01(\t\x12\x12\n\nsession_id\x18\x08 \x01(\t\x12\x1e\n\x04type\x18\t \x01(\x0e\x32\x10.fedn.StatusType\"\xbf\x01\n\x0bModelUpdate\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\x12\x17\n\x0fmodel_update_id\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\t\x12\x0c\n\x04meta\x18\x07 \x01(\t\x12\x0e\n\x06\x63onfig\x18\x08 \x01(\t\"\xd8\x01\n\x0fModelValidation\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12-\n\ttimestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04meta\x18\x07 \x01(\t\x12\x12\n\nsession_id\x18\x08 \x01(\t\"\xdb\x01\n\x0fModelPrediction\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12-\n\ttimestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04meta\x18\x07 \x01(\t\x12\x15\n\rprediction_id\x18\x08 \x01(\t\"\xe1\x01\n\x0bModelMetric\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12!\n\x07metrics\x18\x02 \x03(\x0b\x32\x10.fedn.MetricElem\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x04step\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x10\n\x08model_id\x18\x05 \x01(\t\x12\x10\n\x08round_id\x18\x06 \x01(\t\x12\x12\n\nsession_id\x18\x07 \x01(\t\"(\n\nMetricElem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x02\"\x88\x01\n\x10\x41ttributeMessage\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\'\n\nattributes\x18\x02 \x03(\x0b\x32\x13.fedn.AttributeElem\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"+\n\rAttributeElem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x89\x01\n\x0cModelRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\n\n\x02id\x18\x04 \x01(\t\x12!\n\x06status\x18\x05 \x01(\x0e\x32\x11.fedn.ModelStatus\"]\n\rModelResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\n\n\x02id\x18\x02 \x01(\t\x12!\n\x06status\x18\x03 \x01(\x0e\x32\x11.fedn.ModelStatus\x12\x0f\n\x07message\x18\x04 \x01(\t\"U\n\x15GetGlobalModelRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\"h\n\x16GetGlobalModelResponse\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\"^\n\tHeartbeat\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1a\n\x12memory_utilisation\x18\x02 \x01(\x02\x12\x17\n\x0f\x63pu_utilisation\x18\x03 \x01(\x02\"W\n\x16\x43lientAvailableMessage\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\x12\x11\n\ttimestamp\x18\x03 \x01(\t\"P\n\x12ListClientsRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1c\n\x07\x63hannel\x18\x02 \x01(\x0e\x32\x0b.fedn.Queue\"*\n\nClientList\x12\x1c\n\x06\x63lient\x18\x01 \x03(\x0b\x32\x0c.fedn.Client\"C\n\x06\x43lient\x12\x18\n\x04role\x18\x01 \x01(\x0e\x32\n.fedn.Role\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x11\n\tclient_id\x18\x03 \x01(\t\"m\n\x0fReassignRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x0e\n\x06server\x18\x03 \x01(\t\x12\x0c\n\x04port\x18\x04 \x01(\r\"c\n\x10ReconnectRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x11\n\treconnect\x18\x03 \x01(\r\"\'\n\tParameter\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"T\n\x0e\x43ontrolRequest\x12\x1e\n\x07\x63ommand\x18\x01 \x01(\x0e\x32\r.fedn.Command\x12\"\n\tparameter\x18\x02 \x03(\x0b\x32\x0f.fedn.Parameter\"F\n\x0f\x43ontrolResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\"\n\tparameter\x18\x02 \x03(\x0b\x32\x0f.fedn.Parameter\"\x13\n\x11\x43onnectionRequest\"<\n\x12\x43onnectionResponse\x12&\n\x06status\x18\x01 \x01(\x0e\x32\x16.fedn.ConnectionStatus\"1\n\x18ProvidedFunctionsRequest\x12\x15\n\rfunction_code\x18\x01 \x01(\t\"\xac\x01\n\x19ProvidedFunctionsResponse\x12T\n\x13\x61vailable_functions\x18\x01 \x03(\x0b\x32\x37.fedn.ProvidedFunctionsResponse.AvailableFunctionsEntry\x1a\x39\n\x17\x41vailableFunctionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"#\n\x13\x43lientConfigRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"/\n\x14\x43lientConfigResponse\x12\x17\n\x0f\x63lient_settings\x18\x01 \x01(\t\",\n\x16\x43lientSelectionRequest\x12\x12\n\nclient_ids\x18\x01 \x01(\t\"-\n\x17\x43lientSelectionResponse\x12\x12\n\nclient_ids\x18\x01 \x01(\t\"8\n\x11\x43lientMetaRequest\x12\x10\n\x08metadata\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\"$\n\x12\x43lientMetaResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\"-\n\x11StoreModelRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\n\n\x02id\x18\x02 \x01(\t\"$\n\x12StoreModelResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\"\'\n\x12\x41ggregationRequest\x12\x11\n\taggregate\x18\x01 \x01(\t\"#\n\x13\x41ggregationResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c*\x98\x01\n\nStatusType\x12\x07\n\x03LOG\x10\x00\x12\x18\n\x14MODEL_UPDATE_REQUEST\x10\x01\x12\x10\n\x0cMODEL_UPDATE\x10\x02\x12\x1c\n\x18MODEL_VALIDATION_REQUEST\x10\x03\x12\x14\n\x10MODEL_VALIDATION\x10\x04\x12\x14\n\x10MODEL_PREDICTION\x10\x05\x12\x0b\n\x07NETWORK\x10\x06*L\n\x08LogLevel\x12\x08\n\x04NONE\x10\x00\x12\x08\n\x04INFO\x10\x01\x12\t\n\x05\x44\x45\x42UG\x10\x02\x12\x0b\n\x07WARNING\x10\x03\x12\t\n\x05\x45RROR\x10\x04\x12\t\n\x05\x41UDIT\x10\x05*$\n\x05Queue\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0e\n\nTASK_QUEUE\x10\x01*S\n\x0bModelStatus\x12\x06\n\x02OK\x10\x00\x12\x0f\n\x0bIN_PROGRESS\x10\x01\x12\x12\n\x0eIN_PROGRESS_OK\x10\x02\x12\n\n\x06\x46\x41ILED\x10\x03\x12\x0b\n\x07UNKNOWN\x10\x04*8\n\x04Role\x12\t\n\x05OTHER\x10\x00\x12\n\n\x06\x43LIENT\x10\x01\x12\x0c\n\x08\x43OMBINER\x10\x02\x12\x0b\n\x07REDUCER\x10\x03*J\n\x07\x43ommand\x12\x08\n\x04IDLE\x10\x00\x12\t\n\x05START\x10\x01\x12\t\n\x05PAUSE\x10\x02\x12\x08\n\x04STOP\x10\x03\x12\t\n\x05RESET\x10\x04\x12\n\n\x06REPORT\x10\x05*I\n\x10\x43onnectionStatus\x12\x11\n\rNOT_ACCEPTING\x10\x00\x12\r\n\tACCEPTING\x10\x01\x12\x13\n\x0fTRY_AGAIN_LATER\x10\x02\x32z\n\x0cModelService\x12\x33\n\x06Upload\x12\x12.fedn.ModelRequest\x1a\x13.fedn.ModelResponse(\x01\x12\x35\n\x08\x44ownload\x12\x12.fedn.ModelRequest\x1a\x13.fedn.ModelResponse0\x01\x32\xbb\x02\n\x07\x43ontrol\x12\x34\n\x05Start\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse\x12\x33\n\x04Stop\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse\x12\x44\n\x15\x46lushAggregationQueue\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse\x12<\n\rSetAggregator\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse\x12\x41\n\x12SetServerFunctions\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse2V\n\x07Reducer\x12K\n\x0eGetGlobalModel\x12\x1b.fedn.GetGlobalModelRequest\x1a\x1c.fedn.GetGlobalModelResponse2\xab\x03\n\tConnector\x12\x44\n\x14\x41llianceStatusStream\x12\x1c.fedn.ClientAvailableMessage\x1a\x0c.fedn.Status0\x01\x12*\n\nSendStatus\x12\x0c.fedn.Status\x1a\x0e.fedn.Response\x12?\n\x11ListActiveClients\x12\x18.fedn.ListClientsRequest\x1a\x10.fedn.ClientList\x12\x45\n\x10\x41\x63\x63\x65ptingClients\x12\x17.fedn.ConnectionRequest\x1a\x18.fedn.ConnectionResponse\x12\x30\n\rSendHeartbeat\x12\x0f.fedn.Heartbeat\x1a\x0e.fedn.Response\x12\x37\n\x0eReassignClient\x12\x15.fedn.ReassignRequest\x1a\x0e.fedn.Response\x12\x39\n\x0fReconnectClient\x12\x16.fedn.ReconnectRequest\x1a\x0e.fedn.Response2\xf3\x02\n\x08\x43ombiner\x12?\n\nTaskStream\x12\x1c.fedn.ClientAvailableMessage\x1a\x11.fedn.TaskRequest0\x01\x12\x34\n\x0fSendModelUpdate\x12\x11.fedn.ModelUpdate\x1a\x0e.fedn.Response\x12<\n\x13SendModelValidation\x12\x15.fedn.ModelValidation\x1a\x0e.fedn.Response\x12<\n\x13SendModelPrediction\x12\x15.fedn.ModelPrediction\x1a\x0e.fedn.Response\x12\x34\n\x0fSendModelMetric\x12\x11.fedn.ModelMetric\x1a\x0e.fedn.Response\x12>\n\x14SendAttributeMessage\x12\x16.fedn.AttributeMessage\x1a\x0e.fedn.Response2\xec\x03\n\x0f\x46unctionService\x12Z\n\x17HandleProvidedFunctions\x12\x1e.fedn.ProvidedFunctionsRequest\x1a\x1f.fedn.ProvidedFunctionsResponse\x12M\n\x12HandleClientConfig\x12\x19.fedn.ClientConfigRequest\x1a\x1a.fedn.ClientConfigResponse(\x01\x12T\n\x15HandleClientSelection\x12\x1c.fedn.ClientSelectionRequest\x1a\x1d.fedn.ClientSelectionResponse\x12\x43\n\x0eHandleMetadata\x12\x17.fedn.ClientMetaRequest\x1a\x18.fedn.ClientMetaResponse\x12G\n\x10HandleStoreModel\x12\x17.fedn.StoreModelRequest\x1a\x18.fedn.StoreModelResponse(\x01\x12J\n\x11HandleAggregation\x12\x18.fedn.AggregationRequest\x1a\x19.fedn.AggregationResponse0\x01\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nfedn.proto\x12\x04\x66\x65\x64n\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\":\n\x08Response\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08response\x18\x02 \x01(\t\"\xf1\x01\n\x06Status\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x0e\n\x06status\x18\x02 \x01(\t\x12!\n\tlog_level\x18\x03 \x01(\x0e\x32\x0e.fedn.LogLevel\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12-\n\ttimestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1e\n\x04type\x18\x07 \x01(\x0e\x32\x10.fedn.StatusType\x12\r\n\x05\x65xtra\x18\x08 \x01(\t\x12\x12\n\nsession_id\x18\t \x01(\t\"\xd8\x01\n\x0bTaskRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\t\x12\x0c\n\x04meta\x18\x07 \x01(\t\x12\x12\n\nsession_id\x18\x08 \x01(\t\x12\x1e\n\x04type\x18\t \x01(\x0e\x32\x10.fedn.StatusType\"\xbf\x01\n\x0bModelUpdate\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\x12\x17\n\x0fmodel_update_id\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12\x11\n\ttimestamp\x18\x06 \x01(\t\x12\x0c\n\x04meta\x18\x07 \x01(\t\x12\x0e\n\x06\x63onfig\x18\x08 \x01(\t\"\xd8\x01\n\x0fModelValidation\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12-\n\ttimestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04meta\x18\x07 \x01(\t\x12\x12\n\nsession_id\x18\x08 \x01(\t\"\xdb\x01\n\x0fModelPrediction\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x05 \x01(\t\x12-\n\ttimestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04meta\x18\x07 \x01(\t\x12\x15\n\rprediction_id\x18\x08 \x01(\t\"\xd0\x01\n\x12\x42\x61\x63kwardCompletion\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x13\n\x0bgradient_id\x18\x03 \x01(\t\x12\x16\n\x0e\x63orrelation_id\x18\x04 \x01(\t\x12\x12\n\nsession_id\x18\x05 \x01(\t\x12-\n\ttimestamp\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x0c\n\x04meta\x18\x07 \x01(\t\"\xe1\x01\n\x0bModelMetric\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12!\n\x07metrics\x18\x02 \x03(\x0b\x32\x10.fedn.MetricElem\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12*\n\x04step\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.UInt32Value\x12\x10\n\x08model_id\x18\x05 \x01(\t\x12\x10\n\x08round_id\x18\x06 \x01(\t\x12\x12\n\nsession_id\x18\x07 \x01(\t\"(\n\nMetricElem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x02\"\x88\x01\n\x10\x41ttributeMessage\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\'\n\nattributes\x18\x02 \x03(\x0b\x32\x13.fedn.AttributeElem\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"+\n\rAttributeElem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"\x89\x01\n\x10TelemetryMessage\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12(\n\x0btelemetries\x18\x02 \x03(\x0b\x32\x13.fedn.TelemetryElem\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"+\n\rTelemetryElem\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x02\"\x89\x01\n\x0cModelRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\n\n\x02id\x18\x04 \x01(\t\x12!\n\x06status\x18\x05 \x01(\x0e\x32\x11.fedn.ModelStatus\"]\n\rModelResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\n\n\x02id\x18\x02 \x01(\t\x12!\n\x06status\x18\x03 \x01(\x0e\x32\x11.fedn.ModelStatus\x12\x0f\n\x07message\x18\x04 \x01(\t\"U\n\x15GetGlobalModelRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\"h\n\x16GetGlobalModelResponse\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x10\n\x08model_id\x18\x03 \x01(\t\"^\n\tHeartbeat\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1a\n\x12memory_utilisation\x18\x02 \x01(\x02\x12\x17\n\x0f\x63pu_utilisation\x18\x03 \x01(\x02\"W\n\x16\x43lientAvailableMessage\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\t\x12\x11\n\ttimestamp\x18\x03 \x01(\t\"P\n\x12ListClientsRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1c\n\x07\x63hannel\x18\x02 \x01(\x0e\x32\x0b.fedn.Queue\"*\n\nClientList\x12\x1c\n\x06\x63lient\x18\x01 \x03(\x0b\x32\x0c.fedn.Client\"C\n\x06\x43lient\x12\x18\n\x04role\x18\x01 \x01(\x0e\x32\n.fedn.Role\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x11\n\tclient_id\x18\x03 \x01(\t\"m\n\x0fReassignRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x0e\n\x06server\x18\x03 \x01(\t\x12\x0c\n\x04port\x18\x04 \x01(\r\"c\n\x10ReconnectRequest\x12\x1c\n\x06sender\x18\x01 \x01(\x0b\x32\x0c.fedn.Client\x12\x1e\n\x08receiver\x18\x02 \x01(\x0b\x32\x0c.fedn.Client\x12\x11\n\treconnect\x18\x03 \x01(\r\"\'\n\tParameter\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t\"T\n\x0e\x43ontrolRequest\x12\x1e\n\x07\x63ommand\x18\x01 \x01(\x0e\x32\r.fedn.Command\x12\"\n\tparameter\x18\x02 \x03(\x0b\x32\x0f.fedn.Parameter\"F\n\x0f\x43ontrolResponse\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\"\n\tparameter\x18\x02 \x03(\x0b\x32\x0f.fedn.Parameter\"\x13\n\x11\x43onnectionRequest\"<\n\x12\x43onnectionResponse\x12&\n\x06status\x18\x01 \x01(\x0e\x32\x16.fedn.ConnectionStatus\"1\n\x18ProvidedFunctionsRequest\x12\x15\n\rfunction_code\x18\x01 \x01(\t\"\xac\x01\n\x19ProvidedFunctionsResponse\x12T\n\x13\x61vailable_functions\x18\x01 \x03(\x0b\x32\x37.fedn.ProvidedFunctionsResponse.AvailableFunctionsEntry\x1a\x39\n\x17\x41vailableFunctionsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x08:\x02\x38\x01\"#\n\x13\x43lientConfigRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\"/\n\x14\x43lientConfigResponse\x12\x17\n\x0f\x63lient_settings\x18\x01 \x01(\t\",\n\x16\x43lientSelectionRequest\x12\x12\n\nclient_ids\x18\x01 \x01(\t\"-\n\x17\x43lientSelectionResponse\x12\x12\n\nclient_ids\x18\x01 \x01(\t\"8\n\x11\x43lientMetaRequest\x12\x10\n\x08metadata\x18\x01 \x01(\t\x12\x11\n\tclient_id\x18\x02 \x01(\t\"$\n\x12\x43lientMetaResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\"-\n\x11StoreModelRequest\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c\x12\n\n\x02id\x18\x02 \x01(\t\"$\n\x12StoreModelResponse\x12\x0e\n\x06status\x18\x01 \x01(\t\"\'\n\x12\x41ggregationRequest\x12\x11\n\taggregate\x18\x01 \x01(\t\"#\n\x13\x41ggregationResponse\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\x0c*\xde\x01\n\nStatusType\x12\x07\n\x03LOG\x10\x00\x12\x18\n\x14MODEL_UPDATE_REQUEST\x10\x01\x12\x10\n\x0cMODEL_UPDATE\x10\x02\x12\x1c\n\x18MODEL_VALIDATION_REQUEST\x10\x03\x12\x14\n\x10MODEL_VALIDATION\x10\x04\x12\x14\n\x10MODEL_PREDICTION\x10\x05\x12\x0b\n\x07NETWORK\x10\x06\x12\x13\n\x0f\x46ORWARD_REQUEST\x10\x07\x12\x0b\n\x07\x46ORWARD\x10\x08\x12\x14\n\x10\x42\x41\x43KWARD_REQUEST\x10\t\x12\x0c\n\x08\x42\x41\x43KWARD\x10\n*L\n\x08LogLevel\x12\x08\n\x04NONE\x10\x00\x12\x08\n\x04INFO\x10\x01\x12\t\n\x05\x44\x45\x42UG\x10\x02\x12\x0b\n\x07WARNING\x10\x03\x12\t\n\x05\x45RROR\x10\x04\x12\t\n\x05\x41UDIT\x10\x05*$\n\x05Queue\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x0e\n\nTASK_QUEUE\x10\x01*S\n\x0bModelStatus\x12\x06\n\x02OK\x10\x00\x12\x0f\n\x0bIN_PROGRESS\x10\x01\x12\x12\n\x0eIN_PROGRESS_OK\x10\x02\x12\n\n\x06\x46\x41ILED\x10\x03\x12\x0b\n\x07UNKNOWN\x10\x04*8\n\x04Role\x12\t\n\x05OTHER\x10\x00\x12\n\n\x06\x43LIENT\x10\x01\x12\x0c\n\x08\x43OMBINER\x10\x02\x12\x0b\n\x07REDUCER\x10\x03*J\n\x07\x43ommand\x12\x08\n\x04IDLE\x10\x00\x12\t\n\x05START\x10\x01\x12\t\n\x05PAUSE\x10\x02\x12\x08\n\x04STOP\x10\x03\x12\t\n\x05RESET\x10\x04\x12\n\n\x06REPORT\x10\x05*I\n\x10\x43onnectionStatus\x12\x11\n\rNOT_ACCEPTING\x10\x00\x12\r\n\tACCEPTING\x10\x01\x12\x13\n\x0fTRY_AGAIN_LATER\x10\x02\x32z\n\x0cModelService\x12\x33\n\x06Upload\x12\x12.fedn.ModelRequest\x1a\x13.fedn.ModelResponse(\x01\x12\x35\n\x08\x44ownload\x12\x12.fedn.ModelRequest\x1a\x13.fedn.ModelResponse0\x01\x32\xbb\x02\n\x07\x43ontrol\x12\x34\n\x05Start\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse\x12\x33\n\x04Stop\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse\x12\x44\n\x15\x46lushAggregationQueue\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse\x12<\n\rSetAggregator\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse\x12\x41\n\x12SetServerFunctions\x12\x14.fedn.ControlRequest\x1a\x15.fedn.ControlResponse2V\n\x07Reducer\x12K\n\x0eGetGlobalModel\x12\x1b.fedn.GetGlobalModelRequest\x1a\x1c.fedn.GetGlobalModelResponse2\xab\x03\n\tConnector\x12\x44\n\x14\x41llianceStatusStream\x12\x1c.fedn.ClientAvailableMessage\x1a\x0c.fedn.Status0\x01\x12*\n\nSendStatus\x12\x0c.fedn.Status\x1a\x0e.fedn.Response\x12?\n\x11ListActiveClients\x12\x18.fedn.ListClientsRequest\x1a\x10.fedn.ClientList\x12\x45\n\x10\x41\x63\x63\x65ptingClients\x12\x17.fedn.ConnectionRequest\x1a\x18.fedn.ConnectionResponse\x12\x30\n\rSendHeartbeat\x12\x0f.fedn.Heartbeat\x1a\x0e.fedn.Response\x12\x37\n\x0eReassignClient\x12\x15.fedn.ReassignRequest\x1a\x0e.fedn.Response\x12\x39\n\x0fReconnectClient\x12\x16.fedn.ReconnectRequest\x1a\x0e.fedn.Response2\xf7\x03\n\x08\x43ombiner\x12?\n\nTaskStream\x12\x1c.fedn.ClientAvailableMessage\x1a\x11.fedn.TaskRequest0\x01\x12\x34\n\x0fSendModelUpdate\x12\x11.fedn.ModelUpdate\x1a\x0e.fedn.Response\x12<\n\x13SendModelValidation\x12\x15.fedn.ModelValidation\x1a\x0e.fedn.Response\x12<\n\x13SendModelPrediction\x12\x15.fedn.ModelPrediction\x1a\x0e.fedn.Response\x12\x42\n\x16SendBackwardCompletion\x12\x18.fedn.BackwardCompletion\x1a\x0e.fedn.Response\x12\x34\n\x0fSendModelMetric\x12\x11.fedn.ModelMetric\x1a\x0e.fedn.Response\x12>\n\x14SendAttributeMessage\x12\x16.fedn.AttributeMessage\x1a\x0e.fedn.Response\x12>\n\x14SendTelemetryMessage\x12\x16.fedn.TelemetryMessage\x1a\x0e.fedn.Response2\xec\x03\n\x0f\x46unctionService\x12Z\n\x17HandleProvidedFunctions\x12\x1e.fedn.ProvidedFunctionsRequest\x1a\x1f.fedn.ProvidedFunctionsResponse\x12M\n\x12HandleClientConfig\x12\x19.fedn.ClientConfigRequest\x1a\x1a.fedn.ClientConfigResponse(\x01\x12T\n\x15HandleClientSelection\x12\x1c.fedn.ClientSelectionRequest\x1a\x1d.fedn.ClientSelectionResponse\x12\x43\n\x0eHandleMetadata\x12\x17.fedn.ClientMetaRequest\x1a\x18.fedn.ClientMetaResponse\x12G\n\x10HandleStoreModel\x12\x17.fedn.StoreModelRequest\x1a\x18.fedn.StoreModelResponse(\x01\x12J\n\x11HandleAggregation\x12\x18.fedn.AggregationRequest\x1a\x19.fedn.AggregationResponse0\x01\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'network.grpc.fedn_pb2', _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'fedn_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: DESCRIPTOR._loaded_options = None _globals['_PROVIDEDFUNCTIONSRESPONSE_AVAILABLEFUNCTIONSENTRY']._loaded_options = None _globals['_PROVIDEDFUNCTIONSRESPONSE_AVAILABLEFUNCTIONSENTRY']._serialized_options = b'8\001' - _globals['_STATUSTYPE']._serialized_start=3677 - _globals['_STATUSTYPE']._serialized_end=3829 - _globals['_LOGLEVEL']._serialized_start=3831 - _globals['_LOGLEVEL']._serialized_end=3907 - _globals['_QUEUE']._serialized_start=3909 - _globals['_QUEUE']._serialized_end=3945 - _globals['_MODELSTATUS']._serialized_start=3947 - _globals['_MODELSTATUS']._serialized_end=4030 - _globals['_ROLE']._serialized_start=4032 - _globals['_ROLE']._serialized_end=4088 - _globals['_COMMAND']._serialized_start=4090 - _globals['_COMMAND']._serialized_end=4164 - _globals['_CONNECTIONSTATUS']._serialized_start=4166 - _globals['_CONNECTIONSTATUS']._serialized_end=4239 - _globals['_RESPONSE']._serialized_start=98 - _globals['_RESPONSE']._serialized_end=156 - _globals['_STATUS']._serialized_start=159 - _globals['_STATUS']._serialized_end=400 - _globals['_TASKREQUEST']._serialized_start=403 - _globals['_TASKREQUEST']._serialized_end=619 - _globals['_MODELUPDATE']._serialized_start=622 - _globals['_MODELUPDATE']._serialized_end=813 - _globals['_MODELVALIDATION']._serialized_start=816 - _globals['_MODELVALIDATION']._serialized_end=1032 - _globals['_MODELPREDICTION']._serialized_start=1035 - _globals['_MODELPREDICTION']._serialized_end=1254 - _globals['_MODELMETRIC']._serialized_start=1257 - _globals['_MODELMETRIC']._serialized_end=1482 - _globals['_METRICELEM']._serialized_start=1484 - _globals['_METRICELEM']._serialized_end=1524 - _globals['_ATTRIBUTEMESSAGE']._serialized_start=1527 - _globals['_ATTRIBUTEMESSAGE']._serialized_end=1663 - _globals['_ATTRIBUTEELEM']._serialized_start=1665 - _globals['_ATTRIBUTEELEM']._serialized_end=1708 - _globals['_MODELREQUEST']._serialized_start=1711 - _globals['_MODELREQUEST']._serialized_end=1848 - _globals['_MODELRESPONSE']._serialized_start=1850 - _globals['_MODELRESPONSE']._serialized_end=1943 - _globals['_GETGLOBALMODELREQUEST']._serialized_start=1945 - _globals['_GETGLOBALMODELREQUEST']._serialized_end=2030 - _globals['_GETGLOBALMODELRESPONSE']._serialized_start=2032 - _globals['_GETGLOBALMODELRESPONSE']._serialized_end=2136 - _globals['_HEARTBEAT']._serialized_start=2138 - _globals['_HEARTBEAT']._serialized_end=2232 - _globals['_CLIENTAVAILABLEMESSAGE']._serialized_start=2234 - _globals['_CLIENTAVAILABLEMESSAGE']._serialized_end=2321 - _globals['_LISTCLIENTSREQUEST']._serialized_start=2323 - _globals['_LISTCLIENTSREQUEST']._serialized_end=2403 - _globals['_CLIENTLIST']._serialized_start=2405 - _globals['_CLIENTLIST']._serialized_end=2447 - _globals['_CLIENT']._serialized_start=2449 - _globals['_CLIENT']._serialized_end=2516 - _globals['_REASSIGNREQUEST']._serialized_start=2518 - _globals['_REASSIGNREQUEST']._serialized_end=2627 - _globals['_RECONNECTREQUEST']._serialized_start=2629 - _globals['_RECONNECTREQUEST']._serialized_end=2728 - _globals['_PARAMETER']._serialized_start=2730 - _globals['_PARAMETER']._serialized_end=2769 - _globals['_CONTROLREQUEST']._serialized_start=2771 - _globals['_CONTROLREQUEST']._serialized_end=2855 - _globals['_CONTROLRESPONSE']._serialized_start=2857 - _globals['_CONTROLRESPONSE']._serialized_end=2927 - _globals['_CONNECTIONREQUEST']._serialized_start=2929 - _globals['_CONNECTIONREQUEST']._serialized_end=2948 - _globals['_CONNECTIONRESPONSE']._serialized_start=2950 - _globals['_CONNECTIONRESPONSE']._serialized_end=3010 - _globals['_PROVIDEDFUNCTIONSREQUEST']._serialized_start=3012 - _globals['_PROVIDEDFUNCTIONSREQUEST']._serialized_end=3061 - _globals['_PROVIDEDFUNCTIONSRESPONSE']._serialized_start=3064 - _globals['_PROVIDEDFUNCTIONSRESPONSE']._serialized_end=3236 - _globals['_PROVIDEDFUNCTIONSRESPONSE_AVAILABLEFUNCTIONSENTRY']._serialized_start=3179 - _globals['_PROVIDEDFUNCTIONSRESPONSE_AVAILABLEFUNCTIONSENTRY']._serialized_end=3236 - _globals['_CLIENTCONFIGREQUEST']._serialized_start=3238 - _globals['_CLIENTCONFIGREQUEST']._serialized_end=3273 - _globals['_CLIENTCONFIGRESPONSE']._serialized_start=3275 - _globals['_CLIENTCONFIGRESPONSE']._serialized_end=3322 - _globals['_CLIENTSELECTIONREQUEST']._serialized_start=3324 - _globals['_CLIENTSELECTIONREQUEST']._serialized_end=3368 - _globals['_CLIENTSELECTIONRESPONSE']._serialized_start=3370 - _globals['_CLIENTSELECTIONRESPONSE']._serialized_end=3415 - _globals['_CLIENTMETAREQUEST']._serialized_start=3417 - _globals['_CLIENTMETAREQUEST']._serialized_end=3473 - _globals['_CLIENTMETARESPONSE']._serialized_start=3475 - _globals['_CLIENTMETARESPONSE']._serialized_end=3511 - _globals['_STOREMODELREQUEST']._serialized_start=3513 - _globals['_STOREMODELREQUEST']._serialized_end=3558 - _globals['_STOREMODELRESPONSE']._serialized_start=3560 - _globals['_STOREMODELRESPONSE']._serialized_end=3596 - _globals['_AGGREGATIONREQUEST']._serialized_start=3598 - _globals['_AGGREGATIONREQUEST']._serialized_end=3637 - _globals['_AGGREGATIONRESPONSE']._serialized_start=3639 - _globals['_AGGREGATIONRESPONSE']._serialized_end=3674 - _globals['_MODELSERVICE']._serialized_start=4241 - _globals['_MODELSERVICE']._serialized_end=4363 - _globals['_CONTROL']._serialized_start=4366 - _globals['_CONTROL']._serialized_end=4681 - _globals['_REDUCER']._serialized_start=4683 - _globals['_REDUCER']._serialized_end=4769 - _globals['_CONNECTOR']._serialized_start=4772 - _globals['_CONNECTOR']._serialized_end=5199 - _globals['_COMBINER']._serialized_start=5202 - _globals['_COMBINER']._serialized_end=5573 - _globals['_FUNCTIONSERVICE']._serialized_start=5576 - _globals['_FUNCTIONSERVICE']._serialized_end=6068 + _globals['_STATUSTYPE']._serialized_start=4060 + _globals['_STATUSTYPE']._serialized_end=4282 + _globals['_LOGLEVEL']._serialized_start=4284 + _globals['_LOGLEVEL']._serialized_end=4360 + _globals['_QUEUE']._serialized_start=4362 + _globals['_QUEUE']._serialized_end=4398 + _globals['_MODELSTATUS']._serialized_start=4400 + _globals['_MODELSTATUS']._serialized_end=4483 + _globals['_ROLE']._serialized_start=4485 + _globals['_ROLE']._serialized_end=4541 + _globals['_COMMAND']._serialized_start=4543 + _globals['_COMMAND']._serialized_end=4617 + _globals['_CONNECTIONSTATUS']._serialized_start=4619 + _globals['_CONNECTIONSTATUS']._serialized_end=4692 + _globals['_RESPONSE']._serialized_start=85 + _globals['_RESPONSE']._serialized_end=143 + _globals['_STATUS']._serialized_start=146 + _globals['_STATUS']._serialized_end=387 + _globals['_TASKREQUEST']._serialized_start=390 + _globals['_TASKREQUEST']._serialized_end=606 + _globals['_MODELUPDATE']._serialized_start=609 + _globals['_MODELUPDATE']._serialized_end=800 + _globals['_MODELVALIDATION']._serialized_start=803 + _globals['_MODELVALIDATION']._serialized_end=1019 + _globals['_MODELPREDICTION']._serialized_start=1022 + _globals['_MODELPREDICTION']._serialized_end=1241 + _globals['_BACKWARDCOMPLETION']._serialized_start=1244 + _globals['_BACKWARDCOMPLETION']._serialized_end=1452 + _globals['_MODELMETRIC']._serialized_start=1455 + _globals['_MODELMETRIC']._serialized_end=1680 + _globals['_METRICELEM']._serialized_start=1682 + _globals['_METRICELEM']._serialized_end=1722 + _globals['_ATTRIBUTEMESSAGE']._serialized_start=1725 + _globals['_ATTRIBUTEMESSAGE']._serialized_end=1861 + _globals['_ATTRIBUTEELEM']._serialized_start=1863 + _globals['_ATTRIBUTEELEM']._serialized_end=1906 + _globals['_TELEMETRYMESSAGE']._serialized_start=1909 + _globals['_TELEMETRYMESSAGE']._serialized_end=2046 + _globals['_TELEMETRYELEM']._serialized_start=2048 + _globals['_TELEMETRYELEM']._serialized_end=2091 + _globals['_MODELREQUEST']._serialized_start=2094 + _globals['_MODELREQUEST']._serialized_end=2231 + _globals['_MODELRESPONSE']._serialized_start=2233 + _globals['_MODELRESPONSE']._serialized_end=2326 + _globals['_GETGLOBALMODELREQUEST']._serialized_start=2328 + _globals['_GETGLOBALMODELREQUEST']._serialized_end=2413 + _globals['_GETGLOBALMODELRESPONSE']._serialized_start=2415 + _globals['_GETGLOBALMODELRESPONSE']._serialized_end=2519 + _globals['_HEARTBEAT']._serialized_start=2521 + _globals['_HEARTBEAT']._serialized_end=2615 + _globals['_CLIENTAVAILABLEMESSAGE']._serialized_start=2617 + _globals['_CLIENTAVAILABLEMESSAGE']._serialized_end=2704 + _globals['_LISTCLIENTSREQUEST']._serialized_start=2706 + _globals['_LISTCLIENTSREQUEST']._serialized_end=2786 + _globals['_CLIENTLIST']._serialized_start=2788 + _globals['_CLIENTLIST']._serialized_end=2830 + _globals['_CLIENT']._serialized_start=2832 + _globals['_CLIENT']._serialized_end=2899 + _globals['_REASSIGNREQUEST']._serialized_start=2901 + _globals['_REASSIGNREQUEST']._serialized_end=3010 + _globals['_RECONNECTREQUEST']._serialized_start=3012 + _globals['_RECONNECTREQUEST']._serialized_end=3111 + _globals['_PARAMETER']._serialized_start=3113 + _globals['_PARAMETER']._serialized_end=3152 + _globals['_CONTROLREQUEST']._serialized_start=3154 + _globals['_CONTROLREQUEST']._serialized_end=3238 + _globals['_CONTROLRESPONSE']._serialized_start=3240 + _globals['_CONTROLRESPONSE']._serialized_end=3310 + _globals['_CONNECTIONREQUEST']._serialized_start=3312 + _globals['_CONNECTIONREQUEST']._serialized_end=3331 + _globals['_CONNECTIONRESPONSE']._serialized_start=3333 + _globals['_CONNECTIONRESPONSE']._serialized_end=3393 + _globals['_PROVIDEDFUNCTIONSREQUEST']._serialized_start=3395 + _globals['_PROVIDEDFUNCTIONSREQUEST']._serialized_end=3444 + _globals['_PROVIDEDFUNCTIONSRESPONSE']._serialized_start=3447 + _globals['_PROVIDEDFUNCTIONSRESPONSE']._serialized_end=3619 + _globals['_PROVIDEDFUNCTIONSRESPONSE_AVAILABLEFUNCTIONSENTRY']._serialized_start=3562 + _globals['_PROVIDEDFUNCTIONSRESPONSE_AVAILABLEFUNCTIONSENTRY']._serialized_end=3619 + _globals['_CLIENTCONFIGREQUEST']._serialized_start=3621 + _globals['_CLIENTCONFIGREQUEST']._serialized_end=3656 + _globals['_CLIENTCONFIGRESPONSE']._serialized_start=3658 + _globals['_CLIENTCONFIGRESPONSE']._serialized_end=3705 + _globals['_CLIENTSELECTIONREQUEST']._serialized_start=3707 + _globals['_CLIENTSELECTIONREQUEST']._serialized_end=3751 + _globals['_CLIENTSELECTIONRESPONSE']._serialized_start=3753 + _globals['_CLIENTSELECTIONRESPONSE']._serialized_end=3798 + _globals['_CLIENTMETAREQUEST']._serialized_start=3800 + _globals['_CLIENTMETAREQUEST']._serialized_end=3856 + _globals['_CLIENTMETARESPONSE']._serialized_start=3858 + _globals['_CLIENTMETARESPONSE']._serialized_end=3894 + _globals['_STOREMODELREQUEST']._serialized_start=3896 + _globals['_STOREMODELREQUEST']._serialized_end=3941 + _globals['_STOREMODELRESPONSE']._serialized_start=3943 + _globals['_STOREMODELRESPONSE']._serialized_end=3979 + _globals['_AGGREGATIONREQUEST']._serialized_start=3981 + _globals['_AGGREGATIONREQUEST']._serialized_end=4020 + _globals['_AGGREGATIONRESPONSE']._serialized_start=4022 + _globals['_AGGREGATIONRESPONSE']._serialized_end=4057 + _globals['_MODELSERVICE']._serialized_start=4694 + _globals['_MODELSERVICE']._serialized_end=4816 + _globals['_CONTROL']._serialized_start=4819 + _globals['_CONTROL']._serialized_end=5134 + _globals['_REDUCER']._serialized_start=5136 + _globals['_REDUCER']._serialized_end=5222 + _globals['_CONNECTOR']._serialized_start=5225 + _globals['_CONNECTOR']._serialized_end=5652 + _globals['_COMBINER']._serialized_start=5655 + _globals['_COMBINER']._serialized_end=6158 + _globals['_FUNCTIONSERVICE']._serialized_start=6161 + _globals['_FUNCTIONSERVICE']._serialized_end=6653 # @@protoc_insertion_point(module_scope) diff --git a/fedn/network/grpc/fedn_pb2_grpc.py b/fedn/network/grpc/fedn_pb2_grpc.py index 70faab8db..c7d20089c 100644 --- a/fedn/network/grpc/fedn_pb2_grpc.py +++ b/fedn/network/grpc/fedn_pb2_grpc.py @@ -18,7 +18,7 @@ if _version_not_supported: raise RuntimeError( f'The grpc package installed is at version {GRPC_VERSION},' - + f' but the generated code in network/grpc/fedn_pb2_grpc.py depends on' + + f' but the generated code in fedn_pb2_grpc.py depends on' + f' grpcio>={GRPC_GENERATED_VERSION}.' + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' @@ -820,6 +820,11 @@ def __init__(self, channel): request_serializer=network_dot_grpc_dot_fedn__pb2.ModelPrediction.SerializeToString, response_deserializer=network_dot_grpc_dot_fedn__pb2.Response.FromString, _registered_method=True) + self.SendBackwardCompletion = channel.unary_unary( + '/fedn.Combiner/SendBackwardCompletion', + request_serializer=network_dot_grpc_dot_fedn__pb2.BackwardCompletion.SerializeToString, + response_deserializer=network_dot_grpc_dot_fedn__pb2.Response.FromString, + _registered_method=True) self.SendModelMetric = channel.unary_unary( '/fedn.Combiner/SendModelMetric', request_serializer=network_dot_grpc_dot_fedn__pb2.ModelMetric.SerializeToString, @@ -830,6 +835,11 @@ def __init__(self, channel): request_serializer=network_dot_grpc_dot_fedn__pb2.AttributeMessage.SerializeToString, response_deserializer=network_dot_grpc_dot_fedn__pb2.Response.FromString, _registered_method=True) + self.SendTelemetryMessage = channel.unary_unary( + '/fedn.Combiner/SendTelemetryMessage', + request_serializer=network_dot_grpc_dot_fedn__pb2.TelemetryMessage.SerializeToString, + response_deserializer=network_dot_grpc_dot_fedn__pb2.Response.FromString, + _registered_method=True) class CombinerServicer(object): @@ -860,6 +870,12 @@ def SendModelPrediction(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def SendBackwardCompletion(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def SendModelMetric(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) @@ -872,6 +888,12 @@ def SendAttributeMessage(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def SendTelemetryMessage(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_CombinerServicer_to_server(servicer, server): rpc_method_handlers = { @@ -895,6 +917,11 @@ def add_CombinerServicer_to_server(servicer, server): request_deserializer=network_dot_grpc_dot_fedn__pb2.ModelPrediction.FromString, response_serializer=network_dot_grpc_dot_fedn__pb2.Response.SerializeToString, ), + 'SendBackwardCompletion': grpc.unary_unary_rpc_method_handler( + servicer.SendBackwardCompletion, + request_deserializer=network_dot_grpc_dot_fedn__pb2.BackwardCompletion.FromString, + response_serializer=network_dot_grpc_dot_fedn__pb2.Response.SerializeToString, + ), 'SendModelMetric': grpc.unary_unary_rpc_method_handler( servicer.SendModelMetric, request_deserializer=network_dot_grpc_dot_fedn__pb2.ModelMetric.FromString, @@ -905,6 +932,11 @@ def add_CombinerServicer_to_server(servicer, server): request_deserializer=network_dot_grpc_dot_fedn__pb2.AttributeMessage.FromString, response_serializer=network_dot_grpc_dot_fedn__pb2.Response.SerializeToString, ), + 'SendTelemetryMessage': grpc.unary_unary_rpc_method_handler( + servicer.SendTelemetryMessage, + request_deserializer=network_dot_grpc_dot_fedn__pb2.TelemetryMessage.FromString, + response_serializer=network_dot_grpc_dot_fedn__pb2.Response.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'fedn.Combiner', rpc_method_handlers) @@ -1024,6 +1056,33 @@ def SendModelPrediction(request, metadata, _registered_method=True) + @staticmethod + def SendBackwardCompletion(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/fedn.Combiner/SendBackwardCompletion', + network_dot_grpc_dot_fedn__pb2.BackwardCompletion.SerializeToString, + network_dot_grpc_dot_fedn__pb2.Response.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def SendModelMetric(request, target, @@ -1078,6 +1137,33 @@ def SendAttributeMessage(request, metadata, _registered_method=True) + @staticmethod + def SendTelemetryMessage(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/fedn.Combiner/SendTelemetryMessage', + network_dot_grpc_dot_fedn__pb2.TelemetryMessage.SerializeToString, + network_dot_grpc_dot_fedn__pb2.Response.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + class FunctionServiceStub(object): """Missing associated documentation comment in .proto file.""" diff --git a/fedn/network/grpc/server.py b/fedn/network/grpc/server.py index 2848e143e..1fedb784e 100644 --- a/fedn/network/grpc/server.py +++ b/fedn/network/grpc/server.py @@ -26,20 +26,25 @@ def __init__(self, servicer, modelservicer, config: ServerConfig): set_log_stream(config.get("logfile", None)) # Keepalive settings: these detect if the client is alive - KEEPALIVE_TIME_MS = 60 * 1000 # send keepalive ping every 60 seconds - # wait 20 seconds for keepalive ping ack before considering connection dead - KEEPALIVE_TIMEOUT_MS = 20 * 1000 + KEEPALIVE_TIME_MS = 60 * 1000 # send keepalive ping every 60 second + # wait 30 seconds for keepalive ping ack before considering connection dead + KEEPALIVE_TIMEOUT_MS = 30 * 1000 # max idle time before server terminates the connection (5 minutes) MAX_CONNECTION_IDLE_MS = 5 * 60 * 1000 - + MAX_CONCURRENT_STREAMS = 10000 + HTTP2_MAX_PINGS_WITHOUT_DATA = 2 # limit clients to 2 pings without data + HTTP2_MIN_RECV_PING_INTERVAL_WITHOUT_DATA_MS = 10000 # require at least 10 seconds between client pings self.server = grpc.server( - futures.ThreadPoolExecutor(max_workers=350), + futures.ThreadPoolExecutor(max_workers=10000), interceptors=[JWTInterceptor()], options=[ ("grpc.keepalive_time_ms", KEEPALIVE_TIME_MS), ("grpc.keepalive_timeout_ms", KEEPALIVE_TIMEOUT_MS), ("grpc.max_connection_idle_ms", MAX_CONNECTION_IDLE_MS), - ], + ("grpc.http2.max_pings_without_data", HTTP2_MAX_PINGS_WITHOUT_DATA), + ("grpc.http2.min_recv_ping_interval_without_data_ms", HTTP2_MIN_RECV_PING_INTERVAL_WITHOUT_DATA_MS), + ("grpc.max_concurrent_streams", MAX_CONCURRENT_STREAMS), + ], ) self.certificate = None self.health_servicer = health.HealthServicer() diff --git a/fedn/network/storage/dbconnection.py b/fedn/network/storage/dbconnection.py index 6377dcbb2..4e8024750 100644 --- a/fedn/network/storage/dbconnection.py +++ b/fedn/network/storage/dbconnection.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.orm.session import Session as SessionClass -from fedn.network.storage.statestore.stores.analytic_store import AnalyticStore, MongoDBAnalyticStore +from fedn.common.log_config import logger from fedn.network.storage.statestore.stores.attribute_store import AttributeStore, MongoDBAttributeStore, SQLAttributeStore from fedn.network.storage.statestore.stores.client_store import ClientStore, MongoDBClientStore, SQLClientStore from fedn.network.storage.statestore.stores.combiner_store import CombinerStore, MongoDBCombinerStore, SQLCombinerStore @@ -26,6 +26,7 @@ from fedn.network.storage.statestore.stores.session_store import MongoDBSessionStore, SessionStore, SQLSessionStore from fedn.network.storage.statestore.stores.sql.shared import MyAbstractBase from fedn.network.storage.statestore.stores.status_store import MongoDBStatusStore, SQLStatusStore, StatusStore +from fedn.network.storage.statestore.stores.telemetry_store import MongoDBTelemetryStore, SQLTelemetryStore, TelemetryStore from fedn.network.storage.statestore.stores.validation_store import MongoDBValidationStore, SQLValidationStore, ValidationStore @@ -43,19 +44,31 @@ class DatabaseConnection: package_store: PackageStore model_store: ModelStore session_store: SessionStore - analytic_store: AnalyticStore metric_store: MetricStore attribute_store: AttributeStore - training_run_store: RunStore + telemetry_store: TelemetryStore + run_store: RunStore - def __init__(self, statestore_config, network_id): + def __init__(self, statestore_config, network_id, connect: bool = True): self.type: str = None self.mdb: Database = None self.Session: sessionmaker = None self.type = statestore_config["type"] + self.statestore_config = statestore_config + self.network_id = network_id + self._initialized = False + if connect: + self.initialize_connection() + + def initialize_connection(self): + if self._initialized: + logger.warning("DatabaseConnection is already initialized.") + return + self._initialized = True if self.type == "MongoDB": - mdb: Database = self._setup_mongo(statestore_config, network_id) + logger.info("Connecting to MongoDB") + mdb: Database = self._setup_mongo(self.statestore_config, self.network_id) client_store = MongoDBClientStore(mdb, "network.clients") validation_store = MongoDBValidationStore(mdb, "control.validations") @@ -66,14 +79,16 @@ def __init__(self, statestore_config, network_id): package_store = MongoDBPackageStore(mdb, "control.packages") model_store = MongoDBModelStore(mdb, "control.model") session_store = MongoDBSessionStore(mdb, "control.sessions") - analytic_store = MongoDBAnalyticStore(mdb, "control.analytics") metric_store = MongoDBMetricStore(mdb, "control.metrics") attribute_store = MongoDBAttributeStore(mdb, "control.attributes") - training_run_store = MongoDBRunStore(mdb, "control.training_runs") + telemetry_store = MongoDBTelemetryStore(mdb, "control.telemetry") + run_store = MongoDBRunStore(mdb, "control.training_runs") + self.mdb = mdb elif self.type in ["SQLite", "PostgreSQL"]: - Session = self._setup_sql(statestore_config) # noqa: N806 + logger.info("Connecting to SQL database") + Session = self._setup_sql(self.statestore_config) # noqa: N806 client_store = SQLClientStore(Session) validation_store = SQLValidationStore(Session) @@ -84,16 +99,15 @@ def __init__(self, statestore_config, network_id): package_store = SQLPackageStore(Session) model_store = SQLModelStore(Session) session_store = SQLSessionStore(Session) - analytic_store = None metric_store = SQLMetricStore(Session) attribute_store = SQLAttributeStore(Session) - training_run_store = SQLRunStore(Session) + telemetry_store = SQLTelemetryStore(Session) + run_store = SQLRunStore(Session) self.Session = Session else: raise ValueError("Unknown statestore type") - self.analytic_store: AnalyticStore = analytic_store self.client_store: ClientStore = client_store self.validation_store: ValidationStore = validation_store self.combiner_store: CombinerStore = combiner_store @@ -105,7 +119,8 @@ def __init__(self, statestore_config, network_id): self.session_store: SessionStore = session_store self.metric_store: SQLMetricStore = metric_store self.attribute_store: AttributeStore = attribute_store - self.training_run_store: RunStore = training_run_store + self.telemetry_store: TelemetryStore = telemetry_store + self.run_store: RunStore = run_store def _setup_mongo(self, statestore_config: dict, network_id: str) -> Database: mc = pymongo.MongoClient(**statestore_config["mongo_config"]) diff --git a/fedn/network/storage/s3/boto3repository.py b/fedn/network/storage/s3/boto3repository.py new file mode 100644 index 000000000..f9f1b2ae1 --- /dev/null +++ b/fedn/network/storage/s3/boto3repository.py @@ -0,0 +1,148 @@ +"""Module implementing Repository for Amazon S3 using boto3.""" + +import io +from typing import IO, List + +import boto3 +from botocore.exceptions import BotoCoreError, ClientError + +from fedn.common.log_config import logger +from fedn.network.storage.s3.base import RepositoryBase + + +class Boto3Repository(RepositoryBase): + """Class implementing Repository for Amazon S3 using boto3.""" + + def __init__(self, config: dict) -> None: + """Initialize object. + + :param config: Dictionary containing configuration for credentials and bucket names. + :type config: dict + """ + super().__init__() + self.name = "Boto3Repository" + + common_config = { + "region_name": config.get("storage_region", "eu-west-1"), + "endpoint_url": config.get("storage_endpoint", "http://minio:9000"), + "use_ssl": config.get("storage_secure_mode", True), + "verify": config.get("storage_verify_ssl", True), + } + + if "storage_access_key" in config and "storage_secret_key" in config: + self.s3_client = boto3.client( + "s3", + aws_access_key_id=config["storage_access_key"], + aws_secret_access_key=config["storage_secret_key"], + **common_config, + ) + else: + # Use default credentials (e.g., from a service account or environment variables) + self.s3_client = boto3.client("s3", **common_config) + logger.info(f"Using {self.name} for S3 storage.") + + def set_artifact(self, instance_name: str, instance: IO, bucket: str, is_file: bool = False) -> bool: + """Set object with name instance_name. + + :param instance_name: The name of the object + :type instance_name: str + :param instance: The object + :type instance: Any + :param bucket: The bucket name + :type bucket: str + :param is_file: Whether the instance is a file, defaults to False + :type is_file: bool, optional + :return: True if the artifact was set successfully + :rtype: bool + """ + try: + if is_file: + logger.info(f"Uploading file: {instance} to bucket: {bucket} with key: {instance_name}") + self.s3_client.upload_file(Filename=instance, Bucket=bucket, Key=instance_name) + else: + logger.info(f"Uploading object: {instance} to bucket: {bucket} with key: {instance_name}") + self.s3_client.put_object(Bucket=bucket, Key=instance_name, Body=instance) + return True + except (BotoCoreError, ClientError) as e: + logger.error(f"Failed to upload artifact: {instance_name} to bucket: {bucket}. Error: {e}") + raise Exception(f"Could not upload artifact: {e}") from e + + def get_artifact(self, instance_name: str, bucket: str) -> bytes: + """Retrieve object with name instance_name. + + :param instance_name: The name of the object to retrieve + :type instance_name: str + :param bucket: The bucket name + :type bucket: str + :return: The retrieved object + :rtype: bytes + """ + try: + response = self.s3_client.get_object(Bucket=bucket, Key=instance_name) + return response["Body"].read() + except (BotoCoreError, ClientError) as e: + logger.error(f"Failed to fetch artifact: {instance_name} from bucket: {bucket}. Error: {e}") + raise Exception(f"Could not fetch artifact: {e}") from e + + def get_artifact_stream(self, instance_name: str, bucket: str) -> io.BytesIO: + """Return a stream handler for object with name instance_name. + + :param instance_name: The name of the object + :type instance_name: str + :param bucket: The bucket name + :type bucket: str + :return: Stream handler for object instance_name + :rtype: io.BytesIO + """ + try: + response = self.s3_client.get_object(Bucket=bucket, Key=instance_name) + return io.BytesIO(response["Body"].read()) + except (BotoCoreError, ClientError) as e: + logger.error(f"Failed to fetch artifact stream: {instance_name} from bucket: {bucket}. Error: {e}") + raise Exception(f"Could not fetch artifact stream: {e}") from e + + def list_artifacts(self, bucket: str) -> List[str]: + """List all objects in bucket. + + :param bucket: Name of the bucket + :type bucket: str + :return: A list of object names + :rtype: List[str] + """ + try: + response = self.s3_client.list_objects_v2(Bucket=bucket) + return [obj["Key"] for obj in response.get("Contents", [])] + except (BotoCoreError, ClientError) as e: + logger.error(f"Failed to list artifacts in bucket: {bucket}. Error: {e}") + raise Exception(f"Could not list artifacts: {e}") from e + + def delete_artifact(self, instance_name: str, bucket: str) -> None: + """Delete object with name instance_name from bucket. + + :param instance_name: The object name + :type instance_name: str + :param bucket: Bucket to delete from + :type bucket: str + """ + try: + self.s3_client.delete_object(Bucket=bucket, Key=instance_name) + except (BotoCoreError, ClientError) as e: + logger.error(f"Failed to delete artifact: {instance_name} from bucket: {bucket}. Error: {e}") + raise Exception(f"Could not delete artifact: {e}") from e + + def create_bucket(self, bucket_name: str) -> None: + """Create a new bucket. If bucket exists, do nothing. + + :param bucket_name: The name of the bucket + :type bucket_name: str + """ + try: + self.s3_client.create_bucket(Bucket=bucket_name) + logger.info(f"Bucket {bucket_name} created successfully.") + except self.s3_client.exceptions.BucketAlreadyExists: + logger.info(f"Bucket {bucket_name} already exists. No new bucket was created.") + except self.s3_client.exceptions.BucketAlreadyOwnedByYou: + logger.info(f"Bucket {bucket_name} already owned by you. No new bucket was created.") + except (BotoCoreError, ClientError) as e: + logger.error(f"Failed to create bucket: {bucket_name}. Error: {e}") + raise Exception(f"Could not create bucket: {e}") from e diff --git a/fedn/network/storage/s3/repository.py b/fedn/network/storage/s3/repository.py index fdeb79b20..dc83907de 100644 --- a/fedn/network/storage/s3/repository.py +++ b/fedn/network/storage/s3/repository.py @@ -1,50 +1,75 @@ """Interface for storing model objects and compute packages in S3 compatible storage.""" import datetime -import os +import importlib import uuid from typing import Union +from fedn.common.config import FEDN_OBJECT_STORAGE_BUCKETS, FEDN_OBJECT_STORAGE_TYPE from fedn.common.log_config import logger -from fedn.network.storage.s3.miniorepository import MINIORepository -from fedn.network.storage.s3.saasrepository import SAASRepository class Repository: """Interface for storing model objects and compute packages in S3 compatible storage.""" - def __init__(self, config: dict, init_buckets: bool = True, storage_type: str = "MINIO") -> None: + def __init__(self, config: dict, init_buckets: bool = True, storage_type: str = None) -> None: """Initialize the repository. :param config: Configuration dictionary for credentials and bucket names. :type config: dict :param init_buckets: Whether to initialize buckets, defaults to True :type init_buckets: bool, optional - :param storage_type: Type of storage to use, defaults to "MINIO" - :type storage_type: str, optional + :param storage_type: Type of storage to use, defaults to an empty string which falls back to FEDN_OBJECT_STORAGE_TYPE """ - self.model_bucket = os.environ.get("FEDN_MODEL_BUCKET", config["storage_bucket"]) - self.context_bucket = os.environ.get("FEDN_CONTEXT_BUCKET", config["context_bucket"]) try: - self.prediction_bucket = os.environ.get("FEDN_PREDICTION_BUCKET", config["prediction_bucket"]) + self.model_bucket = config.get("storage_bucket", FEDN_OBJECT_STORAGE_BUCKETS["model"]) + self.context_bucket = config.get("context_bucket", FEDN_OBJECT_STORAGE_BUCKETS["context"]) + self.prediction_bucket = config.get("prediction_bucket", FEDN_OBJECT_STORAGE_BUCKETS["prediction"]) except KeyError: - self.prediction_bucket = "fedn-prediction" - - # TODO: Make a plug-in solution - storage_type = os.environ.get("FEDN_STORAGE_TYPE", storage_type) - if storage_type == "SAAS": - self.client = SAASRepository(config) - elif storage_type == "MINIO": - self.client = MINIORepository(config) - else: - # Default to MinIO. - self.client = MINIORepository(config) + logger.error("Missing required bucket names in configuration.") + raise ValueError("Missing required bucket names in configuration.") + + # Dynamically import the repository class based on storage_type + storage_type = (storage_type or FEDN_OBJECT_STORAGE_TYPE).upper() + self.client = self._load_repository(storage_type, config) if init_buckets: self.client.create_bucket(self.context_bucket) self.client.create_bucket(self.model_bucket) self.client.create_bucket(self.prediction_bucket) + def _load_repository(self, storage_type: str, config: dict): + """Dynamically load the repository class based on the storage type. + + :param storage_type: The type of storage (e.g., "MINIO", "BOTO3", "SAAS"). + :type storage_type: str + :param config: Configuration dictionary for the repository. + :type config: dict + :return: An instance of the repository class. + :rtype: object + """ + repository_mapping = { + "MINIO": "fedn.network.storage.s3.miniorepository.MINIORepository", + "BOTO3": "fedn.network.storage.s3.boto3repository.Boto3Repository", + "SAAS": "fedn.network.storage.s3.saasrepository.SAASRepository", + } + + if storage_type not in repository_mapping: + raise ValueError(f"Unsupported storage type: {storage_type}") + + module_path, class_name = repository_mapping[storage_type].rsplit(".", 1) + + try: + module = importlib.import_module(module_path) + repository_class = getattr(module, class_name) + return repository_class(config) + except ImportError as e: + logger.error(f"Failed to import module for storage type {storage_type}. Error: {e}") + raise ImportError(f"Could not import repository for storage type {storage_type}.") from e + except AttributeError as e: + logger.error(f"Failed to load class {class_name} from module {module_path}. Error: {e}") + raise AttributeError(f"Could not load repository class {class_name}.") from e + def get_model(self, model_id: str) -> bytes: """Retrieve a model with id model_id. diff --git a/fedn/network/storage/s3/saasrepository.py b/fedn/network/storage/s3/saasrepository.py index 9a734a831..0638d732e 100644 --- a/fedn/network/storage/s3/saasrepository.py +++ b/fedn/network/storage/s3/saasrepository.py @@ -1,20 +1,27 @@ -"""Implementation of the Repository interface for SaaS deployment.""" +"""Implementation of the Repository interface for SaaS deployment using boto3.""" import io import os from typing import IO, List -from minio import Minio -from minio.error import InvalidResponseError - +import boto3 +from botocore.config import Config +from botocore.exceptions import BotoCoreError, ClientError + +from fedn.common.config import ( + FEDN_OBJECT_STORAGE_ACCESS_KEY, + FEDN_OBJECT_STORAGE_ENDPOINT, + FEDN_OBJECT_STORAGE_REGION, + FEDN_OBJECT_STORAGE_SECRET_KEY, + FEDN_OBJECT_STORAGE_SECURE_MODE, + FEDN_OBJECT_STORAGE_VERIFY_SSL, +) from fedn.common.log_config import logger from fedn.network.storage.s3.base import RepositoryBase class SAASRepository(RepositoryBase): - """Class implementing Repository for SaaS deployment.""" - - client = None + """Class implementing Repository for SaaS deployment using boto3.""" def __init__(self, config: dict) -> None: """Initialize object. @@ -24,25 +31,43 @@ def __init__(self, config: dict) -> None: """ super().__init__() self.name = "SAASRepository" - self.project_slug = os.environ.get("FEDN_JWT_CUSTOM_CLAIM_VALUE") - - # Check environment variables first. If they are not set, then use values from config file. - access_key = os.environ.get("FEDN_ACCESS_KEY", config["storage_access_key"]) - secret_key = os.environ.get("FEDN_SECRET_KEY", config["storage_secret_key"]) - storage_hostname = os.environ.get("FEDN_STORAGE_HOSTNAME", config["storage_hostname"]) - storage_port = os.environ.get("FEDN_STORAGE_PORT", config["storage_port"]) - storage_secure_mode = os.environ.get("FEDN_STORAGE_SECURE_MODE", config["storage_secure_mode"]) - storage_region = os.environ.get("FEDN_STORAGE_REGION") or config.get("storage_region", "auto") - - storage_secure_mode = storage_secure_mode.lower() == "true" - - self.client = Minio( - f"{storage_hostname}:{storage_port}", - access_key=access_key, - secret_key=secret_key, - secure=storage_secure_mode, - region=storage_region, - ) + self.project_slug = os.environ.get("FEDN_JWT_CUSTOM_CLAIM_VALUE", "default_project") + + access_key = config.get("storage_access_key", FEDN_OBJECT_STORAGE_ACCESS_KEY) + secret_key = config.get("storage_secret_key", FEDN_OBJECT_STORAGE_SECRET_KEY) + endpoint_url = config.get("storage_endpoint", FEDN_OBJECT_STORAGE_ENDPOINT) + region_name = config.get("storage_region", FEDN_OBJECT_STORAGE_REGION) + use_ssl = config.get("storage_secure_mode", FEDN_OBJECT_STORAGE_SECURE_MODE) + use_ssl = use_ssl.lower() == "true" if isinstance(use_ssl, str) else use_ssl + verify_ssl = config.get("storage_verify_ssl", FEDN_OBJECT_STORAGE_VERIFY_SSL) + verify_ssl = verify_ssl.lower() == "true" if isinstance(verify_ssl, str) else verify_ssl + + # Initialize the boto3 client + common_config = { + "endpoint_url": endpoint_url, + "region_name": region_name, + "use_ssl": use_ssl, + "verify": verify_ssl, + } + logger.debug(f"Connection parameters: {common_config}") + + if access_key and secret_key: + # Use provided credentials + self.s3_client = boto3.client( + "s3", + aws_access_key_id=access_key, + aws_secret_access_key=secret_key, + config=Config(request_checksum_calculation="when_required", response_checksum_validation="when_required"), + **common_config, + ) + logger.info(f"Using {self.name} with provided credentials for SaaS storage.") + else: + # Use default credentials (e.g., IAM roles, service accounts, or environment variables) + self.s3_client = boto3.client( + "s3", config=Config(request_checksum_calculation="when_required", response_checksum_validation="when_required"), **common_config + ) + + logger.info(f"Using {self.name} with default credentials for SaaS storage.") def set_artifact(self, instance_name: str, instance: IO, bucket: str, is_file: bool = False) -> bool: """Set object with name instance_name. @@ -63,14 +88,15 @@ def set_artifact(self, instance_name: str, instance: IO, bucket: str, is_file: b try: if is_file: - self.client.fput_object(bucket, instance_name, instance) + logger.info(f"Uploading file: {instance} to bucket: {bucket} with key: {instance_name}") + self.s3_client.upload_file(Filename=instance, Bucket=bucket, Key=instance_name) else: - self.client.put_object(bucket, instance_name, io.BytesIO(instance), len(instance)) - except Exception as e: + logger.info("Uploading object: {instance} to bucket: {bucket} with key: {instance_name}") + self.s3_client.put_object(Bucket=bucket, Key=instance_name, Body=instance) + return True + except (BotoCoreError, ClientError) as e: logger.error(f"Failed to upload artifact: {instance_name} to bucket: {bucket}. Error: {e}") - raise Exception(f"Could not load data into bytes: {e}") from e - - return True + raise Exception(f"Could not upload artifact: {e}") from e def get_artifact(self, instance_name: str, bucket: str) -> bytes: """Retrieve object with name instance_name. @@ -86,14 +112,11 @@ def get_artifact(self, instance_name: str, bucket: str) -> bytes: logger.info(f"Getting artifact: {instance_name} from bucket: {bucket}") try: - data = self.client.get_object(bucket, instance_name) - return data.read() - except Exception as e: + response = self.s3_client.get_object(Bucket=bucket, Key=instance_name) + return response["Body"].read() + except (BotoCoreError, ClientError) as e: logger.error(f"Failed to fetch artifact: {instance_name} from bucket: {bucket}. Error: {e}") - raise Exception(f"Could not fetch data from bucket: {e}") from e - finally: - data.close() - data.release_conn() + raise Exception(f"Could not fetch artifact: {e}") from e def get_artifact_stream(self, instance_name: str, bucket: str) -> io.BytesIO: """Return a stream handler for object with name instance_name. @@ -109,11 +132,11 @@ def get_artifact_stream(self, instance_name: str, bucket: str) -> io.BytesIO: logger.info(f"Getting artifact stream: {instance_name} from bucket: {bucket}") try: - data = self.client.get_object(bucket, instance_name) - return data - except Exception as e: + response = self.s3_client.get_object(Bucket=bucket, Key=instance_name) + return io.BytesIO(response["Body"].read()) + except (BotoCoreError, ClientError) as e: logger.error(f"Failed to fetch artifact stream: {instance_name} from bucket: {bucket}. Error: {e}") - raise Exception(f"Could not fetch data from bucket: {e}") from e + raise Exception(f"Could not fetch artifact stream: {e}") from e def list_artifacts(self, bucket: str) -> List[str]: """List all objects in bucket. @@ -127,30 +150,31 @@ def list_artifacts(self, bucket: str) -> List[str]: objects = [] try: - objs = self.client.list_objects(bucket, prefix=self.project_slug) - for obj in objs: - objects.append(obj.object_name) - except Exception as err: - logger.error(f"Failed to list artifacts in bucket: {bucket}. Error: {err}") - raise Exception(f"Could not list models in bucket: {bucket}") from err + response = self.s3_client.list_objects_v2(Bucket=bucket, Prefix=self.project_slug) + for obj in response.get("Contents", []): + objects.append(obj["Key"]) + except (BotoCoreError, ClientError) as e: + logger.error(f"Failed to list artifacts in bucket: {bucket}. Error: {e}") + raise Exception(f"Could not list artifacts: {e}") from e return objects def delete_artifact(self, instance_name: str, bucket: str) -> None: - """Delete object with name instance_name from buckets. + """Delete object with name instance_name from bucket. :param instance_name: The object name :type instance_name: str - :param bucket: Buckets to delete from + :param bucket: Bucket to delete from :type bucket: str """ instance_name = f"{self.project_slug}/{instance_name}" logger.info(f"Deleting artifact: {instance_name} from bucket: {bucket}") try: - self.client.remove_object(bucket, instance_name) - except InvalidResponseError as err: - logger.error(f"Could not delete artifact: {instance_name}. Error: {err}") + self.s3_client.delete_object(Bucket=bucket, Key=instance_name) + except (BotoCoreError, ClientError) as e: + logger.error(f"Failed to delete artifact: {instance_name} from bucket: {bucket}. Error: {e}") + raise Exception(f"Could not delete artifact: {e}") from e def create_bucket(self, bucket_name: str) -> None: """Create a new bucket. If bucket exists, do nothing. @@ -161,8 +185,23 @@ def create_bucket(self, bucket_name: str) -> None: logger.info(f"Creating bucket: {bucket_name}") try: - if not self.client.bucket_exists(bucket_name): - self.client.make_bucket(bucket_name) - except InvalidResponseError as err: - logger.error(f"Failed to create bucket: {bucket_name}. Error: {err}") - raise + # Check if the bucket already exists + try: + self.s3_client.head_bucket(Bucket=bucket_name) + logger.info(f"Bucket {bucket_name} already exists. No action needed.") + return + except self.s3_client.exceptions.ClientError as e: + if e.response["Error"]["Code"] != "404": + logger.error(f"Error checking bucket {bucket_name}: {e}") + raise + + # Create the bucket if it does not exist + self.s3_client.create_bucket(Bucket=bucket_name) + except self.s3_client.exceptions.BucketAlreadyExists: + logger.info(f"Bucket {bucket_name} already exists.") + except self.s3_client.exceptions.BucketAlreadyOwnedByYou: + logger.info(f"Bucket {bucket_name} already owned by you. No action needed.") + except (BotoCoreError, ClientError) as e: + logger.error(f"Failed to create bucket: {bucket_name}. Error: {e}") + raise Exception(f"Could not create bucket: {e}") from e + logger.info(f"Bucket {bucket_name} created successfully.") diff --git a/fedn/network/storage/statestore/stores/__init__.py b/fedn/network/storage/statestore/stores/__init__.py index 38459adf9..673ff6c9b 100644 --- a/fedn/network/storage/statestore/stores/__init__.py +++ b/fedn/network/storage/statestore/stores/__init__.py @@ -1,4 +1,3 @@ -from fedn.network.storage.statestore.stores.analytic_store import AnalyticStore from fedn.network.storage.statestore.stores.client_store import ClientStore from fedn.network.storage.statestore.stores.combiner_store import CombinerStore from fedn.network.storage.statestore.stores.metric_store import MetricStore @@ -11,7 +10,6 @@ from fedn.network.storage.statestore.stores.validation_store import ValidationStore __all__ = [ - "AnalyticStore", "ClientStore", "CombinerStore", "MetricStore", diff --git a/fedn/network/storage/statestore/stores/analytic_store.py b/fedn/network/storage/statestore/stores/analytic_store.py deleted file mode 100644 index 1b27f551b..000000000 --- a/fedn/network/storage/statestore/stores/analytic_store.py +++ /dev/null @@ -1,41 +0,0 @@ -from datetime import datetime, timedelta, timezone -from typing import Dict, List - -import pymongo -from pymongo.database import Database - -from fedn.network.storage.statestore.stores.dto.analytic import AnalyticDTO -from fedn.network.storage.statestore.stores.shared import SortOrder -from fedn.network.storage.statestore.stores.store import MongoDBStore, Store, from_document - - -class AnalyticStore(Store[AnalyticDTO]): - pass - - -class MongoDBAnalyticStore(AnalyticStore, MongoDBStore[AnalyticDTO]): - def __init__(self, database: Database, collection: str): - super().__init__(database, collection, "id") - self.database[self.collection].create_index([("sender_id", pymongo.DESCENDING)]) - - def add(self, item: AnalyticDTO) -> AnalyticDTO: - analytic = super().add(item) - self._delete_old_records(analytic.sender_id) - return analytic - - def _delete_old_records(self, sender_id: str) -> int: - time_threshold = datetime.now(timezone.utc) - timedelta(minutes=5) - - result = self.database[self.collection].delete_many({"sender_id": sender_id, "committed_at": {"$lt": time_threshold}}) - return result.deleted_count - - def list(self, limit: int, skip: int, sort_key: str, sort_order=SortOrder.DESCENDING, **kwargs) -> List[AnalyticDTO]: - return super().list(limit, skip, sort_key or "committed_at", sort_order, **kwargs) - - def _document_from_dto(self, item: AnalyticDTO) -> Dict: - item_dict = item.to_db(exclude_unset=False) - return item_dict - - def _dto_from_document(self, document: Dict) -> AnalyticDTO: - item = from_document(document) - return AnalyticDTO().patch_with(item, throw_on_extra_keys=False) diff --git a/fedn/network/storage/statestore/stores/attribute_store.py b/fedn/network/storage/statestore/stores/attribute_store.py index fa4785013..506d9f661 100644 --- a/fedn/network/storage/statestore/stores/attribute_store.py +++ b/fedn/network/storage/statestore/stores/attribute_store.py @@ -9,7 +9,7 @@ class AttributeStore(Store[AttributeDTO]): - def get_attributes_for_client(self, client_id: str) -> List[AttributeDTO]: + def get_current_attributes_for_client(self, client_id: str) -> List[AttributeDTO]: """Get all attributes for a specific client. This method returns the most recent attributes for the given client_id. diff --git a/fedn/network/storage/statestore/stores/dto/__init__.py b/fedn/network/storage/statestore/stores/dto/__init__.py index af3579dac..2b4632648 100644 --- a/fedn/network/storage/statestore/stores/dto/__init__.py +++ b/fedn/network/storage/statestore/stores/dto/__init__.py @@ -1,6 +1,5 @@ """DTOs for the StateStore.""" -from fedn.network.storage.statestore.stores.dto.analytic import AnalyticDTO from fedn.network.storage.statestore.stores.dto.client import ClientDTO from fedn.network.storage.statestore.stores.dto.combiner import CombinerDTO from fedn.network.storage.statestore.stores.dto.metric import MetricDTO @@ -23,7 +22,6 @@ "PredictionDTO", "NodeDTO", "MetricDTO", - "AnalyticDTO", "RoundDTO", "StatusDTO", "ValidationDTO", diff --git a/fedn/network/storage/statestore/stores/dto/analytic.py b/fedn/network/storage/statestore/stores/dto/analytic.py deleted file mode 100644 index 80bd05e9f..000000000 --- a/fedn/network/storage/statestore/stores/dto/analytic.py +++ /dev/null @@ -1,19 +0,0 @@ -from fedn.network.storage.statestore.stores.dto.shared import BaseDTO, Field, PrimaryID - - -class AnalyticDTO(BaseDTO): - id: str = PrimaryID(None) - sender_id: str = Field(None) - sender_role: str = Field(None) - memory_utilisation: float = Field(None) - cpu_utilisation: float = Field(None) - - -# valid, msg = _validate_analytic(item_dict) - -# def _validate_analytic(analytic: dict) -> Tuple[bool, str]: -# if "sender_id" not in analytic: -# return False, "sender_id is required" -# if "sender_role" not in analytic or analytic["sender_role"] not in ["combiner", "client"]: -# return False, "sender_role must be either 'combiner' or 'client'" -# return analytic, "" diff --git a/fedn/network/storage/statestore/stores/dto/round.py b/fedn/network/storage/statestore/stores/dto/round.py index 9dc03cca7..f80af0646 100644 --- a/fedn/network/storage/statestore/stores/dto/round.py +++ b/fedn/network/storage/statestore/stores/dto/round.py @@ -12,6 +12,7 @@ class RoundConfigDTO(SessionConfigDTO): rounds: int = Field(None) round_id: str = Field(None) client_settings: Optional[dict] = Field({}) + is_sl_inference: Optional[bool] = Field(False) class RoundReduceDTO(DictDTO): diff --git a/fedn/network/storage/statestore/stores/dto/run.py b/fedn/network/storage/statestore/stores/dto/run.py index 3d588e79b..b9fae9f53 100644 --- a/fedn/network/storage/statestore/stores/dto/run.py +++ b/fedn/network/storage/statestore/stores/dto/run.py @@ -7,7 +7,7 @@ class RunDTO(BaseDTO): """Training run data transfer object.""" - training_run_id: Optional[str] = PrimaryID(None) + run_id: Optional[str] = PrimaryID(None) session_id: str = Field(None) model_id: str = Field(None) completed_at_model_id: Optional[str] = Field(None) # active model id when training run was completed diff --git a/fedn/network/storage/statestore/stores/dto/shared.py b/fedn/network/storage/statestore/stores/dto/shared.py index 278e5acab..a99ca554e 100644 --- a/fedn/network/storage/statestore/stores/dto/shared.py +++ b/fedn/network/storage/statestore/stores/dto/shared.py @@ -350,6 +350,7 @@ class BaseDTO(DictDTO): """BaseDTO for Data Transfer Objects.""" committed_at: datetime = Field(None) + updated_at: datetime = Field(None) @property def primary_id(self) -> str: @@ -371,7 +372,7 @@ def primary_key(self) -> str: raise AttributeError(f"{self.__class__.__name__} has no field of type PrimaryID") def _is_field_optional(self, key): - return super()._is_field_optional(key) or key == "committed_at" + return super()._is_field_optional(key) or key in ["committed_at", "updated_at"] class NodeDTO(DictDTO): diff --git a/fedn/network/storage/statestore/stores/dto/telemetry.py b/fedn/network/storage/statestore/stores/dto/telemetry.py new file mode 100644 index 000000000..bad338ba4 --- /dev/null +++ b/fedn/network/storage/statestore/stores/dto/telemetry.py @@ -0,0 +1,15 @@ +from datetime import datetime +from typing import Optional + +from fedn.network.storage.statestore.stores.dto.shared import BaseDTO, Field, NodeDTO, PrimaryID + + +class TelemetryDTO(BaseDTO): + telemetry_id: str = PrimaryID(None) + + key: str = Field(None) + value: float = Field(None) + + timestamp: Optional[datetime] = Field(None) + + sender: NodeDTO = Field(NodeDTO()) diff --git a/fedn/network/storage/statestore/stores/model_store.py b/fedn/network/storage/statestore/stores/model_store.py index 5f5f01450..d0846f335 100644 --- a/fedn/network/storage/statestore/stores/model_store.py +++ b/fedn/network/storage/statestore/stores/model_store.py @@ -1,4 +1,5 @@ from abc import abstractmethod +from datetime import datetime from typing import Dict, List from pymongo.database import Database @@ -59,6 +60,7 @@ def update(self, item: ModelDTO) -> ModelDTO: item.check_validity() item_dict = self._document_from_dto(item) id = item_dict[self.primary_key] + item_dict["updated_at"] = datetime.now() result = self.database[self.collection].update_one({self.primary_key: id, "key": "models"}, {"$set": item_dict}) if result.matched_count == 1: document = self.database[self.collection].find_one({self.primary_key: id, "key": "models"}) diff --git a/fedn/network/storage/statestore/stores/round_store.py b/fedn/network/storage/statestore/stores/round_store.py index b1d2fff1f..5acd3b4df 100644 --- a/fedn/network/storage/statestore/stores/round_store.py +++ b/fedn/network/storage/statestore/stores/round_store.py @@ -36,7 +36,10 @@ def get_latest_round_id(self) -> int: return int(obj["round_id"]) else: return 0 - + + def update_one(self, *args, **kwargs): + return self.database[self.collection].update_one(*args, **kwargs) + def _document_from_dto(self, item: RoundDTO) -> Dict: return item.to_db(exclude_unset=False) @@ -119,6 +122,7 @@ def _dto_from_orm_model(self, item: RoundModel) -> RoundDTO: orm_dict["round_config"] = from_orm_model(item.round_config, RoundConfigModel) del orm_dict["round_config"]["id"] del orm_dict["round_config"]["committed_at"] + del orm_dict["round_config"]["updated_at"] if item.combiners is not None: orm_dict["combiners"] = self._combiners_to_dict_list(item.combiners) @@ -132,6 +136,7 @@ def _dto_from_orm_model(self, item: RoundModel) -> RoundDTO: } del orm_dict["round_data"]["id"] del orm_dict["round_data"]["committed_at"] + del orm_dict["round_data"]["updated_at"] orm_dict["round_id"] = orm_dict.pop("id") del orm_dict["round_config_id"] @@ -152,15 +157,18 @@ def _combiners_to_dict_list(self, combiners: List[RoundCombinerModel]): } del c_dict["data"]["id"] del c_dict["data"]["committed_at"] + del c_dict["data"]["updated_at"] if c.round_config is not None: c_dict["config"] = from_orm_model(c.round_config, RoundConfigModel) c_dict["config"]["_job_id"] = c_dict.pop("config_job_id") del c_dict["config"]["id"] del c_dict["config"]["committed_at"] + del c_dict["config"]["updated_at"] del c_dict["id"] del c_dict["committed_at"] + del c_dict["updated_at"] del c_dict["round_config_id"] del c_dict["data_id"] del c_dict["parent_round_id"] diff --git a/fedn/network/storage/statestore/stores/run_store.py b/fedn/network/storage/statestore/stores/run_store.py index c1f52fb25..75b9f095a 100644 --- a/fedn/network/storage/statestore/stores/run_store.py +++ b/fedn/network/storage/statestore/stores/run_store.py @@ -13,7 +13,7 @@ class RunStore(Store[RunDTO]): class MongoDBRunStore(RunStore, MongoDBStore[RunDTO]): def __init__(self, database: Database, collection: str): - super().__init__(database, collection, "training_run_id") + super().__init__(database, collection, "run_id") def _document_from_dto(self, item: RunDTO) -> Dict: item_dict = item.to_db(exclude_unset=False) @@ -29,16 +29,16 @@ def update(self, item: RunDTO) -> RunDTO: class SQLRunStore(RunStore, SQLStore[RunDTO, RunModel]): def __init__(self, session): - super().__init__(session, RunModel, "training_run_id") + super().__init__(session, RunModel, "run_id") def _update_orm_model_from_dto(self, entity: RunModel, item: RunDTO): item_dict = item.to_db(exclude_unset=False) - item_dict["id"] = item_dict.pop("training_run_id", None) + item_dict["id"] = item_dict.pop("run_id", None) for key, value in item_dict.items(): setattr(entity, key, value) return entity def _dto_from_orm_model(self, item: RunModel) -> RunDTO: orm_dict = from_orm_model(item, RunModel) - orm_dict["training_run_id"] = orm_dict.pop("id") + orm_dict["run_id"] = orm_dict.pop("id") return RunDTO().populate_with(orm_dict) diff --git a/fedn/network/storage/statestore/stores/session_store.py b/fedn/network/storage/statestore/stores/session_store.py index 10d007bd6..fd06105d6 100644 --- a/fedn/network/storage/statestore/stores/session_store.py +++ b/fedn/network/storage/statestore/stores/session_store.py @@ -61,6 +61,7 @@ def _dto_from_orm_model(self, item: SessionModel) -> SessionDTO: session_config_dict.pop("id") session_config_dict.pop("committed_at") + session_config_dict.pop("updated_at") session_dict.pop("session_config_id") return SessionDTO().populate_with(session_dict) diff --git a/fedn/network/storage/statestore/stores/sql/shared.py b/fedn/network/storage/statestore/stores/sql/shared.py index 9d7939c58..ba00a273d 100644 --- a/fedn/network/storage/statestore/stores/sql/shared.py +++ b/fedn/network/storage/statestore/stores/sql/shared.py @@ -30,6 +30,7 @@ class MyAbstractBase(Base): id: Mapped[str] = mapped_column(primary_key=True, default=lambda: str(uuid.uuid4())) committed_at: Mapped[datetime] = mapped_column(default=datetime.now) + updated_at: Mapped[datetime] = mapped_column(default=datetime.now, onupdate=datetime.now) class SessionConfigModel(MyAbstractBase): @@ -97,6 +98,7 @@ class RoundConfigModel(MyAbstractBase): round_id: Mapped[str] rounds: Mapped[int] client_settings: Mapped[Optional[Dict]] = mapped_column(JSON) + is_sl_inference: Mapped[bool] class RoundCombinerDataModel(MyAbstractBase): @@ -275,3 +277,16 @@ class RunModel(MyAbstractBase): round_timeout: Mapped[int] rounds: Mapped[Optional[int]] completed_at: Mapped[Optional[datetime]] + + +class TelemetryModel(MyAbstractBase): + __tablename__ = "telemetry" + + key: Mapped[str] = mapped_column(String(255)) + value: Mapped[float] + + timestamp: Mapped[Optional[datetime]] + + sender_name: Mapped[str] + sender_role: Mapped[str] + sender_client_id: Mapped[Optional[str]] diff --git a/fedn/network/storage/statestore/stores/store.py b/fedn/network/storage/statestore/stores/store.py index a385d1f54..1942de90b 100644 --- a/fedn/network/storage/statestore/stores/store.py +++ b/fedn/network/storage/statestore/stores/store.py @@ -143,7 +143,9 @@ def add(self, item: DTO) -> DTO: elif self.database[self.collection].find_one({self.primary_key: item_dict[self.primary_key]}): raise Exception(f"Entity with id {item_dict[self.primary_key]} already exists") - item_dict["committed_at"] = datetime.now() + current_time = datetime.now() + item_dict["committed_at"] = current_time + item_dict["updated_at"] = current_time self.database[self.collection].insert_one(item_dict) document = self.database[self.collection].find_one({self.primary_key: item_dict[self.primary_key]}) @@ -156,6 +158,7 @@ def update(self, item: DTO) -> DTO: def mongo_update(self, item: DTO) -> DTO: item.check_validity() item_dict = self._document_from_dto(item) + item_dict["updated_at"] = datetime.now() id = item_dict[self.primary_key] result = self.database[self.collection].update_one({self.primary_key: id}, {"$set": item_dict}) if result.matched_count == 1: @@ -167,6 +170,55 @@ def delete(self, id: str) -> bool: result = self.database[self.collection].delete_one({self.primary_key: id}) return result.deleted_count == 1 + def _parse_mongo_filters(self, query_args): + """Convert URL query parameters into PyMongo filter dict.""" + mongo_filter = {} + operator_map = { + "gt": "$gt", + "lt": "$lt", + "gte": "$gte", + "lte": "$lte", + "ne": "$ne", + "eq": "$eq", # fallback + } + + for param, raw_value in query_args.items(): + # Handle __ syntax (e.g. field__gte) + if "__" in param: + field, op = param.split("__", 1) + mongo_op = operator_map.get(op) + if not mongo_op: + continue # Unknown operator, skip + + # Handle 'null' and convert types + value = self._parse_value(raw_value) + mongo_filter.setdefault(field, {})[mongo_op] = value + + else: + # Equality check (or null) + value = self._parse_value(raw_value) + mongo_filter[param] = value + + return mongo_filter + + def _parse_value(self, value): + """Helper to parse values to appropriate types.""" + if value.lower() == "null": + return None + # Try to convert to number + try: + if "." in value: + return float(value) + return int(value) + except ValueError: + pass + # Try to convert to datetime + try: + return datetime.fromisoformat(value) + except ValueError: + pass + return value # default to string + def list(self, limit: int = 0, skip: int = 0, sort_key: str = None, sort_order=SortOrder.DESCENDING, **kwargs) -> List[DTO]: _sort_order = sort_order or SortOrder.DESCENDING if _sort_order == SortOrder.DESCENDING: @@ -176,6 +228,8 @@ def list(self, limit: int = 0, skip: int = 0, sort_key: str = None, sort_order=S else: raise ValueError(f"Invalid sort order: {_sort_order}") + kwargs = self._parse_mongo_filters(kwargs) + if sort_key and sort_key != "committed_at": cursor = self.database[self.collection].find(kwargs).sort({sort_key: _sort_order, "committed_at": _sort_order}).skip(skip or 0).limit(limit or 0) else: @@ -184,6 +238,7 @@ def list(self, limit: int = 0, skip: int = 0, sort_key: str = None, sort_order=S return [self._dto_from_document(document) for document in cursor] def count(self, **kwargs) -> int: + kwargs = self._parse_mongo_filters(kwargs) return self.database[self.collection].count_documents(kwargs) @abstractmethod @@ -259,16 +314,64 @@ def delete(self, id: str) -> bool: return True + def _build_filters(self, **kwargs): + filters = [] + + operator_map = { + "__gt": lambda col, val: col > val, + "__lt": lambda col, val: col < val, + "__gte": lambda col, val: col >= val, + "__lte": lambda col, val: col <= val, + "__ne": lambda col, val: col != val, + "__eq": lambda col, val: col == val, + } + + def parse_value(value): + if isinstance(value, str) and value.lower() == "null": + return None + try: + return int(value) + except ValueError: + pass + try: + return float(value) + except ValueError: + pass + return value + + for key, value in kwargs.items(): + value = parse_value(value) + + for suffix, op_fn in operator_map.items(): + if key.endswith(suffix): + base_key = key[: -len(suffix)] + if not hasattr(self.SQLModel, base_key): + continue + col = getattr(self.SQLModel, base_key) + filters.append(op_fn(col, value)) + break + else: + if key == self.primary_key: + key = "id" + elif not hasattr(self.SQLModel, key): + continue + col = getattr(self.SQLModel, key) + if value is None: + filters.append(col.is_(None)) + else: + filters.append(col == value) + + return filters + def list(self, limit=0, skip=0, sort_key=None, sort_order=SortOrder.DESCENDING, **kwargs) -> List[DTO]: with self.Session() as session: stmt = select(self.SQLModel) if sort_key == self.primary_key: sort_key = "id" - for key, value in kwargs.items(): - if key == self.primary_key: - key = "id" - stmt = stmt.where(getattr(self.SQLModel, key) == value) + if kwargs: + filters = self._build_filters(**kwargs) + stmt = stmt.where(*filters) _sort_order = sort_order or SortOrder.DESCENDING if _sort_order not in (SortOrder.DESCENDING, SortOrder.ASCENDING): @@ -298,9 +401,9 @@ def count(self, **kwargs) -> int: with self.Session() as session: stmt = select(func.count()).select_from(self.SQLModel) - for key, value in kwargs.items(): - stmt = stmt.where(getattr(self.SQLModel, key) == value) - + if kwargs: + filters = self._build_filters(**kwargs) + stmt = stmt.where(*filters) return session.scalar(stmt) @abstractmethod diff --git a/fedn/network/storage/statestore/stores/telemetry_store.py b/fedn/network/storage/statestore/stores/telemetry_store.py new file mode 100644 index 000000000..527d64fe8 --- /dev/null +++ b/fedn/network/storage/statestore/stores/telemetry_store.py @@ -0,0 +1,105 @@ +from datetime import datetime, timedelta, timezone +from typing import Dict, List + +import pymongo +from pymongo.database import Database + +from fedn.network.storage.statestore.stores.dto.telemetry import TelemetryDTO +from fedn.network.storage.statestore.stores.shared import SortOrder +from fedn.network.storage.statestore.stores.sql.shared import TelemetryModel, from_orm_model +from fedn.network.storage.statestore.stores.store import MongoDBStore, SQLStore, Store, from_document + + +class TelemetryStore(Store[TelemetryDTO]): + pass + + +class MongoDBTelemetryStore(TelemetryStore, MongoDBStore[TelemetryDTO]): + def __init__(self, database: Database, collection: str): + super().__init__(database, collection, "telemetry_id") + self.database[self.collection].create_index([("sender.client_id", pymongo.DESCENDING)]) + + def add(self, item: TelemetryDTO) -> TelemetryDTO: + telemetry = super().add(item) + self._delete_old_records(telemetry.sender.client_id, telemetry.key) + return telemetry + + def _delete_old_records(self, sender_id: str, key: str) -> int: + time_threshold = datetime.now(timezone.utc) - timedelta(minutes=5) + + result = self.database[self.collection].delete_many({"sender.client_id": sender_id, "key": key, "committed_at": {"$lt": time_threshold}}) + return result.deleted_count + + def list(self, limit: int, skip: int, sort_key: str, sort_order=SortOrder.DESCENDING, **kwargs) -> List[TelemetryDTO]: + return super().list(limit, skip, sort_key or "committed_at", sort_order, **kwargs) + + def _document_from_dto(self, item: TelemetryDTO) -> Dict: + item_dict = item.to_db(exclude_unset=False) + return item_dict + + def _dto_from_document(self, document: Dict) -> TelemetryDTO: + item = from_document(document) + return TelemetryDTO().patch_with(item, throw_on_extra_keys=False) + + +def _translate_key_sql(key: str): + if key == "sender.name": + key = "sender_name" + elif key == "sender.role": + key = "sender_role" + elif key == "sender.client_id": + key = "sender_client_id" + return key + + +class SQLTelemetryStore(TelemetryStore, SQLStore[TelemetryDTO, TelemetryModel]): + def __init__(self, Session): + super().__init__(Session, TelemetryModel, "telemetry_id") + + def add(self, item: TelemetryDTO) -> TelemetryDTO: + telemetry = super().add(item) + self._delete_old_records(telemetry.sender.client_id, telemetry.key) + return telemetry + + def _delete_old_records(self, sender_id: str, key: str) -> int: + with self.Session() as session: + time_threshold = datetime.now(timezone.utc) - timedelta(minutes=5) + result = ( + session.query(TelemetryModel) + .filter(TelemetryModel.sender_client_id == sender_id, TelemetryModel.key == key, TelemetryModel.committed_at < time_threshold) + .delete() + ) + session.commit() + return result + + def list(self, limit=0, skip=0, sort_key=None, sort_order=SortOrder.DESCENDING, **kwargs): + sort_key = _translate_key_sql(sort_key) + kwargs = {_translate_key_sql(k): v for k, v in kwargs.items()} + return super().list(limit, skip, sort_key, sort_order, **kwargs) + + def count(self, **kwargs): + kwargs = {_translate_key_sql(k): v for k, v in kwargs.items()} + return super().count(**kwargs) + + def _update_orm_model_from_dto(self, entity: TelemetryModel, item: TelemetryDTO): + item_dict = item.to_db(exclude_unset=False) + item_dict["id"] = item_dict.pop("telemetry_id", None) + + sender: Dict = item_dict.pop("sender", {}) + item_dict["sender_name"] = sender.get("name") + item_dict["sender_role"] = sender.get("role") + item_dict["sender_client_id"] = sender.get("client_id") + + for key, value in item_dict.items(): + setattr(entity, key, value) + return entity + + def _dto_from_orm_model(self, item: TelemetryModel) -> TelemetryDTO: + orm_dict = from_orm_model(item, TelemetryModel) + orm_dict["telemetry_id"] = orm_dict.pop("id") + orm_dict["sender"] = { + "name": orm_dict.pop("sender_name"), + "role": orm_dict.pop("sender_role"), + "client_id": orm_dict.pop("sender_client_id"), + } + return TelemetryDTO().populate_with(orm_dict) diff --git a/fedn/tests/stores/test_attribute_store.py b/fedn/tests/stores/test_attribute_store.py index e52703877..b5734114f 100644 --- a/fedn/tests/stores/test_attribute_store.py +++ b/fedn/tests/stores/test_attribute_store.py @@ -68,7 +68,7 @@ class TestAttributeStore: def test_get_attributes_for_client(self, db_connections_with_data: List[tuple[str, DatabaseConnection]]): for (name1, db_1) in db_connections_with_data: client_id = "test_sender_id" - attributes_distinct = db_1.attribute_store.get_attributes_for_client(client_id) + attributes_distinct = db_1.attribute_store.get_current_attributes_for_client(client_id) attributes_all = db_1.attribute_store.list(limit=0, skip=0, sort_key="committed_at", sort_order=SortOrder.ASCENDING, **{"sender.client_id": client_id}) attributes_distinct_2 = [attributes_all[0], attributes_all[-1]] assert len(attributes_distinct) == len(attributes_distinct_2) @@ -86,10 +86,12 @@ def test_add_update_delete(self, db_connection: DatabaseConnection, test_attribu attribute_id = read_attribute1_dict["attribute_id"] del read_attribute1_dict["attribute_id"] del read_attribute1_dict["committed_at"] + del read_attribute1_dict["updated_at"] test_attribute_dict = test_attribute.to_dict() del test_attribute_dict["attribute_id"] del test_attribute_dict["committed_at"] + del test_attribute_dict["updated_at"] assert read_attribute1_dict == test_attribute_dict diff --git a/fedn/tests/stores/test_client_store.py b/fedn/tests/stores/test_client_store.py index 2a9e62eeb..1209a1969 100644 --- a/fedn/tests/stores/test_client_store.py +++ b/fedn/tests/stores/test_client_store.py @@ -76,10 +76,12 @@ def test_add_get_update_delete(self, db_connection:DatabaseConnection, test_clie client_id = read_client1_dict["client_id"] del read_client1_dict["client_id"] del read_client1_dict["committed_at"] + del read_client1_dict["updated_at"] test_client_dict = test_client.to_dict() del test_client_dict["client_id"] del test_client_dict["committed_at"] + del test_client_dict["updated_at"] assert read_client1_dict == test_client_dict diff --git a/fedn/tests/stores/test_combiner_store.py b/fedn/tests/stores/test_combiner_store.py index 1eefb4609..b6bbbce4e 100644 --- a/fedn/tests/stores/test_combiner_store.py +++ b/fedn/tests/stores/test_combiner_store.py @@ -14,29 +14,21 @@ def test_combiners(): start_date = datetime.datetime(2021, 1, 4, 1, 2, 4) combiner1 = CombinerDTO(combiner_id=str(uuid.uuid4()), name="test_combiner1", - parent="localhost", ip="123:13:12:2", fqdn="", port=8080, - updated_at=start_date - datetime.timedelta(days=52), address="test_address") + parent="localhost", ip="123:13:12:2", fqdn="", port=8080, address="test_address") combiner2 = CombinerDTO(combiner_id=str(uuid.uuid4()), name="test_combiner2", - parent="localhost", ip="123:13:12:2", fqdn="", port=8080, - updated_at=start_date - datetime.timedelta(days=12), address="test_address") + parent="localhost", ip="123:13:12:2", fqdn="", port=8080, address="test_address") combiner3 = CombinerDTO(combiner_id=str(uuid.uuid4()), name="test_combiner3", - parent="localhost", ip="123:13:12:5", fqdn="", port=8080, - updated_at=start_date - datetime.timedelta(days=322), address="test_address") + parent="localhost", ip="123:13:12:5", fqdn="", port=8080, address="test_address") combiner4 = CombinerDTO(combiner_id=str(uuid.uuid4()), name="test_combiner4", - parent="localhost", ip="123:13:12:4", fqdn="", port=8080, - updated_at=start_date - datetime.timedelta(days=23), address="test_address") + parent="localhost", ip="123:13:12:4", fqdn="", port=8080, address="test_address") combiner5 = CombinerDTO(combiner_id=str(uuid.uuid4()), name="test_combiner5", - parent="localhost", ip="123:13:12:3", fqdn="", port=8080, - updated_at=start_date - datetime.timedelta(days=22), address="test_address") + parent="localhost", ip="123:13:12:3", fqdn="", port=8080, address="test_address") combiner6 = CombinerDTO(combiner_id=str(uuid.uuid4()), name="test_combiner6", - parent="localhost", ip="123:13:12:3", fqdn="", port=8080, - updated_at=start_date - datetime.timedelta(days=24), address="test_address") + parent="localhost", ip="123:13:12:3", fqdn="", port=8080, address="test_address") combiner7 = CombinerDTO(combiner_id=str(uuid.uuid4()), name="test_combiner8", - parent="localhost", ip="123:13:12:3", fqdn="", port=8080, - updated_at=start_date - datetime.timedelta(days=42), address="test_address") + parent="localhost", ip="123:13:12:3", fqdn="", port=8080, address="test_address") combiner8 = CombinerDTO(combiner_id=str(uuid.uuid4()), name="test_combiner7", - parent="localhost", ip="123:13:12:2", fqdn="", port=8080, - updated_at=start_date - datetime.timedelta(days=12), address="test_address1") + parent="localhost", ip="123:13:12:2", fqdn="", port=8080, address="test_address1") return [combiner1, combiner2, combiner3, combiner4, combiner5, combiner6, combiner7, combiner8] @pytest.fixture @@ -90,10 +82,12 @@ def test_add_update_delete(self, db_connection: DatabaseConnection, test_combine combiner_id = read_combiner1_dict["combiner_id"] del read_combiner1_dict["combiner_id"] del read_combiner1_dict["committed_at"] + del read_combiner1_dict["updated_at"] test_combiner_dict = test_combiner.to_dict() del test_combiner_dict["combiner_id"] del test_combiner_dict["committed_at"] + del test_combiner_dict["updated_at"] assert read_combiner1_dict == test_combiner_dict diff --git a/fedn/tests/stores/test_metric_store.py b/fedn/tests/stores/test_metric_store.py index bf8babb8d..ef3888f6e 100644 --- a/fedn/tests/stores/test_metric_store.py +++ b/fedn/tests/stores/test_metric_store.py @@ -95,10 +95,12 @@ def test_add_update_delete(self, db_connection: DatabaseConnection, test_model_m metric_id = read_metric1_dict["metric_id"] del read_metric1_dict["metric_id"] del read_metric1_dict["committed_at"] + del read_metric1_dict["updated_at"] test_metric_dict = test_metric.to_dict() del test_metric_dict["metric_id"] del test_metric_dict["committed_at"] + del test_metric_dict["updated_at"] assert read_metric1_dict == test_metric_dict diff --git a/fedn/tests/stores/test_model_store.py b/fedn/tests/stores/test_model_store.py index b35961416..36493a153 100644 --- a/fedn/tests/stores/test_model_store.py +++ b/fedn/tests/stores/test_model_store.py @@ -70,12 +70,14 @@ def test_add_update_delete(self, db_connection:DatabaseConnection, test_model:Mo del read_model1_dict["model_id"] del read_model1_dict["model"] del read_model1_dict["committed_at"] + del read_model1_dict["updated_at"] input_dict = test_model.to_dict() del input_dict["model_id"] del input_dict["model"] del input_dict["committed_at"] + del input_dict["updated_at"] assert read_model1_dict == input_dict diff --git a/fedn/tests/stores/test_package_store.py b/fedn/tests/stores/test_package_store.py index 4487a5917..df65e7bc5 100644 --- a/fedn/tests/stores/test_package_store.py +++ b/fedn/tests/stores/test_package_store.py @@ -68,11 +68,13 @@ def test_add_update_delete(self, db_connection: DatabaseConnection, test_package del read_package1_dict["package_id"] del read_package1_dict["committed_at"] del read_package1_dict["storage_file_name"] + del read_package1_dict["updated_at"] test_package_dict = test_package.to_dict() del test_package_dict["package_id"] del test_package_dict["committed_at"] del test_package_dict["storage_file_name"] + del test_package_dict["updated_at"] assert read_package1_dict == test_package_dict diff --git a/fedn/tests/stores/test_prediction_store.py b/fedn/tests/stores/test_prediction_store.py index e395f22ea..52319bebd 100644 --- a/fedn/tests/stores/test_prediction_store.py +++ b/fedn/tests/stores/test_prediction_store.py @@ -86,10 +86,12 @@ def test_add_update_delete(self, db_connection: DatabaseConnection, test_predict prediction_id = read_prediction1_dict["prediction_id"] del read_prediction1_dict["prediction_id"] del read_prediction1_dict["committed_at"] + del read_prediction1_dict["updated_at"] test_prediction_dict = test_prediction.to_dict() del test_prediction_dict["prediction_id"] del test_prediction_dict["committed_at"] + del test_prediction_dict["updated_at"] assert read_prediction1_dict == test_prediction_dict diff --git a/fedn/tests/stores/test_round_store.py b/fedn/tests/stores/test_round_store.py index 7f6bcd99b..00640acc3 100644 --- a/fedn/tests/stores/test_round_store.py +++ b/fedn/tests/stores/test_round_store.py @@ -88,10 +88,12 @@ def test_add_update_delete(self, db_connection: DatabaseConnection, test_round_m round_id = read_round1_dict["round_id"] del read_round1_dict["round_id"] del read_round1_dict["committed_at"] + del read_round1_dict["updated_at"] test_round_dict = test_round.to_dict() del test_round_dict["round_id"] del test_round_dict["committed_at"] + del test_round_dict["updated_at"] assert read_round1_dict == test_round_dict diff --git a/fedn/tests/stores/test_status_store.py b/fedn/tests/stores/test_status_store.py index 07d2e00f4..db8a0d108 100644 --- a/fedn/tests/stores/test_status_store.py +++ b/fedn/tests/stores/test_status_store.py @@ -71,10 +71,12 @@ def test_add_update_delete(self, db_connection: DatabaseConnection, test_status: status_id = read_status1_dict["status_id"] del read_status1_dict["status_id"] del read_status1_dict["committed_at"] + del read_status1_dict["updated_at"] test_status_dict = test_status.to_dict() del test_status_dict["status_id"] del test_status_dict["committed_at"] + del test_status_dict["updated_at"] assert read_status1_dict == test_status_dict diff --git a/fedn/tests/stores/test_validation_store.py b/fedn/tests/stores/test_validation_store.py index 3483d9a69..90937d290 100644 --- a/fedn/tests/stores/test_validation_store.py +++ b/fedn/tests/stores/test_validation_store.py @@ -73,10 +73,12 @@ def test_add_update_delete(self, db_connection:DatabaseConnection, test_validati validation_id = read_validation1_dict["validation_id"] del read_validation1_dict["validation_id"] del read_validation1_dict["committed_at"] + del read_validation1_dict["updated_at"] test_validation_dict = test_validation.to_dict() del test_validation_dict["validation_id"] del test_validation_dict["committed_at"] + del test_validation_dict["updated_at"] assert read_validation1_dict == test_validation_dict diff --git a/fedn/utils/helpers/plugins/splitlearninghelper.py b/fedn/utils/helpers/plugins/splitlearninghelper.py new file mode 100644 index 000000000..2140ae85f --- /dev/null +++ b/fedn/utils/helpers/plugins/splitlearninghelper.py @@ -0,0 +1,78 @@ +import os +import tempfile + +import numpy as np + +# import torch +from fedn.common.log_config import logger +from fedn.utils.helpers.helperbase import HelperBase + + +class Helper(HelperBase): + """FEDn helper class for models weights/parameters that can be transformed to numpy ndarrays.""" + + def __init__(self): + """Initialize helper.""" + super().__init__() + self.name = "splitlearninghelper" + + def increment_average(self, embedding1, embedding2): + """Concatenates two embeddings of format {client_id: embedding} into a new dictionary + + :param embedding1: First embedding dictionary + :param embedding2: Second embedding dictionary + :return: Concatenated embedding dictionary + """ + return {**embedding1, **embedding2} + + def save(self, data_dict, path=None, file_type="npz"): + if not path: + path = self.get_tmp_path() + + logger.info("SPLIT LEARNING HELPER: Saving data to {}".format(path)) + + # Ensure all values are numpy arrays + processed_dict = {str(k): np.array(v) for k, v in data_dict.items()} + + with open(path, "wb") as f: + np.savez_compressed(f, **processed_dict) + + return path + + def load(self, path): + """Load embeddings/gradients. + + :param path: Path to file + :return: Dict mapping client IDs to numpy arrays (either embeddings or gradients) + """ + try: + data = np.load(path) + logger.info("SPLIT LEARNING HELPER: loaded data from {}".format(path)) + result_dict = {k: data[k] for k in data.files} + return result_dict + except Exception as e: + logger.error(f"Error in splitlearninghelper: loading data from {path}: {str(e)}") + raise + + def get_tmp_path(self, suffix=".npz"): + """Return a temporary output path compatible with save_model, load_model. + + :param suffix: File suffix. + :return: Path to file. + """ + fd, path = tempfile.mkstemp(suffix=suffix) + os.close(fd) + return path + + def check_supported_file_type(self, file_type): + """Check if the file type is supported. + + :param file_type: File type to check. + :type file_type: str + :return: True if supported, False otherwise. + :rtype: bool + """ + supported_file_types = ["npz", "raw_binary"] + if file_type not in supported_file_types: + raise ValueError("File type not supported. Supported types are: {}".format(supported_file_types)) + return True diff --git a/pyproject.toml b/pyproject.toml index 0390c5b82..1fa3a44f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "fedn" -version = "0.26.0" +version = "0.28.1" description = "Scaleout Federated Learning" authors = [{ name = "Scaleout Systems AB", email = "contact@scaleoutsystems.com" }] readme = "README.rst" @@ -31,13 +31,13 @@ dependencies = [ "requests", "urllib3>=1.26.4", "gunicorn>=20.0.4", - "minio", + "boto3==1.38.14", "grpcio>=1.68.1,<=1.70", "grpcio-tools>=1.68.1,<=1.70", "numpy>=1.21.6", - "protobuf>=5.0.0,<5.30.0", + "protobuf>=5.0.0,<6.31.0", "pymongo", - "Flask==3.1.0", + "Flask==3.1.1", "pyjwt", "pyopenssl", "psutil", @@ -49,7 +49,10 @@ dependencies = [ "tenacity!=8.4.0", "graphene>=3.1", "SQLAlchemy>=2.0.36", - "psycopg2-binary>=2.9.10" + "psycopg2-binary>=2.9.10", + "opentelemetry-api", + "opentelemetry-sdk", + "opentelemetry-exporter-otlp", ] [project.urls] @@ -209,3 +212,6 @@ norecursedirs = [ ".ci", "build" ] + +log_cli = true +log_cli_level = "INFO" \ No newline at end of file