diff --git a/docs/design/in-memory-config.md b/docs/design/in-memory-config.md new file mode 100644 index 000000000..746718c3a --- /dev/null +++ b/docs/design/in-memory-config.md @@ -0,0 +1,227 @@ +# In-Memory Configuration for GUI Submit + +## Problem + +The current configuration system in deadline-cloud always reads settings from the on-disk config file (`~/.deadline/config`). When a user runs `deadline bundle gui-submit`, there is no way to pass CLI arguments like `--farm-id` or `--queue-id` to pre-populate the submission dialog. The non-GUI `bundle submit` command supports these options via `_apply_cli_options_to_config`, which creates an in-memory `ConfigParser` overlay, but this pattern is not wired into the GUI submission path. + +Additionally, the settings dialog (`DeadlineConfigDialog`) always writes changes directly to the on-disk config. There is no concept of editing settings that only apply to the current session. + +## Goals + +1. Allow users to pass `--farm-id`, `--queue-id`, `--profile`, and `--storage-profile-id` into `deadline bundle gui-submit`, applying them as in-memory overrides for the submission session without writing to disk. +2. In the settings dialog opened from the submission window, allow users to choose between modifying the in-memory (session) config or the workstation (on-disk) config. + +## Current Architecture + +### Config Read/Write Flow + +- `config_file.read_config()` reads from `~/.deadline/config` (with caching based on mtime). +- `config_file.get_setting(name, config=None)` reads from the on-disk config when `config=None`, or from a provided `ConfigParser` when one is passed. +- `config_file.set_setting(name, value, config=None)` writes to disk when `config=None`, or mutates the provided `ConfigParser` in-memory when one is passed. +- `config_file.write_config(config)` persists a `ConfigParser` to disk. + +### CLI to In-Memory Config (existing pattern) + +`_apply_cli_options_to_config(**args)` in `_common.py`: +- Always returns a fresh `ConfigParser` copy — never `None`, never mutates the caller's config or the `read_config()` cache. +- If any CLI args are non-None, applies overrides via `set_setting(name, value, config=config)` on the copy. +- If no CLI args are provided, returns an unmodified copy of the disk config. +- Used by all CLI commands (`bundle submit`, `bundle gui-submit`, `job get`, etc.). + +### GUI Submission Flow + +`bundle_gui_submit` -> `show_job_bundle_submitter()` -> creates `SubmitJobToDeadlineDialog`. + +The dialog's `SharedJobSettingsWidget` calls `get_setting("defaults.farm_id")` and `get_setting("defaults.queue_id")` **without** passing a config object, so it always reads from disk. + +The `DeadlineUIController` and `DeadlineAuthenticationStatus` singletons accept a `ConfigParser` via `set_config()`, but the submission dialog never passes one derived from CLI args. + +### Settings Dialog + +`DeadlineConfigDialog` / `DeadlineWorkstationConfigWidget`: +- Maintains a `self.config` (copy of on-disk config) and a `self.changes` dict. +- On `apply()`, reads fresh config from disk, applies changes, then calls `write_config()` to persist. +- There is no concept of "session-only" changes. + +## Design + +### 1. CLI Options on `bundle gui-submit` + +Add `--profile`, `--farm-id`, `--queue-id`, and `--storage-profile-id` options to the `bundle_gui_submit` click command, matching what `bundle_submit` already accepts. + +In the command handler, use `_apply_cli_options_to_config()` to produce an in-memory `ConfigParser` with the overrides applied. Since `_apply_cli_options_to_config` always returns a fresh copy (never `None`, never mutates the cache), the result can be passed directly as the session config. Pass this config object through to `show_job_bundle_submitter()` and into `SubmitJobToDeadlineDialog`. + +### 2. Session Config Propagation + +`SubmitJobToDeadlineDialog.__init__` accepts an optional `session_config: Optional[ConfigParser]` parameter. When provided: + +- Stored as `self._session_config`. +- Passed to `SharedJobSettingsWidget` via its new `config` parameter. +- Used in `_set_submit_button_state` to check farm/queue configuration. +- Used in `on_submit` when starting job submission (falls back to `config_file.read_config()` when `None`). + +When `config` is `None` (no CLI overrides), behavior is unchanged — everything reads from disk as before. + +### 3. Config Threading Through Widget Hierarchy + +During implementation we discovered that the config needs to be threaded deeper than initially expected. The full propagation chain is: + +``` +bundle_gui_submit (CLI) + -> _apply_cli_options_to_config(**args) -> ConfigParser (always a fresh copy) + -> show_job_bundle_submitter(session_config=...) + -> SubmitJobToDeadlineDialog(session_config=...) [stores as self._session_config] + -> SharedJobSettingsWidget(config=...) [stores as self._config] + -> DeadlineCloudSettingsWidget(config=...) [stores as self._config] + -> DeadlineFarmDisplay(config=...) [stores as self._config] + -> DeadlineQueueDisplay(config=...) [stores as self._config] + -> All get_setting() calls pass config=self._config + -> _set_submit_button_state() uses config=self._session_config + -> on_submit() uses self._session_config or config_file.read_config() +``` + +The `_DeadlineNamedResourceDisplay` base class and all its subclasses (`DeadlineFarmDisplay`, `DeadlineQueueDisplay`, `DeadlineStorageProfileNameDisplay`) needed to be updated to accept and use the config. These widgets call `get_setting()` both during `__init__` (to get the initial ID) and in `refresh()` / `get_item()` (to fetch current IDs for API calls). All 14 `get_setting()` call sites in `shared_job_settings_tab.py` were updated to pass `config=self._config`. + +### 4. Settings Dialog: Config File Menu + +The settings dialog has a consistent button bar in both `deadline config gui` and `bundle gui-submit`: Ok, Cancel, and "Config File" (dropdown menu). + +- **`deadline config gui`** (no session_config): "Save to Disk" and "Load from Disk" in the Config File menu are enabled when there are pending changes. "Load from Disk" discards pending changes and refreshes from disk. Ok = Save to Disk + close. +- **`bundle gui-submit`** (with session_config): The "Config File" menu contains "Save to Disk" (writes session config to `~/.deadline/config`) and "Load from Disk" (reloads the session config from the on-disk config, discarding in-memory overrides). Ok = Apply pending changes to session + close. + +The submission dialog always provides a session config when opening the settings dialog, even if no CLI overrides were passed. If `_session_config` is `None`, `_ensure_session_config()` creates one from the current on-disk config. + +Implementation: + +- The button bar always contains Ok, Cancel, and "Config File" (dropdown menu with "Save to Disk" and "Load from Disk"). +- "Save to Disk" starts disabled and is enabled when the effective config differs from disk (session mode) or there are pending changes (workstation mode). +- The "Config File" button itself is disabled when all its menu actions are disabled. +- `refresh()` in `DeadlineWorkstationConfigWidget` prunes pending changes that match the base config, so changing a value back to its original disables the buttons. +- `DeadlineConfigDialog.configure_settings()` accepts an optional `session_config: Optional[ConfigParser]` parameter. +- `DeadlineWorkstationConfigWidget` determines its mode from whether `session_config` is provided. `refresh()` uses the session config as the base when one is present. +- In **session mode** (when `session_config` is provided), `apply()` mutates the provided `ConfigParser` in-memory. It does **not** call `write_config()`. +- In **workstation mode** (no `session_config`, e.g. `deadline config gui`), `apply()` behaves as before — reads from disk, applies changes, writes to disk. +- `configure_settings()` returns a `ConfigureSettingsResult(changes_applied, session_config)` so the caller can update its state. + +### 5. Return Flow + +`_apply_settings_result` in `SubmitJobToDeadlineDialog` handles the result from the settings dialog: + +```python +def _apply_settings_result(self, result): + """Apply the result from DeadlineConfigDialog.configure_settings().""" + if result.session_config is not None: + self._session_config = result.session_config + self.deadline_authentication_status.set_config(self._session_config) + self.shared_job_settings.set_session_config(self._session_config) + self.refresh_deadline_settings() +``` + +The submitter always refreshes when a session config is returned, regardless of whether `changes_applied` is True. This ensures the submitter picks up any changes made via Ok in the settings dialog. + +Both `on_settings_button_clicked` and `on_switch_profile_clicked` call `_ensure_session_config()` to guarantee a session config exists (creating one from disk if needed), pass it to `configure_settings()`, and delegate to `_apply_settings_result()`. + +### 6. Job ID Persistence with `persist_job_id` + +After a successful submission, the job ID should be written to the on-disk config so that subsequent CLI commands like `deadline job get` can find it. The config's hierarchical section structure (`profile → farm → queue → job_id`) means the job ID must be written to the section matching the farm/queue the job was submitted to — not necessarily the on-disk defaults. + +Previously, `create_job_from_job_bundle` in the API layer auto-persisted the job ID when `config=None`, and `bundle_submit` skipped persistence entirely when CLI overrides were present. The GUI path wrote the job ID using the on-disk defaults, which was incorrect when `--farm-id`/`--queue-id` overrides were used. + +The new approach: +- **The API layer (`create_job_from_job_bundle`) still auto-persists the job ID when `config=None`**, preserving backward compatibility for public API callers (e.g. Unreal) that use on-disk defaults. When a `config` is provided, the API layer does not persist — the caller owns that responsibility. +- **A new `config_file.persist_job_id(job_id, profile, farm_id, queue_id)` helper** handles persistence correctly. The caller extracts the profile, farm ID, and queue ID from its config (e.g. a session config with CLI overrides) and passes them explicitly. The helper builds the correct hierarchical section name and writes the job ID to the on-disk config. The on-disk farm/queue defaults are never changed. +- **Both CLI and GUI callers** call `persist_job_id(job_id, profile=..., farm_id=..., queue_id=...)` after a successful submission, extracting the values from their session config so the section resolves correctly regardless of whether overrides were used. + +## Implementation Progress + +### Done + +- [x] `bundle_group.py`: Added `--profile`, `--farm-id`, `--queue-id`, `--storage-profile-id` click options to `bundle_gui_submit`. Added `_apply_cli_options_to_config(**args)` call. Passing `session_config=config` to `show_job_bundle_submitter()`. +- [x] `_common.py`: `_apply_cli_options_to_config` always copies the input config and always returns a `ConfigParser` (never `None`, never mutates the caller's object or the `read_config()` cache). +- [x] `config_file.py`: Added `persist_job_id(job_id, profile, farm_id, queue_id)` helper that writes the job ID to the correct hierarchical section on disk without changing on-disk farm/queue defaults. +- [x] `bundle_group.py` `bundle_submit`: Uses `persist_job_id(job_id, profile=..., farm_id=..., queue_id=...)` to always persist the job ID to the correct section, even when CLI overrides are present. +- [x] `bundle_group.py` `bundle_gui_submit`: Simplified — no longer needs manual pre-copy workaround since `_apply_cli_options_to_config` always copies. +- [x] `_submit_job_bundle.py`: Scoped `set_setting("defaults.job_id", job_id)` side effect to `config is None` only (preserving backward compat for public API callers). When a `config` is provided, callers own their persistence policy. +- [x] `job_bundle_submitter.py`: `show_job_bundle_submitter()` accepts `session_config` param and passes it to `SubmitJobToDeadlineDialog`. +- [x] `submit_job_to_deadline_dialog.py`: `SubmitJobToDeadlineDialog.__init__` accepts `session_config: Optional[ConfigParser]`, stores as `self._session_config`. Passes to `SharedJobSettingsWidget`. `_set_submit_button_state` and `on_submit` use session config. Added `_ensure_session_config()` to lazily create a session config from disk when none was provided via CLI args. Added `_apply_settings_result()` helper to propagate updated session config to singletons and child widgets after settings dialog closes. `_submission_succeeded_signal_receiver` uses `persist_job_id(job_id, profile=..., farm_id=..., queue_id=...)` to write the job ID to the correct farm/queue section on disk. +- [x] `shared_job_settings_tab.py`: `SharedJobSettingsWidget` accepts `config` param. All 14 `get_setting()` calls updated to pass `config=self._config`. `DeadlineCloudSettingsWidget`, `_DeadlineNamedResourceDisplay`, `DeadlineFarmDisplay`, `DeadlineQueueDisplay`, `DeadlineStorageProfileNameDisplay` all accept and thread config. Added `set_session_config()` method for updating the config after settings dialog closes. +- [x] `deadline_config_dialog.py`: Accept `session_config` in `configure_settings()`. "Config File" dropdown menu offers "Save to Disk" and "Load from Disk". Ok applies to session (session mode) or saves to disk (workstation mode). Returns `ConfigureSettingsResult` dataclass instead of `bool`. +- [x] Propagate session config to `DeadlineUIController` and `DeadlineAuthenticationStatus` singletons via their existing `set_config()` methods. +- [x] Tests for the new behavior (`test/unit/deadline_client/ui/test_session_config.py`). + +## Affected Components Summary + +| Component | Change | Status | +|---|---|---| +| `_common.py` `_apply_cli_options_to_config` | Always copy input config. Always return `ConfigParser` (never `None`). Never mutate caller's object or `read_config()` cache. | Done | +| `config_file.py` `persist_job_id` | New helper. Accepts explicit profile, farm_id, queue_id to build the correct hierarchical section. Writes job ID to on-disk config without changing farm/queue defaults. | Done | +| `config/__init__.py` | Export `persist_job_id`. | Done | +| `bundle_group.py` `bundle_submit` | Use `persist_job_id(job_id, profile=..., farm_id=..., queue_id=...)` instead of conditional `set_setting`. Job ID now always persisted to correct section even with CLI overrides. | Done | +| `bundle_group.py` `bundle_gui_submit` | Add `--profile`, `--farm-id`, `--queue-id`, `--storage-profile-id` options. Call `_apply_cli_options_to_config()` directly (no manual pre-copy needed). Pass `session_config` to `show_job_bundle_submitter()`. | Done | +| `_submit_job_bundle.py` `create_job_from_job_bundle` | Scoped `set_setting("defaults.job_id", job_id)` to `config is None` only. When a `config` is provided, callers own persistence via `persist_job_id`. | Done | +| `job_bundle_submitter.py` `show_job_bundle_submitter()` | Accept optional `session_config` param, pass to `SubmitJobToDeadlineDialog`. | Done | +| `submit_job_to_deadline_dialog.py` `SubmitJobToDeadlineDialog` | Accept optional `session_config` param. Store as `_session_config`. Propagate to child widgets. Use in `_set_submit_button_state` and `on_submit`. Use `persist_job_id` in `_submission_succeeded_signal_receiver`. | Done | +| `shared_job_settings_tab.py` `SharedJobSettingsWidget` | Accept optional `config` param. Use `get_setting(..., config=self._config)` instead of bare `get_setting(...)`. | Done | +| `shared_job_settings_tab.py` display widgets | `DeadlineCloudSettingsWidget`, `_DeadlineNamedResourceDisplay`, `DeadlineFarmDisplay`, `DeadlineQueueDisplay`, `DeadlineStorageProfileNameDisplay` all accept and use config. | Done | +| `deadline_config_dialog.py` `DeadlineConfigDialog` | Accept optional `session_config`. Button bar: Ok, Cancel, "Config File" dropdown ("Save to Disk" / "Load from Disk"). Ok applies to session or saves to disk depending on mode. Return `ConfigureSettingsResult`. | Done | +| `submit_job_to_deadline_dialog.py` settings button | Update `on_settings_button_clicked` to pass/receive session config via `_ensure_session_config()`. Propagate to singletons and child widgets via `_apply_settings_result()`. | Done | +| `_deadline_controller.py` `DeadlineUIController` | Already supports `set_config()` — called with session config at init and after settings dialog. | Done | +| `deadline_authentication_status.py` | Already supports `set_config()` — called with session config at init and after settings dialog. | Done | + +## Breaking Changes + +### For library consumers (`deadline.client.api`) + +**`create_job_from_job_bundle` auto-persist behavior is now scoped to `config=None` only.** + +When calling `create_job_from_job_bundle()` without passing `config=`, the function still auto-persists the job ID to `~/.deadline/config` as before — no change needed. This preserves backward compatibility for callers like Unreal that use the public API with on-disk defaults. + +When `config=` is provided (a `ConfigParser` object), the function no longer auto-persists the job ID. This was already the behavior before — the `config is None` check meant it only ever auto-persisted in the no-config case. Callers that pass a config and want persistence should use `persist_job_id`: +```python +from deadline.client.config import persist_job_id, get_setting +job_id = create_job_from_job_bundle(..., config=my_config) +if job_id: + persist_job_id( + job_id, + profile=get_setting("defaults.aws_profile_name", config=my_config), + farm_id=get_setting("defaults.farm_id", config=my_config), + queue_id=get_setting("defaults.queue_id", config=my_config), + ) +``` + +### For UI plugin authors (`deadline.client.ui`) + +**`DeadlineConfigDialog.configure_settings()` return type changed from `bool` to `ConfigureSettingsResult`.** + +- **Who is affected**: Code that captures and inspects the return value of `configure_settings()`, including truthiness checks like `if configure_settings():`. +- **Migration**: The `ConfigureSettingsResult` dataclass instance is always truthy (even when `changes_applied=False`), so `if configure_settings():` will no longer work as before. Replace with `if configure_settings().changes_applied:`. Code that discarded the return value is unaffected. + +### Behavioral changes (non-breaking but notable) + +**Job ID is now persisted even when CLI overrides are used.** + +Previously, `deadline bundle submit --farm-id X --queue-id Y` would submit the job but *not* save the job ID to disk. Now it always persists the job ID to the correct hierarchical section (`profile/farm-X/queue-Y`). This means `deadline job get` will find the job ID when the same farm/queue is configured, even if they were originally specified via CLI flags. + +**`deadline bundle gui-submit` always passes a session config to the submit dialog.** + +Previously, when no `--farm-id`/`--queue-id`/`--profile`/`--storage-profile-id` flags were provided, `bundle_gui_submit` passed `session_config=None` to the dialog. Now it always passes a `ConfigParser` (a copy of the disk config). The dialog's `Optional[ConfigParser]` parameter still accepts `None` for third-party submitters that don't use the CLI entry point. + +## Key Design Decisions + +1. **Reuse `_apply_cli_options_to_config`**: Rather than inventing a new mechanism, we reuse the existing pattern from `bundle submit` to create the in-memory config overlay. The function was hardened to always copy and always return a `ConfigParser`, eliminating a class of cache-mutation bugs. +2. **ConfigParser as the session state carrier**: The `ConfigParser` object is already the abstraction used throughout the codebase. Passing it explicitly avoids introducing a new config abstraction. +3. **No global/singleton session config**: The session config is scoped to the dialog instance and passed explicitly. This avoids side effects on other parts of the system and keeps the on-disk config as the single source of truth for non-GUI paths. +4. **Consistent settings dialog button bar**: The button bar is identical across both entry points: Ok, Cancel, and "Config File" (dropdown menu with "Save to Disk" and "Load from Disk"). Without a session config (`deadline config gui`), "Save to Disk" and "Load from Disk" enable when there are pending changes. With a session config (`bundle gui-submit`), "Save to Disk" and "Load from Disk" enable when the effective config differs from disk. Ok saves to disk in workstation mode and applies to the in-memory session in session mode. +5. **Session config does not persist across submissions**: If the user closes the GUI and re-runs `bundle gui-submit` without CLI args, the session config is gone. This is intentional — CLI args are ephemeral by nature. +6. **Workstation changes in settings dialog write to disk on Ok**: This preserves the existing behavior and user expectations for workstation config. +7. **Deep config threading required**: The config must be passed through the entire widget hierarchy, not just to `SharedJobSettingsWidget`. The display widgets (`DeadlineFarmDisplay`, `DeadlineQueueDisplay`, `DeadlineStorageProfileNameDisplay`) also call `get_setting()` and need the config for correct behavior. All 14 `get_setting()` call sites in `shared_job_settings_tab.py` were updated. +8. **`_apply_cli_options_to_config` always returns a `ConfigParser`**: The function never returns `None`. When no CLI overrides are provided, it returns an unmodified copy of the disk config. This eliminates `config or read_config()` fallback patterns in callers and ensures the session config is always available for threading through the widget hierarchy. +9. **`configure_settings()` returns a dataclass instead of `bool`**: The return type changed from `bool` to `ConfigureSettingsResult(changes_applied, session_config)`. This is a breaking change for callers that used the return value in a truthiness check (e.g. `if configure_settings():`), since a dataclass instance is always truthy. Existing callers in this repo (`dev_application.py`, `config_group.py`) discard the return value and are unaffected. +10. **Session config must be re-propagated after settings dialog closes**: When the settings dialog returns an updated session config, it must be pushed to `DeadlineAuthenticationStatus`, `DeadlineUIController`, and `SharedJobSettingsWidget._config` — not just stored on the submit dialog. The `_apply_settings_result()` helper method handles this and always refreshes the submitter when a session config is returned. +11. **Config sections are hierarchical (farm-scoped queues)**: The `ConfigParser` sections are structured like `profile-(default) farm-XXXX defaults`, meaning `queue_id` is scoped under the farm section. Overriding `--farm-id` alone causes `queue_id` to resolve to empty (the new farm section doesn't exist yet). This is correct behavior — a queue belongs to a specific farm — but means `--farm-id` and `--queue-id` should typically be passed together. +12. **Lazy session config creation from the submitter**: The submit dialog's `_ensure_session_config()` creates a session config from the on-disk config on first use if none was provided via CLI args. This guarantees the settings dialog always operates in session mode when opened from the submitter, while `deadline config gui` (which never passes a session config) remains workstation-only. +13. **Load from Disk is a menu action with context-dependent enablement**: In session mode, enabled when the effective config (session + pending changes) differs from disk. In workstation mode, enabled when there are pending changes (acts as a discard-changes operation). "Save to Disk" follows the same logic per mode. The "Config File" button itself is disabled when all its menu actions are disabled. +14. **Change pruning keeps button state accurate**: `refresh()` removes pending changes whose values match the base config. This means changing a value and changing it back correctly disables Save to Disk, rather than leaving stale entries in the changes dict. +15. **Job ID persistence moved out of the API layer for config-aware callers**: `create_job_from_job_bundle` still auto-persists the job ID when `config=None` (preserving backward compatibility for public API callers like Unreal). When a `config` is provided, the caller owns persistence via `persist_job_id`. This keeps the simple public API path working while giving CLI and GUI callers control over which section the job ID is written to. +16. **`persist_job_id` takes explicit profile, farm_id, and queue_id**: Rather than accepting a `ConfigParser` and internally resolving the section via the dependency chain, `persist_job_id(job_id, profile, farm_id, queue_id)` makes its dependencies explicit. The caller extracts the three values from its config (via `get_setting`) and passes them directly. This makes the function easier to test, removes coupling to `_get_section_prefixes` internals, and makes the section construction transparent. For example, `deadline bundle submit --farm-id X --queue-id Y` extracts the profile, farm, and queue from the session config and passes them to `persist_job_id`, which writes the job ID under the `profile-{profile} X Y defaults` section. +17. **`_apply_cli_options_to_config` never mutates its inputs**: The function always creates a fresh `ConfigParser` copy before applying overrides. This eliminates the footgun where the `read_config()` cache could be permanently mutated by CLI overrides, which previously required callers (like `bundle_gui_submit`) to defensively pre-copy the config. diff --git a/src/deadline/client/api/_submit_job_bundle.py b/src/deadline/client/api/_submit_job_bundle.py index 9e89fe14e..9d1826571 100644 --- a/src/deadline/client/api/_submit_job_bundle.py +++ b/src/deadline/client/api/_submit_job_bundle.py @@ -844,8 +844,10 @@ def create_job_from_job_bundle( job_id = create_job_response["jobId"] print_function_callback("Waiting for Job to be created...") - # If using the default config, set the default job id so it holds the - # most-recently submitted job. + # When no config was provided, the caller is using the public API with + # on-disk defaults — auto-persist the job ID so `deadline job get` works. + # When a config IS provided, the caller (CLI or GUI) owns persistence + # and can resolve the correct hierarchical section via persist_job_id. if config is None: set_setting("defaults.job_id", job_id) diff --git a/src/deadline/client/cli/_common.py b/src/deadline/client/cli/_common.py index b69091141..54a97e9e9 100644 --- a/src/deadline/client/cli/_common.py +++ b/src/deadline/client/cli/_common.py @@ -89,20 +89,21 @@ def wraps(ctx: click.Context, *args, **kwargs): def _apply_cli_options_to_config( *, config: Optional[ConfigParser] = None, required_options: Set[str] = set(), **args -) -> Optional[ConfigParser]: +) -> ConfigParser: """ - Modifies an AWS Deadline Cloud config object to apply standard option names to it, such as - the AWS profile, AWS Deadline Cloud Farm, or AWS Deadline Cloud Queue to use. + Returns an in-memory AWS Deadline Cloud config with standard CLI option overrides applied. + Always returns a fresh copy — never mutates the caller's config or the read_config() cache. Args: - config (ConfigParser, optional): an AWS Deadline Cloud config, read by config_file.read_config(). - If not provided, loads the config from disk. + config (ConfigParser, optional): a base config to copy from. If not provided, loads from disk. """ - # Only work with a custom config if there are standard options provided - if any(value is not None for value in args.values()): - if config is None: - config = config_file.read_config() + # Always start from a copy so we never mutate the caller's object or the cache + base = config if config is not None else config_file.read_config() + config = ConfigParser() + config.read_dict(base) + # Apply any provided standard options + if any(value is not None for value in args.values()): aws_profile_name = args.pop("profile", None) if aws_profile_name: config_file.set_setting("defaults.aws_profile_name", aws_profile_name, config=config) diff --git a/src/deadline/client/cli/_groups/bundle_group.py b/src/deadline/client/cli/_groups/bundle_group.py index 21080a0b4..6c259a277 100644 --- a/src/deadline/client/cli/_groups/bundle_group.py +++ b/src/deadline/client/cli/_groups/bundle_group.py @@ -20,7 +20,7 @@ from botocore.exceptions import ClientError from ... import api -from ...config import config_file +from ...config import config_file, get_setting, persist_job_id from ...dataclasses import SubmitterInfo from ....job_attachments.exceptions import ( AssetSyncError, @@ -310,17 +310,16 @@ def _check_create_job_wait_canceled() -> bool: click.echo("Saved job debug snapshot:") click.echo(f" {save_debug_snapshot}") - # Check Whether the CLI options are modifying any of the default settings that affect - # the job id. If not, we'll save the job id submitted as the default job id. + # Persist the job ID to the on-disk config under the correct + # farm/queue section so `deadline job get` picks it up. # If a job snapshot directory was provided, the job_id will be None. - if ( - args.get("profile") is None - and args.get("farm_id") is None - and args.get("queue_id") is None - and args.get("storage_profile_id") is None - and job_id - ): - config_file.set_setting("defaults.job_id", job_id) + if job_id: + persist_job_id( + job_id, + profile=get_setting("defaults.aws_profile_name", config=config), + farm_id=get_setting("defaults.farm_id", config=config), + queue_id=get_setting("defaults.queue_id", config=config), + ) except AssetSyncCancelledError as exc: if sigint_handler.continue_operation: @@ -418,6 +417,10 @@ def _check_create_job_wait_canceled() -> bool: 'OR --submitter-info \'{"submitter_name": "MyApp", "additional_info": {"render_engine": "Cycles"}}\' ' "OR --submitter-info file://path/to/submitter.json", ) +@click.option("--profile", help="The AWS profile to use.") +@click.option("--farm-id", help="The farm to use.") +@click.option("--queue-id", help="The queue to use.") +@click.option("--storage-profile-id", help="The storage profile to use.") @_handle_error def bundle_gui_submit( parameter, @@ -439,6 +442,9 @@ def bundle_gui_submit( Learn more about [job bundles](https://docs.aws.amazon.com/deadline-cloud/latest/developerguide/build-job-bundle.html) """ + # Apply CLI options (--profile, --farm-id, --queue-id, --storage-profile-id) to an in-memory config. + # _apply_cli_options_to_config always returns a fresh copy, so the on-disk cache is never mutated. + config = _apply_cli_options_to_config(**args) if submitter_name: click.echo( click.style( @@ -473,6 +479,7 @@ def bundle_gui_submit( submitter_info=submitter_info, known_asset_paths=known_asset_path, job_parameters=parameter, + session_config=config, ) if not submitter: diff --git a/src/deadline/client/config/__init__.py b/src/deadline/client/config/__init__.py index d2bf89c45..84e20cc36 100644 --- a/src/deadline/client/config/__init__.py +++ b/src/deadline/client/config/__init__.py @@ -15,6 +15,7 @@ "get_setting", "set_setting", "clear_setting", + "persist_job_id", "get_best_profile_for_farm", "str2bool", "DEFAULT_DEADLINE_ENDPOINT_URL", @@ -27,5 +28,6 @@ get_setting_default, set_setting, clear_setting, + persist_job_id, str2bool, ) diff --git a/src/deadline/client/config/config_file.py b/src/deadline/client/config/config_file.py index 66c1406bd..8fc439a2b 100644 --- a/src/deadline/client/config/config_file.py +++ b/src/deadline/client/config/config_file.py @@ -429,6 +429,30 @@ def set_setting(setting_name: str, value: str, config: Optional[ConfigParser] = write_config(config) +def persist_job_id(job_id: str, profile: str, farm_id: str, queue_id: str) -> None: + """ + Persists a job ID to the on-disk config file under the correct + hierarchical section (profile / farm / queue). + + The caller provides the explicit profile, farm, and queue that the job + was submitted to. The job ID is written into the matching section of + the on-disk config without changing the on-disk farm/queue defaults. + + Args: + job_id: The job ID to persist. + profile: The AWS profile name used for submission. + farm_id: The farm ID the job was submitted to. + queue_id: The queue ID the job was submitted to. + """ + section = f"profile-{profile} {farm_id} {queue_id} defaults" + + disk_config = read_config() + if section not in disk_config: + disk_config[section] = {} + disk_config.set(section, "job_id", job_id) + write_config(disk_config) + + def clear_setting(setting_name: str, config: Optional[ConfigParser] = None): """ Sets the value of the specified setting back to the default value. diff --git a/src/deadline/client/ui/dialogs/__init__.py b/src/deadline/client/ui/dialogs/__init__.py index 60545700e..7ddf63a9e 100644 --- a/src/deadline/client/ui/dialogs/__init__.py +++ b/src/deadline/client/ui/dialogs/__init__.py @@ -1,6 +1,7 @@ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. __all__ = [ + "ConfigureSettingsResult", "DeadlineConfigDialog", "DeadlineLoginDialog", "SubmitJobProgressDialog", @@ -10,7 +11,7 @@ ] from ._types import JobBundlePurpose -from .deadline_config_dialog import DeadlineConfigDialog +from .deadline_config_dialog import ConfigureSettingsResult, DeadlineConfigDialog from .deadline_login_dialog import DeadlineLoginDialog from .submit_job_progress_dialog import SubmitJobProgressDialog from .submit_job_to_deadline_dialog import SubmitJobToDeadlineDialog diff --git a/src/deadline/client/ui/dialogs/deadline_config_dialog.py b/src/deadline/client/ui/dialogs/deadline_config_dialog.py index 52bb3aa43..74437111d 100644 --- a/src/deadline/client/ui/dialogs/deadline_config_dialog.py +++ b/src/deadline/client/ui/dialogs/deadline_config_dialog.py @@ -11,6 +11,7 @@ __all__ = ["DeadlineConfigDialog"] from configparser import ConfigParser +from dataclasses import dataclass from logging import getLogger, root from typing import Callable, Dict, List, Optional @@ -30,6 +31,7 @@ QHBoxLayout, QLabel, QListWidget, + QMenu, QMessageBox, QPushButton, QSizePolicy, @@ -60,41 +62,66 @@ NOT_VALID_MARKER = "[NOT VALID]" +@dataclass +class ConfigureSettingsResult: + """Result from DeadlineConfigDialog.configure_settings().""" + + changes_applied: bool + session_config: Optional[ConfigParser] = None + + class DeadlineConfigDialog(QDialog): """ A modal dialog box for modifying the AWS Deadline Cloud local workstation configuration. + When a session_config is provided, "Ok" applies changes to the in-memory + session and closes. "Save to Disk" writes to the on-disk config, and + "Load from Disk" reverts the session to match the on-disk config. + Without a session_config, "Ok" saves to disk and closes. + Example code: DeadlineConfigDialog.configure_settings(parent=self) """ @staticmethod def configure_settings( - parent: Optional[QWidget] = None, set_profile_focus: bool = False - ) -> bool: + parent: Optional[QWidget] = None, + set_profile_focus: bool = False, + session_config: Optional[ConfigParser] = None, + ) -> ConfigureSettingsResult: """ Static method that runs the Deadline Config Dialog. Args: parent: Parent widget set_profile_focus: Optional boolean to set the initial focus to the profile selector + session_config: Optional in-memory config for session mode - Returns True if any changes were applied, False otherwise. + Returns a ConfigureSettingsResult with changes_applied and the + (possibly modified) session_config. """ - deadline_config = DeadlineConfigDialog(parent=parent) + deadline_config = DeadlineConfigDialog(parent=parent, session_config=session_config) if set_profile_focus: deadline_config.config_box.aws_profiles_box.setFocus() deadline_config.exec_() - return deadline_config.changes_were_applied + return ConfigureSettingsResult( + changes_applied=deadline_config.changes_were_applied, + session_config=deadline_config._session_config, + ) - def __init__(self, parent: Optional[QWidget] = None) -> None: + def __init__( + self, + parent: Optional[QWidget] = None, + session_config: Optional[ConfigParser] = None, + ) -> None: super().__init__( parent=parent, f=Qt.WindowSystemMenuHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint ) + self._session_config = session_config self.setWindowTitle(tr("AWS Deadline Cloud workstation configuration")) self.deadline_authentication_status = DeadlineAuthenticationStatus.getInstance() self._build_ui() @@ -116,7 +143,10 @@ def sizeHint(self): def _build_ui(self): self.layout = QVBoxLayout(self) - self.config_box = DeadlineWorkstationConfigWidget(parent=self) + self.config_box = DeadlineWorkstationConfigWidget( + parent=self, + session_config=self._session_config, + ) self.scrollArea = DeadlineScrollArea(self) self.scrollArea.setWidget(self.config_box) @@ -139,16 +169,38 @@ def _build_ui(self): self.on_auth_status_update ) - # We only use a Close button, not OK/Cancel, because we live update the settings. + # Build the button bar: Ok, Cancel, and "Config File" dropdown. self.button_box = QDialogButtonBox( - QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Apply, Qt.Horizontal + QDialogButtonBox.Ok | QDialogButtonBox.Cancel, Qt.Horizontal ) self.button_box.button(QDialogButtonBox.Ok).setText(tr("Ok")) self.button_box.button(QDialogButtonBox.Cancel).setText(tr("Cancel")) - self.button_box.button(QDialogButtonBox.Apply).setText(tr("Apply")) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) - self.button_box.clicked.connect(self.on_button_box_clicked) + + # Config File dropdown menu with Save to Disk / Load from Disk + self._config_file_button = QPushButton(tr("Config File")) + config_file_menu = QMenu(self._config_file_button) + self._save_to_disk_action = config_file_menu.addAction( + tr("Save to Disk"), self._on_save_to_disk + ) + self._save_to_disk_action.setEnabled(False) + self._load_from_disk_action = config_file_menu.addAction( + tr("Load from Disk"), self._on_load_from_disk + ) + self._load_from_disk_action.setEnabled(False) + self._config_file_button.setMenu(config_file_menu) + self.button_box.addButton(self._config_file_button, QDialogButtonBox.ActionRole) + + # Set initial enabled state: in session mode, enable if session differs from disk + if self._session_config is not None: + differs = not self._effective_config_matches_disk() + self._config_file_button.setEnabled(differs) + self._save_to_disk_action.setEnabled(differs) + self._load_from_disk_action.setEnabled(differs) + else: + self._config_file_button.setEnabled(False) + self.auth_status_box.logout_clicked.connect(self.on_logout) self.auth_status_box.login_clicked.connect(self.on_login) self.layout.addWidget(self.button_box) @@ -160,9 +212,55 @@ def _build_ui(self): def changes_were_applied(self) -> bool: return self.config_box.changes_were_applied + def _effective_config_matches_disk(self) -> bool: + """Return True if the effective config (session + pending changes) matches disk.""" + disk = config_file.read_config() + effective = self.config_box.config if self.config_box.config else self._session_config + if effective is None: + return True + return {s: dict(disk[s]) for s in disk} == {s: dict(effective[s]) for s in effective} + + def _on_load_from_disk(self) -> None: + """Reload config from disk, discarding pending changes (and session overrides if any).""" + if self._session_config is not None: + self._session_config = ConfigParser() + self._session_config.read_dict(config_file.read_config()) + self.config_box.set_session_config(self._session_config) + else: + self.config_box.changes.clear() + self.config_box.refresh() + + def _on_save_to_disk(self) -> bool: + """Save the current changes to the on-disk config. Returns True on success.""" + if not self.config_box._validate_changes("Save to Disk"): + return False + if self._session_config is not None: + # Session path: apply pending changes to session config, then write it all to disk + for setting_name, value in self.config_box.changes.items(): + config_file.set_setting(setting_name, value, self._session_config) + config_file.write_config(self._session_config) + else: + # Workstation path: read from disk, apply changes, write back + config = config_file.read_config() + for setting_name, value in self.config_box.changes.items(): + config_file.set_setting(setting_name, value, config) + config_file.write_config(config) + self.config_box.changes.clear() + self.config_box.changes_were_applied = True + self.config_box.refresh() + return True + def accept(self): - if self.config_box.apply(): - super().accept() + if self._session_config is not None: + # Session mode: Ok = Apply to session + close + if not self.config_box.apply(): + return + self._session_config = self.config_box.config + else: + # Workstation mode: Ok = Save to Disk + close + if not self._on_save_to_disk(): + return + super().accept() def reject(self): self.deadline_authentication_status.set_config(config_file.read_config()) @@ -178,13 +276,17 @@ def on_logout(self): self.deadline_authentication_status.refresh_status() self.config_box.refresh() - def on_button_box_clicked(self, button): - if self.button_box.standardButton(button) == QDialogButtonBox.Apply: - self.config_box.apply() - def on_refresh(self): - # Enable the "Apply" button only if there are changes - self.button_box.button(QDialogButtonBox.Apply).setEnabled(bool(self.config_box.changes)) + has_changes = bool(self.config_box.changes) + differs_from_disk = not self._effective_config_matches_disk() + if self._session_config is not None: + self._save_to_disk_action.setEnabled(differs_from_disk) + self._load_from_disk_action.setEnabled(differs_from_disk) + self._config_file_button.setEnabled(differs_from_disk) + else: + self._save_to_disk_action.setEnabled(has_changes) + self._load_from_disk_action.setEnabled(has_changes) + self._config_file_button.setEnabled(has_changes) # Update the auth status with the refreshed config self.deadline_authentication_status.set_config(self.config_box.config) @@ -223,12 +325,15 @@ class DeadlineWorkstationConfigWidget(QWidget): # provides (operation_name, BaseException) _background_exception = Signal(str, BaseException) - def __init__(self, parent: Optional[QWidget] = None): + def __init__( + self, parent: Optional[QWidget] = None, session_config: Optional[ConfigParser] = None + ): super().__init__(parent) self.changes: dict = {} self.config: Optional[ConfigParser] = None self.changes_were_applied = False + self._session_config = session_config # Flags to track when we're waiting for cascading list refreshes # These prevent the list_updated handlers from interfering with manual user selections @@ -242,6 +347,13 @@ def __init__(self, parent: Optional[QWidget] = None): def minimumSizeHint(self): return QSize(500, 700) + def set_session_config(self, session_config: ConfigParser) -> None: + """Replace the session config and refresh the UI.""" + self._session_config = session_config + self.changes.clear() + self.refresh() + self.refresh_lists() + def _build_ui(self): # Ensure the widget expands horizontally self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) @@ -755,9 +867,21 @@ def refresh(self): """ Refreshes all the configuration UI elements from the current config. """ - # Make self.config be a deep copy of the config, with changes applied + # Make self.config be a deep copy of the config, with changes applied. + # In session mode, start from the session config; otherwise from disk. self.config = ConfigParser() - self.config.read_dict(config_file.read_config()) + base_config = ( + self._session_config if self._session_config is not None else config_file.read_config() + ) + self.config.read_dict(base_config) + + # Remove changes that match the base config (e.g. user changed a value then changed it back) + for setting_name in list(self.changes): + if self.changes[setting_name] == config_file.get_setting( + setting_name, config=self.config + ): + del self.changes[setting_name] + for setting_name, value in self.changes.items(): config_file.set_setting(setting_name, value, self.config) self.default_farm_box.set_config(self.config) @@ -803,9 +927,24 @@ def refresh(self): self.refreshed.emit() + def _validate_changes(self, action_name: str) -> bool: + """Check that no pending changes contain NOT_VALID_MARKER. Shows a warning and returns False if invalid.""" + for setting_name, value in self.changes.items(): + if value.startswith(NOT_VALID_MARKER): + QMessageBox.warning( + self, + action_name, + f"Cannot apply changes, {value} is not valid for setting {setting_name}", + ) + return False + return True + def apply(self) -> bool: """ - Apply all the settings that the user has changed into the config file. + Apply all the settings that the user has changed. + + In workstation mode, writes changes to the on-disk config file. + In session mode, mutates the in-memory session config without writing to disk. Returns True if the settings were applied, False otherwise. """ @@ -815,19 +954,22 @@ def apply(self) -> bool: self.default_storage_profile_box.box.currentData() ) - for setting_name, value in self.changes.items(): - if value.startswith(NOT_VALID_MARKER): - QMessageBox.warning( # type: ignore[call-arg] - self, - "Apply changes", - f"Cannot apply changes, {value} is not valid for setting {setting_name}", - ) - return False - - self.config = config_file.read_config() + if not self._validate_changes("Apply changes"): + return False + + if self._session_config is not None: + # Session mode: mutate the in-memory config without writing to disk + for setting_name, value in self.changes.items(): + config_file.set_setting(setting_name, value, self._session_config) + self.config = ConfigParser() + self.config.read_dict(self._session_config) + else: + # Workstation mode: read from disk, apply changes, write back + self.config = config_file.read_config() + for setting_name, value in self.changes.items(): + config_file.set_setting(setting_name, value, self.config) + config_file.write_config(self.config) - for setting_name, value in self.changes.items(): - config_file.set_setting(setting_name, value, self.config) root.setLevel(config_file.get_setting("settings.log_level")) api.get_deadline_cloud_library_telemetry_client().set_opt_out(config=self.config) @@ -838,8 +980,6 @@ def apply(self) -> bool: self.changes.clear() - config_file.write_config(self.config) - # Refresh the GUI (writing the config file should cause this, but do this redundantly to make sure) self.refresh() diff --git a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py index 2bd2bba9a..7f15143e6 100644 --- a/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py +++ b/src/deadline/client/ui/dialogs/submit_job_to_deadline_dialog.py @@ -9,6 +9,7 @@ import os import sys import json +from configparser import ConfigParser from typing import Any, Dict, Optional, Protocol import yaml @@ -35,7 +36,7 @@ from ...api._session import session_context as _session_context from ..deadline_authentication_status import DeadlineAuthenticationStatus from .._utils import block_signals, tr -from ...config import get_setting, set_setting, config_file +from ...config import get_setting, config_file, persist_job_id from ...exceptions import UserInitiatedCancel, NonValidInputError from ...job_bundle import create_job_history_bundle_dir from ...job_bundle.parameters import JobParameter @@ -44,7 +45,7 @@ from ..widgets.job_attachments_tab import JobAttachmentsWidget from ..widgets.shared_job_settings_tab import SharedJobSettingsWidget from ..widgets.host_requirements_tab import HostRequirementsWidget -from . import DeadlineConfigDialog, DeadlineLoginDialog +from . import ConfigureSettingsResult, DeadlineConfigDialog, DeadlineLoginDialog from ._types import JobBundlePurpose from ._help_dialog import _HelpDialog @@ -114,6 +115,7 @@ def __init__( attachments: AssetReferences, on_create_job_bundle_callback: OnCreateJobBundleCallback, parent: Optional[QWidget] = None, + session_config: Optional[ConfigParser] = None, f: Any = Qt.WindowFlags(), show_host_requirements_tab: bool = False, host_requirements: Optional[HostRequirements] = None, @@ -123,6 +125,7 @@ def __init__( # The Qt.Tool flag makes sure our widget stays in front of the main application window super().__init__(parent=parent, f=f) + self._session_config = session_config # Set window title with submitter package info if available window_title = tr("Submit to AWS Deadline Cloud") if submitter_info: @@ -147,6 +150,8 @@ def __init__( self.job_id = None self.job_history_bundle_dir: Optional[str] = None self.deadline_authentication_status = DeadlineAuthenticationStatus.getInstance() + if self._session_config is not None: + self.deadline_authentication_status.set_config(self._session_config) self.show_host_requirements_tab = show_host_requirements_tab self.known_asset_paths = known_asset_paths or [] self.should_close = False @@ -166,7 +171,16 @@ def __init__( def _submission_succeeded_signal_receiver(self, job_id: str): self.job_id = job_id - set_setting("defaults.job_id", job_id) + # Persist the job ID to the on-disk config under the correct + # farm/queue section so that CLI overrides (--farm-id / --queue-id) + # target the right place without changing the on-disk defaults. + config = self._session_config + persist_job_id( + job_id, + profile=get_setting("defaults.aws_profile_name", config=config), + farm_id=get_setting("defaults.farm_id", config=config), + queue_id=get_setting("defaults.queue_id", config=config), + ) def _close_event_receiver(self): if self.submitter_info.submitter_name != "JobBundle" and self.job_id: @@ -257,8 +271,8 @@ def _set_submit_button_state(self): # Enable/disable the Submit button based on whether the # AWS Deadline Cloud API is accessible and the farm+queue are configured. api_available = self.deadline_authentication_status.api_availability is True - farm_configured = get_setting("defaults.farm_id") != "" - queue_configured = get_setting("defaults.queue_id") != "" + farm_configured = get_setting("defaults.farm_id", config=self._session_config) != "" + queue_configured = get_setting("defaults.queue_id", config=self._session_config) != "" queue_valid = self.shared_job_settings.is_queue_valid() enable = api_available and farm_configured and queue_configured and queue_valid @@ -319,6 +333,7 @@ def _build_shared_job_settings_tab(self, initial_job_settings, initial_shared_pa initial_settings=initial_job_settings, initial_shared_parameter_values=initial_shared_parameter_values, parent=self, + config=self._session_config, ) self.shared_job_settings.parameter_changed.connect(self.on_shared_job_parameter_changed) self.shared_job_settings_tab.setWidget(self.shared_job_settings) @@ -403,13 +418,35 @@ def on_logout(self): # not always catch a change so force a refresh here. self.deadline_authentication_status.refresh_status() - def on_switch_profile_clicked(self): - if DeadlineConfigDialog.configure_settings(parent=self, set_profile_focus=True): + def _apply_settings_result(self, result: ConfigureSettingsResult) -> None: + """Apply the result from DeadlineConfigDialog.configure_settings().""" + if result.session_config is not None: + self._session_config = result.session_config + self.deadline_authentication_status.set_config(self._session_config) + self.shared_job_settings.set_session_config(self._session_config) self.refresh_deadline_settings() + def _ensure_session_config(self) -> ConfigParser: + """Return the current session config, creating one from disk if needed.""" + if self._session_config is None: + self._session_config = ConfigParser() + self._session_config.read_dict(config_file.read_config()) + return self._session_config + + def on_switch_profile_clicked(self): + result = DeadlineConfigDialog.configure_settings( + parent=self, + set_profile_focus=True, + session_config=self._ensure_session_config(), + ) + self._apply_settings_result(result) + def on_settings_button_clicked(self): - if DeadlineConfigDialog.configure_settings(parent=self): - self.refresh_deadline_settings() + result = DeadlineConfigDialog.configure_settings( + parent=self, + session_config=self._ensure_session_config(), + ) + self._apply_settings_result(result) def _on_help_button_clicked(self): """Show the Help dialog with submitter information.""" @@ -599,7 +636,7 @@ def on_submit(self): job_progress_dialog.start_job_submission( job_bundle_dir=self.job_history_bundle_dir, submitter_name=self.submitter_info.submitter_name, - config=config_file.read_config(), + config=self._session_config or config_file.read_config(), require_paths_exist=self.job_attachments.get_require_paths_exist(), job_parameters=job_parameters, known_asset_paths=self.known_asset_paths diff --git a/src/deadline/client/ui/job_bundle_submitter.py b/src/deadline/client/ui/job_bundle_submitter.py index 84a0d2ee0..4731c8edd 100644 --- a/src/deadline/client/ui/job_bundle_submitter.py +++ b/src/deadline/client/ui/job_bundle_submitter.py @@ -3,6 +3,7 @@ from __future__ import annotations import copy import os +from configparser import ConfigParser from logging import getLogger from typing import Any, Optional, Dict @@ -123,6 +124,7 @@ def show_job_bundle_submitter( submitter_info: Optional[SubmitterInfo] = None, known_asset_paths: Optional[list[str]] = None, job_parameters: Optional[list[dict[str, Any]]] = None, + session_config: Optional[ConfigParser] = None, ) -> Optional[SubmitJobToDeadlineDialog]: """ Opens an AWS Deadline Cloud job submission dialog for the provided job bundle. @@ -305,6 +307,7 @@ def on_create_job_bundle_callback( f=f, submitter_info=submitter_info, known_asset_paths=known_asset_paths, + session_config=session_config, ) if job_parameters: diff --git a/src/deadline/client/ui/translations/locales/de_DE.json b/src/deadline/client/ui/translations/locales/de_DE.json index a5cf52407..7c1cc5a1f 100644 --- a/src/deadline/client/ui/translations/locales/de_DE.json +++ b/src/deadline/client/ui/translations/locales/de_DE.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "Beim Zugriff auf die Anmeldeinformationen für das Profil '{profile}' ist ein Konfigurationsfehler aufgetreten.", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "Die AWS Deadline Cloud-API ist nicht erreichbar. Überprüfen Sie Ihren Authentifizierungsstatus.", - "AWS Deadline Cloud submission": "AWS Deadline Cloud-Übermittlung", - "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud-Workstation-Konfiguration", - "AWS profile": "AWS-Profil", "About": "Über", "Application Restart Required": "Neustart der Anwendung erforderlich", "Add": "Hinzufügen", @@ -22,14 +18,19 @@ "Attach input files": "Eingabedateien anhängen", "Attribute name": "Attributname", "Auto accept prompt defaults": "Standardwerte automatisch akzeptieren", - "CPU architecture": "CPU-Architektur", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "Die AWS Deadline Cloud-API ist nicht erreichbar. Überprüfen Sie Ihren Authentifizierungsstatus.", + "AWS Deadline Cloud submission": "AWS Deadline Cloud-Übermittlung", + "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud-Workstation-Konfiguration", + "AWS profile": "AWS-Profil", "Cancel": "Abbrechen", "Canceling submission...": "Übermittlung wird abgebrochen...", "Cannot submit job:\n\n\u2022 {issues}": "Job kann nicht übermittelt werden:\n\n\\u2022 {issues}", "Choose job bundle directory": "Jobpaket-Verzeichnis auswählen", "Close": "Schließen", + "Config File": "Konfigurationsdatei", "Conflict resolution option": "Option zur Konfliktlösung", "Copy": "Kopieren", + "CPU architecture": "CPU-Architektur", "Current: {current_version} -> New: {latest_version}": "Aktuell: {current_version} -> Neu: {latest_version}", "Current logging level": "Aktuelle Protokollierungsebene", "Custom host requirements": "Benutzerdefinierte Host-Anforderungen", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "Anmeldung bei AWS Deadline Cloud fehlgeschlagen:

{error}", "Farm": "Farm", "Farm settings": "Farm-Einstellungen", - "GPU memory (GiB)": "GPU-Speicher (GiB)", - "GPUs": "GPUs", "General settings": "Allgemeine Einstellungen", "General submission settings": "Allgemeine Übermittlungseinstellungen", "Global settings": "Globale Einstellungen", + "GPU memory (GiB)": "GPU-Speicher (GiB)", + "GPUs": "GPUs", "Hardware requirements": "Hardwareanforderungen", "Hashing progress": "Hashing-Fortschritt", "Help": "Hilfe", "Host requirements": "Host-Anforderungen", "Initial state": "Anfangszustand", "Issue With Profile Configuration": "Problem mit Profilkonfiguration", - "Job Properties": "Auftragseigenschaften", - "Job Submission Confirmation": "Bestätigung der Jobübermittlung", "Job attachments": "Arbeitsanhänge", "Job attachments filesystem options": "Dateisystemoptionen für Jobanhänge", "Job history directory": "Jobverlaufsverzeichnis", + "Job Properties": "Auftragseigenschaften", + "Job Submission Confirmation": "Bestätigung der Jobübermittlung", "Job submission confirmation": "Bestätigung der Jobübermittlung", "Job-specific settings": "Jobspezifische Einstellungen", "Known asset paths": "Bekannte Asset-Pfade", "Language": "Sprache", "Language will change next time the submitter is opened": "Die Sprache wird beim nächsten Öffnen des Submitters geändert", "Load Bundle": "Paket laden", + "Load from Disk": "Von Festplatte laden", "Log in": "Anmelden", "Log in to AWS Deadline Cloud": "Bei AWS Deadline Cloud anmelden", "Logging you in...": "Sie werden angemeldet...", @@ -106,6 +108,7 @@ "Require all input paths exist": "Alle Eingabepfade müssen vorhanden sein", "Run on all available worker hosts": "Auf allen verfügbaren Worker-Hosts ausführen", "Run on worker hosts that meet the following requirements": "Auf Worker-Hosts ausführen, die die folgenden Anforderungen erfüllen", + "Save to Disk": "Auf Festplatte speichern", "Saved the submission as a job bundle:\n{path}": "Die Übermittlung wurde als Jobpaket gespeichert:\n{path}", "Scratch space": "Temporärer Speicherplatz", "Set max worker count": "Maximale Worker-Anzahl festlegen", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - Sie sind abgemeldet.", "{profile} doesn't have access permissions to submit a job.": "{profile} hat keine Zugriffsberechtigungen zum Übermitteln eines Jobs.", "{submitter} job submission": "{submitter}-Jobübermittlung" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/en_US.json b/src/deadline/client/ui/translations/locales/en_US.json index 2a1435de7..6de41a616 100644 --- a/src/deadline/client/ui/translations/locales/en_US.json +++ b/src/deadline/client/ui/translations/locales/en_US.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "A configuration error was received while accessing credentials for the profile '{profile}'.", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "AWS Deadline Cloud API is not accessible. Check your authentication status.", - "AWS Deadline Cloud submission": "AWS Deadline Cloud submission", - "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud workstation configuration", - "AWS profile": "AWS profile", "About": "About", "Application Restart Required": "Application Restart Required", "Add": "Add", @@ -22,14 +18,19 @@ "Attach input files": "Attach input files", "Attribute name": "Attribute name", "Auto accept prompt defaults": "Auto accept prompt defaults", - "CPU architecture": "CPU architecture", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "AWS Deadline Cloud API is not accessible. Check your authentication status.", + "AWS Deadline Cloud submission": "AWS Deadline Cloud submission", + "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud workstation configuration", + "AWS profile": "AWS profile", "Cancel": "Cancel", "Canceling submission...": "Canceling submission...", "Cannot submit job:\n\n\u2022 {issues}": "Cannot submit job:\n\n\\u2022 {issues}", "Choose job bundle directory": "Choose job bundle directory", "Close": "Close", + "Config File": "Config File", "Conflict resolution option": "Conflict resolution option", "Copy": "Copy", + "CPU architecture": "CPU architecture", "Current: {current_version} -> New: {latest_version}": "Current: {current_version} -> New: {latest_version}", "Current logging level": "Current logging level", "Custom host requirements": "Custom host requirements", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "Failed to log in to AWS Deadline Cloud:

{error}", "Farm": "Farm", "Farm settings": "Farm settings", - "GPU memory (GiB)": "GPU memory (GiB)", - "GPUs": "GPUs", "General settings": "General settings", "General submission settings": "General submission settings", "Global settings": "Global settings", + "GPU memory (GiB)": "GPU memory (GiB)", + "GPUs": "GPUs", "Hardware requirements": "Hardware requirements", "Hashing progress": "Hashing progress", "Help": "Help", "Host requirements": "Host requirements", "Initial state": "Initial state", "Issue With Profile Configuration": "Issue With Profile Configuration", - "Job Properties": "Job Properties", - "Job Submission Confirmation": "Job Submission Confirmation", "Job attachments": "Job attachments", "Job attachments filesystem options": "Job attachments filesystem options", "Job history directory": "Job history directory", + "Job Properties": "Job Properties", + "Job Submission Confirmation": "Job Submission Confirmation", "Job submission confirmation": "Job submission confirmation", "Job-specific settings": "Job-specific settings", "Known asset paths": "Known asset paths", "Language": "Language", "Language will change next time the submitter is opened": "Language will change next time the submitter is opened", "Load Bundle": "Load Bundle", + "Load from Disk": "Load from Disk", "Log in": "Log in", "Log in to AWS Deadline Cloud": "Log in to AWS Deadline Cloud", "Logging you in...": "Logging you in...", @@ -106,6 +108,7 @@ "Require all input paths exist": "Require all input paths exist", "Run on all available worker hosts": "Run on all available worker hosts", "Run on worker hosts that meet the following requirements": "Run on worker hosts that meet the following requirements", + "Save to Disk": "Save to Disk", "Saved the submission as a job bundle:\n{path}": "Saved the submission as a job bundle:\n{path}", "Scratch space": "Scratch space", "Set max worker count": "Set max worker count", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - You are logged out.", "{profile} doesn't have access permissions to submit a job.": "{profile} doesn't have access permissions to submit a job.", "{submitter} job submission": "{submitter} job submission" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/es_ES.json b/src/deadline/client/ui/translations/locales/es_ES.json index 50587f1c4..255b9afbf 100644 --- a/src/deadline/client/ui/translations/locales/es_ES.json +++ b/src/deadline/client/ui/translations/locales/es_ES.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "Se recibió un error de configuración al acceder a las credenciales del perfil '{profile}'.", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "No se puede acceder a la API de AWS Deadline Cloud. Compruebe su estado de autenticación.", - "AWS Deadline Cloud submission": "Envío de AWS Deadline Cloud", - "AWS Deadline Cloud workstation configuration": "Configuración de estación de trabajo de AWS Deadline Cloud", - "AWS profile": "Perfil de AWS", "About": "Acerca de", "Application Restart Required": "Se requiere reiniciar la aplicación", "Add": "Agregar", @@ -22,14 +18,19 @@ "Attach input files": "Adjuntar archivos de entrada", "Attribute name": "Nombre de atributo", "Auto accept prompt defaults": "Aceptar automáticamente valores predeterminados", - "CPU architecture": "Arquitectura de CPU", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "No se puede acceder a la API de AWS Deadline Cloud. Compruebe su estado de autenticación.", + "AWS Deadline Cloud submission": "Envío de AWS Deadline Cloud", + "AWS Deadline Cloud workstation configuration": "Configuración de estación de trabajo de AWS Deadline Cloud", + "AWS profile": "Perfil de AWS", "Cancel": "Cancelar", "Canceling submission...": "Cancelando envío...", "Cannot submit job:\n\n\u2022 {issues}": "No se puede enviar el trabajo:\n\n\\u2022 {issues}", "Choose job bundle directory": "Elegir directorio de paquete de trabajos", "Close": "Cerrar", + "Config File": "Archivo de configuración", "Conflict resolution option": "Opción de resolución de conflictos", "Copy": "Copiar", + "CPU architecture": "Arquitectura de CPU", "Current: {current_version} -> New: {latest_version}": "Actual: {current_version} -> Nuevo: {latest_version}", "Current logging level": "Nivel de registro actual", "Custom host requirements": "Requisitos de host personalizados", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "Error al iniciar sesión en AWS Deadline Cloud:

{error}", "Farm": "Granja", "Farm settings": "Configuración de granja", - "GPU memory (GiB)": "Memoria de GPU (GiB)", - "GPUs": "GPU", "General settings": "Configuración general", "General submission settings": "Configuración general de envío", "Global settings": "Configuración global", + "GPU memory (GiB)": "Memoria de GPU (GiB)", + "GPUs": "GPU", "Hardware requirements": "Requisitos de hardware", "Hashing progress": "Progreso de hash", "Help": "Ayuda", "Host requirements": "Requisitos de host", "Initial state": "Estado inicial", "Issue With Profile Configuration": "Problema con la configuración del perfil", - "Job Properties": "Propiedades del trabajo", - "Job Submission Confirmation": "Confirmación de envío de trabajo", "Job attachments": "Adjuntos de trabajo", "Job attachments filesystem options": "Opciones del sistema de archivos de adjuntos de trabajo", "Job history directory": "Directorio de historial de trabajos", + "Job Properties": "Propiedades del trabajo", + "Job Submission Confirmation": "Confirmación de envío de trabajo", "Job submission confirmation": "Confirmación de envío de trabajo", "Job-specific settings": "Configuración específica del trabajo", "Known asset paths": "Rutas de recursos conocidas", "Language": "Idioma", "Language will change next time the submitter is opened": "El idioma cambiará la próxima vez que se abra el submitter", "Load Bundle": "Cargar paquete", + "Load from Disk": "Cargar desde disco", "Log in": "Iniciar sesión", "Log in to AWS Deadline Cloud": "Iniciar sesión en AWS Deadline Cloud", "Logging you in...": "Iniciando sesión...", @@ -106,6 +108,7 @@ "Require all input paths exist": "Requerir que existan todas las rutas de entrada", "Run on all available worker hosts": "Ejecutar en todos los hosts de trabajadores disponibles", "Run on worker hosts that meet the following requirements": "Ejecutar en hosts de trabajadores que cumplan los siguientes requisitos", + "Save to Disk": "Guardar en disco", "Saved the submission as a job bundle:\n{path}": "Se guardó el envío como paquete de trabajos:\n{path}", "Scratch space": "Espacio temporal", "Set max worker count": "Establecer recuento máximo de trabajadores", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - Ha cerrado sesión.", "{profile} doesn't have access permissions to submit a job.": "{profile} no tiene permisos de acceso para enviar un trabajo.", "{submitter} job submission": "Envío de trabajo de {submitter}" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/fr_FR.json b/src/deadline/client/ui/translations/locales/fr_FR.json index 0370c48e0..2918a5233 100644 --- a/src/deadline/client/ui/translations/locales/fr_FR.json +++ b/src/deadline/client/ui/translations/locales/fr_FR.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "Une erreur de configuration s'est produite lors de l'accès aux informations d'identification du profil '{profile}'.", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "L'API AWS Deadline Cloud n'est pas accessible. Vérifiez votre statut d'authentification.", - "AWS Deadline Cloud submission": "Soumission AWS Deadline Cloud", - "AWS Deadline Cloud workstation configuration": "Configuration de poste de travail AWS Deadline Cloud", - "AWS profile": "Profil AWS", "About": "À propos", "Application Restart Required": "Redémarrage de l'application requis", "Add": "Ajouter", @@ -22,14 +18,19 @@ "Attach input files": "Joindre des fichiers d'entrée", "Attribute name": "Nom d'attribut", "Auto accept prompt defaults": "Accepter automatiquement les valeurs par défaut", - "CPU architecture": "Architecture CPU", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "L'API AWS Deadline Cloud n'est pas accessible. Vérifiez votre statut d'authentification.", + "AWS Deadline Cloud submission": "Soumission AWS Deadline Cloud", + "AWS Deadline Cloud workstation configuration": "Configuration de poste de travail AWS Deadline Cloud", + "AWS profile": "Profil AWS", "Cancel": "Annuler", "Canceling submission...": "Annulation de la soumission...", "Cannot submit job:\n\n\u2022 {issues}": "Impossible de soumettre la tâche :\n\n\\u2022 {issues}", "Choose job bundle directory": "Choisir le répertoire du lot de tâches", "Close": "Fermer", + "Config File": "Fichier de configuration", "Conflict resolution option": "Option de résolution de conflits", "Copy": "Copier", + "CPU architecture": "Architecture CPU", "Current: {current_version} -> New: {latest_version}": "Actuel : {current_version} -> Nouveau : {latest_version}", "Current logging level": "Niveau de journalisation actuel", "Custom host requirements": "Exigences d'hôte personnalisées", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "Échec de la connexion à AWS Deadline Cloud :

{error}", "Farm": "Ferme", "Farm settings": "Paramètres de ferme", - "GPU memory (GiB)": "Mémoire GPU (Gio)", - "GPUs": "GPU", "General settings": "Paramètres généraux", "General submission settings": "Paramètres généraux de soumission", "Global settings": "Paramètres globaux", + "GPU memory (GiB)": "Mémoire GPU (Gio)", + "GPUs": "GPU", "Hardware requirements": "Exigences matérielles", "Hashing progress": "Progression du hachage", "Help": "Aide", "Host requirements": "Exigences d'hôte", "Initial state": "État initial", "Issue With Profile Configuration": "Problème avec la configuration du profil", - "Job Properties": "Propriétés de la tâche", - "Job Submission Confirmation": "Confirmation de soumission de tâche", "Job attachments": "Fichiers joints de tâche", "Job attachments filesystem options": "Options du système de fichiers des pièces jointes aux tâches", "Job history directory": "Répertoire d'historique des tâches", + "Job Properties": "Propriétés de la tâche", + "Job Submission Confirmation": "Confirmation de soumission de tâche", "Job submission confirmation": "Confirmation de soumission de tâche", "Job-specific settings": "Paramètres spécifiques à la tâche", "Known asset paths": "Chemins d'actifs connus", "Language": "Langue", "Language will change next time the submitter is opened": "La langue changera lors de la prochaine ouverture du submitter", "Load Bundle": "Charger le lot", + "Load from Disk": "Charger depuis le disque", "Log in": "Se connecter", "Log in to AWS Deadline Cloud": "Se connecter à AWS Deadline Cloud", "Logging you in...": "Connexion en cours...", @@ -106,6 +108,7 @@ "Require all input paths exist": "Exiger que tous les chemins d'entrée existent", "Run on all available worker hosts": "Exécuter sur tous les hôtes de travail disponibles", "Run on worker hosts that meet the following requirements": "Exécuter sur les hôtes de travail qui répondent aux exigences suivantes", + "Save to Disk": "Enregistrer sur le disque", "Saved the submission as a job bundle:\n{path}": "La soumission a été enregistrée en tant que lot de tâches :\n{path}", "Scratch space": "Espace temporaire", "Set max worker count": "Définir le nombre maximal de travailleurs", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - Vous êtes déconnecté.", "{profile} doesn't have access permissions to submit a job.": "{profile} n'a pas les autorisations d'accès pour soumettre une tâche.", "{submitter} job submission": "Soumission de tâche {submitter}" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/id_ID.json b/src/deadline/client/ui/translations/locales/id_ID.json index 2a066fc28..467682784 100644 --- a/src/deadline/client/ui/translations/locales/id_ID.json +++ b/src/deadline/client/ui/translations/locales/id_ID.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "Kesalahan konfigurasi diterima saat mengakses kredensial untuk profil '{profile}'.", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "API AWS Deadline Cloud tidak dapat diakses. Periksa status autentikasi Anda.", - "AWS Deadline Cloud submission": "Pengiriman AWS Deadline Cloud", - "AWS Deadline Cloud workstation configuration": "Konfigurasi workstation AWS Deadline Cloud", - "AWS profile": "Profil AWS", "About": "Tentang", "Application Restart Required": "Diperlukan restart aplikasi", "Add": "Tambah", @@ -22,14 +18,19 @@ "Attach input files": "Lampirkan file input", "Attribute name": "Nama atribut", "Auto accept prompt defaults": "Terima default secara otomatis", - "CPU architecture": "Arsitektur CPU", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "API AWS Deadline Cloud tidak dapat diakses. Periksa status autentikasi Anda.", + "AWS Deadline Cloud submission": "Pengiriman AWS Deadline Cloud", + "AWS Deadline Cloud workstation configuration": "Konfigurasi workstation AWS Deadline Cloud", + "AWS profile": "Profil AWS", "Cancel": "Batal", "Canceling submission...": "Membatalkan pengiriman...", "Cannot submit job:\n\n\u2022 {issues}": "Tidak dapat mengirim pekerjaan:\n\n\\u2022 {issues}", "Choose job bundle directory": "Pilih direktori bundel pekerjaan", "Close": "Tutup", + "Config File": "File Konfigurasi", "Conflict resolution option": "Opsi resolusi konflik", "Copy": "Salin", + "CPU architecture": "Arsitektur CPU", "Current: {current_version} -> New: {latest_version}": "Saat ini: {current_version} -> Baru: {latest_version}", "Current logging level": "Tingkat logging saat ini", "Custom host requirements": "Persyaratan host kustom", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "Gagal masuk ke AWS Deadline Cloud:

{error}", "Farm": "Peternakan", "Farm settings": "Pengaturan peternakan", - "GPU memory (GiB)": "Memori GPU (GiB)", - "GPUs": "GPU", "General settings": "Pengaturan umum", "General submission settings": "Pengaturan pengiriman umum", "Global settings": "Pengaturan global", + "GPU memory (GiB)": "Memori GPU (GiB)", + "GPUs": "GPU", "Hardware requirements": "Persyaratan perangkat keras", "Hashing progress": "Kemajuan hashing", "Help": "Bantuan", "Host requirements": "Persyaratan host", "Initial state": "Status awal", "Issue With Profile Configuration": "Masalah dengan konfigurasi profil", - "Job Properties": "Properti Job", - "Job Submission Confirmation": "Konfirmasi pengiriman pekerjaan", "Job attachments": "Lampiran Job", "Job attachments filesystem options": "Opsi sistem file lampiran pekerjaan", "Job history directory": "Direktori riwayat pekerjaan", + "Job Properties": "Properti Job", + "Job Submission Confirmation": "Konfirmasi pengiriman pekerjaan", "Job submission confirmation": "Konfirmasi pengiriman pekerjaan", "Job-specific settings": "Pengaturan khusus pekerjaan", "Known asset paths": "Jalur aset yang diketahui", "Language": "Bahasa", "Language will change next time the submitter is opened": "Bahasa akan berubah saat submitter dibuka kembali", "Load Bundle": "Muat bundel", + "Load from Disk": "Muat dari Disk", "Log in": "Masuk", "Log in to AWS Deadline Cloud": "Masuk ke AWS Deadline Cloud", "Logging you in...": "Memasukkan Anda...", @@ -106,6 +108,7 @@ "Require all input paths exist": "Memerlukan semua jalur input ada", "Run on all available worker hosts": "Jalankan di semua host pekerja yang tersedia", "Run on worker hosts that meet the following requirements": "Jalankan di host pekerja yang memenuhi persyaratan berikut", + "Save to Disk": "Simpan ke Disk", "Saved the submission as a job bundle:\n{path}": "Menyimpan pengiriman sebagai bundel pekerjaan:\n{path}", "Scratch space": "Ruang sementara", "Set max worker count": "Atur jumlah pekerja maksimum", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - Anda telah keluar.", "{profile} doesn't have access permissions to submit a job.": "{profile} tidak memiliki izin akses untuk mengirim pekerjaan.", "{submitter} job submission": "Pengiriman pekerjaan {submitter}" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/it_IT.json b/src/deadline/client/ui/translations/locales/it_IT.json index 3562adeb1..cf77e0a29 100644 --- a/src/deadline/client/ui/translations/locales/it_IT.json +++ b/src/deadline/client/ui/translations/locales/it_IT.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "Si è verificato un errore di configurazione durante l'accesso alle credenziali per il profilo '{profile}'.", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "L'API AWS Deadline Cloud non è accessibile. Verifica lo stato di autenticazione.", - "AWS Deadline Cloud submission": "Invio AWS Deadline Cloud", - "AWS Deadline Cloud workstation configuration": "Configurazione workstation AWS Deadline Cloud", - "AWS profile": "Profilo AWS", "About": "Informazioni", "Application Restart Required": "Riavvio dell'applicazione richiesto", "Add": "Aggiungi", @@ -22,14 +18,19 @@ "Attach input files": "Allega file di input", "Attribute name": "Nome attributo", "Auto accept prompt defaults": "Accetta automaticamente i valori predefiniti", - "CPU architecture": "Architettura CPU", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "L'API AWS Deadline Cloud non è accessibile. Verifica lo stato di autenticazione.", + "AWS Deadline Cloud submission": "Invio AWS Deadline Cloud", + "AWS Deadline Cloud workstation configuration": "Configurazione workstation AWS Deadline Cloud", + "AWS profile": "Profilo AWS", "Cancel": "Annulla", "Canceling submission...": "Annullamento invio...", "Cannot submit job:\n\n\u2022 {issues}": "Impossibile inviare il lavoro:\n\n\\u2022 {issues}", "Choose job bundle directory": "Scegli directory pacchetto lavoro", "Close": "Chiudi", + "Config File": "File di configurazione", "Conflict resolution option": "Opzione di risoluzione conflitti", "Copy": "Copia", + "CPU architecture": "Architettura CPU", "Current: {current_version} -> New: {latest_version}": "Corrente: {current_version} -> Nuovo: {latest_version}", "Current logging level": "Livello di registrazione corrente", "Custom host requirements": "Requisiti host personalizzati", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "Accesso ad AWS Deadline Cloud non riuscito:

{error}", "Farm": "Farm", "Farm settings": "Impostazioni farm", - "GPU memory (GiB)": "Memoria GPU (GiB)", - "GPUs": "GPU", "General settings": "Impostazioni generali", "General submission settings": "Impostazioni generali di invio", "Global settings": "Impostazioni globali", + "GPU memory (GiB)": "Memoria GPU (GiB)", + "GPUs": "GPU", "Hardware requirements": "Requisiti hardware", "Hashing progress": "Avanzamento hashing", "Help": "Aiuto", "Host requirements": "Requisiti host", "Initial state": "Stato iniziale", "Issue With Profile Configuration": "Problema con la configurazione del profilo", - "Job Properties": "Proprietà processo", - "Job Submission Confirmation": "Conferma invio lavoro", "Job attachments": "Allegati Job", "Job attachments filesystem options": "Opzioni filesystem allegati lavoro", "Job history directory": "Directory cronologia lavori", + "Job Properties": "Proprietà processo", + "Job Submission Confirmation": "Conferma invio lavoro", "Job submission confirmation": "Conferma invio lavoro", "Job-specific settings": "Impostazioni specifiche del lavoro", "Known asset paths": "Percorsi asset noti", "Language": "Lingua", "Language will change next time the submitter is opened": "La lingua cambierà alla prossima apertura del submitter", "Load Bundle": "Carica pacchetto", + "Load from Disk": "Carica da disco", "Log in": "Accedi", "Log in to AWS Deadline Cloud": "Accedi ad AWS Deadline Cloud", "Logging you in...": "Accesso in corso...", @@ -106,6 +108,7 @@ "Require all input paths exist": "Richiedi che tutti i percorsi di input esistano", "Run on all available worker hosts": "Esegui su tutti gli host worker disponibili", "Run on worker hosts that meet the following requirements": "Esegui su host worker che soddisfano i seguenti requisiti", + "Save to Disk": "Salva su disco", "Saved the submission as a job bundle:\n{path}": "Invio salvato come pacchetto lavoro:\n{path}", "Scratch space": "Spazio temporaneo", "Set max worker count": "Imposta numero massimo di worker", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - Hai effettuato il logout.", "{profile} doesn't have access permissions to submit a job.": "{profile} non dispone delle autorizzazioni di accesso per inviare un lavoro.", "{submitter} job submission": "Invio lavoro {submitter}" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/ja_JP.json b/src/deadline/client/ui/translations/locales/ja_JP.json index 8459dc03b..c2c1d9a33 100644 --- a/src/deadline/client/ui/translations/locales/ja_JP.json +++ b/src/deadline/client/ui/translations/locales/ja_JP.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "プロファイル '{profile}' の認証情報にアクセス中に設定エラーが発生しました。", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "AWS Deadline Cloud API にアクセスできません。認証ステータスを確認してください。", - "AWS Deadline Cloud submission": "AWS Deadline Cloud 送信", - "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud ワークステーション設定", - "AWS profile": "AWS プロファイル", "About": "バージョン情報", "Application Restart Required": "アプリケーションの再起動が必要です", "Add": "追加", @@ -22,14 +18,19 @@ "Attach input files": "入力ファイルを添付", "Attribute name": "属性名", "Auto accept prompt defaults": "デフォルト値を自動的に受け入れる", - "CPU architecture": "CPU アーキテクチャ", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "AWS Deadline Cloud API にアクセスできません。認証ステータスを確認してください。", + "AWS Deadline Cloud submission": "AWS Deadline Cloud 送信", + "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud ワークステーション設定", + "AWS profile": "AWS プロファイル", "Cancel": "キャンセル", "Canceling submission...": "送信をキャンセルしています...", "Cannot submit job:\n\n\u2022 {issues}": "ジョブを送信できません:\n\n\\u2022 {issues}", "Choose job bundle directory": "ジョブバンドルディレクトリを選択", "Close": "閉じる", + "Config File": "設定ファイル", "Conflict resolution option": "競合解決オプション", "Copy": "コピー", + "CPU architecture": "CPU アーキテクチャ", "Current: {current_version} -> New: {latest_version}": "現在: {current_version} -> 新規: {latest_version}", "Current logging level": "現在のログレベル", "Custom host requirements": "カスタムホスト要件", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "AWS Deadline Cloud へのログインに失敗しました:

{error}", "Farm": "ファーム", "Farm settings": "ファーム設定", - "GPU memory (GiB)": "GPU メモリ (GiB)", - "GPUs": "GPU", "General settings": "一般設定", "General submission settings": "一般送信設定", "Global settings": "グローバル設定", + "GPU memory (GiB)": "GPU メモリ (GiB)", + "GPUs": "GPU", "Hardware requirements": "ハードウェア要件", "Hashing progress": "ハッシュ進行状況", "Help": "ヘルプ", "Host requirements": "ホスト要件", "Initial state": "初期状態", "Issue With Profile Configuration": "プロファイル設定の問題", - "Job Properties": "ジョブプロパティ", - "Job Submission Confirmation": "ジョブ送信の確認", "Job attachments": "ジョブアタッチメント", "Job attachments filesystem options": "ジョブアタッチメントファイルシステムオプション", "Job history directory": "ジョブ履歴ディレクトリ", + "Job Properties": "ジョブプロパティ", + "Job Submission Confirmation": "ジョブ送信の確認", "Job submission confirmation": "ジョブ送信の確認", "Job-specific settings": "ジョブ固有の設定", "Known asset paths": "既知のアセットパス", "Language": "言語", "Language will change next time the submitter is opened": "言語は次回サブミッターを開いたときに変更されます", "Load Bundle": "バンドルを読み込む", + "Load from Disk": "ディスクから読み込み", "Log in": "ログイン", "Log in to AWS Deadline Cloud": "AWS Deadline Cloud にログイン", "Logging you in...": "ログイン中...", @@ -106,6 +108,7 @@ "Require all input paths exist": "すべての入力パスが存在することを要求", "Run on all available worker hosts": "使用可能なすべてのワーカーホストで実行", "Run on worker hosts that meet the following requirements": "次の要件を満たすワーカーホストで実行", + "Save to Disk": "ディスクに保存", "Saved the submission as a job bundle:\n{path}": "送信をジョブバンドルとして保存しました:\n{path}", "Scratch space": "一時領域", "Set max worker count": "最大ワーカー数を設定", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - ログアウトしています。", "{profile} doesn't have access permissions to submit a job.": "{profile} にはジョブを送信するアクセス許可がありません。", "{submitter} job submission": "{submitter} ジョブ送信" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/ko_KR.json b/src/deadline/client/ui/translations/locales/ko_KR.json index db3e7eb85..f256c12a2 100644 --- a/src/deadline/client/ui/translations/locales/ko_KR.json +++ b/src/deadline/client/ui/translations/locales/ko_KR.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "프로필 '{profile}'의 자격 증명에 액세스하는 동안 구성 오류가 발생했습니다.", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "AWS Deadline Cloud API에 액세스할 수 없습니다. 인증 상태를 확인하세요.", - "AWS Deadline Cloud submission": "AWS Deadline Cloud 제출", - "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud 워크스테이션 구성", - "AWS profile": "AWS 프로필", "About": "정보", "Application Restart Required": "애플리케이션 재시작 필요", "Add": "추가", @@ -22,14 +18,19 @@ "Attach input files": "입력 파일 연결", "Attribute name": "속성 이름", "Auto accept prompt defaults": "기본값 자동 수락", - "CPU architecture": "CPU 아키텍처", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "AWS Deadline Cloud API에 액세스할 수 없습니다. 인증 상태를 확인하세요.", + "AWS Deadline Cloud submission": "AWS Deadline Cloud 제출", + "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud 워크스테이션 구성", + "AWS profile": "AWS 프로필", "Cancel": "취소", "Canceling submission...": "제출 취소 중...", "Cannot submit job:\n\n\u2022 {issues}": "작업을 제출할 수 없습니다:\n\n\\u2022 {issues}", "Choose job bundle directory": "작업 번들 디렉터리 선택", "Close": "닫기", + "Config File": "구성 파일", "Conflict resolution option": "충돌 해결 옵션", "Copy": "복사", + "CPU architecture": "CPU 아키텍처", "Current: {current_version} -> New: {latest_version}": "현재: {current_version} -> 새 버전: {latest_version}", "Current logging level": "현재 로깅 수준", "Custom host requirements": "사용자 지정 호스트 요구 사항", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "AWS Deadline Cloud에 로그인하지 못했습니다:

{error}", "Farm": "팜", "Farm settings": "팜 설정", - "GPU memory (GiB)": "GPU 메모리(GiB)", - "GPUs": "GPU", "General settings": "일반 설정", "General submission settings": "일반 제출 설정", "Global settings": "전역 설정", + "GPU memory (GiB)": "GPU 메모리(GiB)", + "GPUs": "GPU", "Hardware requirements": "하드웨어 요구 사항", "Hashing progress": "해싱 진행률", "Help": "도움말", "Host requirements": "호스트 요구 사항", "Initial state": "초기 상태", "Issue With Profile Configuration": "프로필 구성 문제", - "Job Properties": "작업 속성", - "Job Submission Confirmation": "작업 제출 확인", "Job attachments": "작업 첨부 파일", "Job attachments filesystem options": "작업 첨부 파일 파일 시스템 옵션", "Job history directory": "작업 기록 디렉터리", + "Job Properties": "작업 속성", + "Job Submission Confirmation": "작업 제출 확인", "Job submission confirmation": "작업 제출 확인", "Job-specific settings": "작업별 설정", "Known asset paths": "알려진 자산 경로", "Language": "언어", "Language will change next time the submitter is opened": "언어는 다음에 제출자를 열 때 변경됩니다", "Load Bundle": "번들 로드", + "Load from Disk": "디스크에서 불러오기", "Log in": "로그인", "Log in to AWS Deadline Cloud": "AWS Deadline Cloud에 로그인", "Logging you in...": "로그인 중...", @@ -106,6 +108,7 @@ "Require all input paths exist": "모든 입력 경로가 존재해야 함", "Run on all available worker hosts": "사용 가능한 모든 작업자 호스트에서 실행", "Run on worker hosts that meet the following requirements": "다음 요구 사항을 충족하는 작업자 호스트에서 실행", + "Save to Disk": "디스크에 저장", "Saved the submission as a job bundle:\n{path}": "제출을 작업 번들로 저장했습니다:\n{path}", "Scratch space": "임시 공간", "Set max worker count": "최대 작업자 수 설정", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - 로그아웃되었습니다.", "{profile} doesn't have access permissions to submit a job.": "{profile}에 작업을 제출할 액세스 권한이 없습니다.", "{submitter} job submission": "{submitter} 작업 제출" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/pt_BR.json b/src/deadline/client/ui/translations/locales/pt_BR.json index 0a1cc251b..6ff7b7b94 100644 --- a/src/deadline/client/ui/translations/locales/pt_BR.json +++ b/src/deadline/client/ui/translations/locales/pt_BR.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "Um erro de configuração foi recebido ao acessar as credenciais do perfil '{profile}'.", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "A API do AWS Deadline Cloud não está acessível. Verifique seu status de autenticação.", - "AWS Deadline Cloud submission": "Envio do AWS Deadline Cloud", - "AWS Deadline Cloud workstation configuration": "Configuração de estação de trabalho do AWS Deadline Cloud", - "AWS profile": "Perfil da AWS", "About": "Sobre", "Application Restart Required": "Reinicialização do aplicativo necessária", "Add": "Adicionar", @@ -22,14 +18,19 @@ "Attach input files": "Anexar arquivos de entrada", "Attribute name": "Nome do atributo", "Auto accept prompt defaults": "Aceitar automaticamente padrões", - "CPU architecture": "Arquitetura da CPU", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "A API do AWS Deadline Cloud não está acessível. Verifique seu status de autenticação.", + "AWS Deadline Cloud submission": "Envio do AWS Deadline Cloud", + "AWS Deadline Cloud workstation configuration": "Configuração de estação de trabalho do AWS Deadline Cloud", + "AWS profile": "Perfil da AWS", "Cancel": "Cancelar", "Canceling submission...": "Cancelando envio...", "Cannot submit job:\n\n\u2022 {issues}": "Não é possível enviar o trabalho:\n\n\\u2022 {issues}", "Choose job bundle directory": "Escolher diretório do pacote de tarefas", "Close": "Fechar", + "Config File": "Arquivo de configuração", "Conflict resolution option": "Opção de resolução de conflitos", "Copy": "Copiar", + "CPU architecture": "Arquitetura da CPU", "Current: {current_version} -> New: {latest_version}": "Atual: {current_version} -> Novo: {latest_version}", "Current logging level": "Nível de registro atual", "Custom host requirements": "Requisitos de host personalizados", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "Falha ao fazer login no AWS Deadline Cloud:

{error}", "Farm": "Fazenda", "Farm settings": "Configurações da fazenda", - "GPU memory (GiB)": "Memória da GPU (GiB)", - "GPUs": "GPUs", "General settings": "Configurações gerais", "General submission settings": "Configurações gerais de envio", "Global settings": "Configurações globais", + "GPU memory (GiB)": "Memória da GPU (GiB)", + "GPUs": "GPUs", "Hardware requirements": "Requisitos de hardware", "Hashing progress": "Progresso de hash", "Help": "Ajuda", "Host requirements": "Requisitos de host", "Initial state": "Estado inicial", "Issue With Profile Configuration": "Problema com a configuração do perfil", - "Job Properties": "Propriedades do trabalho", - "Job Submission Confirmation": "Confirmação de envio de trabalho", "Job attachments": "Anexos de trabalho", "Job attachments filesystem options": "Opções do sistema de arquivos de anexos de tarefas", "Job history directory": "Diretório de histórico de trabalhos", + "Job Properties": "Propriedades do trabalho", + "Job Submission Confirmation": "Confirmação de envio de trabalho", "Job submission confirmation": "Confirmação de envio de trabalho", "Job-specific settings": "Configurações específicas do trabalho", "Known asset paths": "Caminhos de ativos conhecidos", "Language": "Idioma", "Language will change next time the submitter is opened": "O idioma será alterado na próxima vez que o submitter for aberto", "Load Bundle": "Carregar pacote", + "Load from Disk": "Carregar do disco", "Log in": "Fazer login", "Log in to AWS Deadline Cloud": "Fazer login no AWS Deadline Cloud", "Logging you in...": "Fazendo login...", @@ -106,6 +108,7 @@ "Require all input paths exist": "Exigir que todos os caminhos de entrada existam", "Run on all available worker hosts": "Executar em todos os hosts de trabalho disponíveis", "Run on worker hosts that meet the following requirements": "Executar em hosts de trabalho que atendam aos seguintes requisitos", + "Save to Disk": "Salvar em disco", "Saved the submission as a job bundle:\n{path}": "O envio foi salvo como um pacote de tarefas:\n{path}", "Scratch space": "Espaço temporário", "Set max worker count": "Definir contagem máxima de trabalhadores", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - Você está desconectado.", "{profile} doesn't have access permissions to submit a job.": "{profile} não tem permissões de acesso para enviar um trabalho.", "{submitter} job submission": "Envio de trabalho {submitter}" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/tr_TR.json b/src/deadline/client/ui/translations/locales/tr_TR.json index 618ede40c..e4a8f8aa6 100644 --- a/src/deadline/client/ui/translations/locales/tr_TR.json +++ b/src/deadline/client/ui/translations/locales/tr_TR.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "'{profile}' profili için kimlik bilgilerine erişilirken bir yapılandırma hatası alındı.", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "AWS Deadline Cloud API'sine erişilemiyor. Kimlik doğrulama durumunuzu kontrol edin.", - "AWS Deadline Cloud submission": "AWS Deadline Cloud gönderimi", - "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud iş istasyonu yapılandırması", - "AWS profile": "AWS profili", "About": "Hakkında", "Application Restart Required": "Uygulama yeniden başlatma gerekli", "Add": "Ekle", @@ -22,14 +18,19 @@ "Attach input files": "Giriş dosyalarını ekle", "Attribute name": "Öznitelik adı", "Auto accept prompt defaults": "Varsayılanları otomatik olarak kabul et", - "CPU architecture": "CPU mimarisi", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "AWS Deadline Cloud API'sine erişilemiyor. Kimlik doğrulama durumunuzu kontrol edin.", + "AWS Deadline Cloud submission": "AWS Deadline Cloud gönderimi", + "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud iş istasyonu yapılandırması", + "AWS profile": "AWS profili", "Cancel": "İptal", "Canceling submission...": "Gönderim iptal ediliyor...", "Cannot submit job:\n\n\u2022 {issues}": "İş gönderilemedi:\n\n\\u2022 {issues}", "Choose job bundle directory": "İş paketi dizini seçin", "Close": "Kapat", + "Config File": "Yapılandırma Dosyası", "Conflict resolution option": "Çakışma çözümleme seçeneği", "Copy": "Kopyala", + "CPU architecture": "CPU mimarisi", "Current: {current_version} -> New: {latest_version}": "Mevcut: {current_version} -> Yeni: {latest_version}", "Current logging level": "Geçerli günlük kaydı düzeyi", "Custom host requirements": "Özel ana bilgisayar gereksinimleri", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "AWS Deadline Cloud'da oturum açılamadı:

{error}", "Farm": "Farm", "Farm settings": "Farm ayarları", - "GPU memory (GiB)": "GPU belleği (GiB)", - "GPUs": "GPU'lar", "General settings": "Genel ayarlar", "General submission settings": "Genel gönderim ayarları", "Global settings": "Genel ayarlar", + "GPU memory (GiB)": "GPU belleği (GiB)", + "GPUs": "GPU'lar", "Hardware requirements": "Donanım gereksinimleri", "Hashing progress": "Karma oluşturma ilerlemesi", "Help": "Yardım", "Host requirements": "Ana bilgisayar gereksinimleri", "Initial state": "Başlangıç durumu", "Issue With Profile Configuration": "Profil yapılandırmasıyla ilgili sorun", - "Job Properties": "İş özellikleri", - "Job Submission Confirmation": "İş gönderimi onayı", "Job attachments": "İş ekleri", "Job attachments filesystem options": "İş ekleri dosya sistemi seçenekleri", "Job history directory": "İş geçmişi dizini", + "Job Properties": "İş özellikleri", + "Job Submission Confirmation": "İş gönderimi onayı", "Job submission confirmation": "İş gönderimi onayı", "Job-specific settings": "İşe özel ayarlar", "Known asset paths": "Bilinen varlık yolları", "Language": "Dil", "Language will change next time the submitter is opened": "Dil, submitter bir sonraki açılışında değişecektir", "Load Bundle": "Paket yükle", + "Load from Disk": "Diskten yükle", "Log in": "Oturum aç", "Log in to AWS Deadline Cloud": "AWS Deadline Cloud'da oturum aç", "Logging you in...": "Oturum açılıyor...", @@ -106,6 +108,7 @@ "Require all input paths exist": "Tüm giriş yollarının var olmasını gerektir", "Run on all available worker hosts": "Tüm kullanılabilir çalışan ana bilgisayarlarda çalıştır", "Run on worker hosts that meet the following requirements": "Aşağıdaki gereksinimleri karşılayan çalışan ana bilgisayarlarda çalıştır", + "Save to Disk": "Diske kaydet", "Saved the submission as a job bundle:\n{path}": "Gönderim iş paketi olarak kaydedildi:\n{path}", "Scratch space": "Geçici alan", "Set max worker count": "Maksimum çalışan sayısını ayarla", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - Oturumunuz kapatıldı.", "{profile} doesn't have access permissions to submit a job.": "{profile} bir iş göndermek için erişim izinlerine sahip değil.", "{submitter} job submission": "{submitter} iş gönderimi" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/zh_CN.json b/src/deadline/client/ui/translations/locales/zh_CN.json index 0b5a7e8e5..eaa9f69d1 100644 --- a/src/deadline/client/ui/translations/locales/zh_CN.json +++ b/src/deadline/client/ui/translations/locales/zh_CN.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "访问配置文件 '{profile}' 的凭证时收到配置错误。", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "无法访问 AWS Deadline Cloud API。请检查您的身份验证状态。", - "AWS Deadline Cloud submission": "AWS Deadline Cloud 提交", - "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud 工作站配置", - "AWS profile": "AWS 配置文件", "About": "关于", "Application Restart Required": "需要重新启动应用程序", "Add": "添加", @@ -22,14 +18,19 @@ "Attach input files": "附加输入文件", "Attribute name": "属性名称", "Auto accept prompt defaults": "自动接受默认值", - "CPU architecture": "CPU 架构", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "无法访问 AWS Deadline Cloud API。请检查您的身份验证状态。", + "AWS Deadline Cloud submission": "AWS Deadline Cloud 提交", + "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud 工作站配置", + "AWS profile": "AWS 配置文件", "Cancel": "取消", "Canceling submission...": "正在取消提交...", "Cannot submit job:\n\n\u2022 {issues}": "无法提交作业:\n\n\\u2022 {issues}", "Choose job bundle directory": "选择作业捆绑包目录", "Close": "关闭", + "Config File": "配置文件", "Conflict resolution option": "冲突解决选项", "Copy": "复制", + "CPU architecture": "CPU 架构", "Current: {current_version} -> New: {latest_version}": "当前: {current_version} -> 新版本: {latest_version}", "Current logging level": "当前日志记录级别", "Custom host requirements": "自定义主机要求", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "登录 AWS Deadline Cloud 失败:

{error}", "Farm": "服务器农场", "Farm settings": "服务器农场设置", - "GPU memory (GiB)": "GPU 内存 (GiB)", - "GPUs": "GPU", "General settings": "常规设置", "General submission settings": "常规提交设置", "Global settings": "全局设置", + "GPU memory (GiB)": "GPU 内存 (GiB)", + "GPUs": "GPU", "Hardware requirements": "硬件要求", "Hashing progress": "哈希进度", "Help": "帮助", "Host requirements": "主机要求", "Initial state": "初始状态", "Issue With Profile Configuration": "配置文件配置问题", - "Job Properties": "作业属性", - "Job Submission Confirmation": "作业提交确认", "Job attachments": "作业附件", "Job attachments filesystem options": "作业附件文件系统选项", "Job history directory": "作业历史记录目录", + "Job Properties": "作业属性", + "Job Submission Confirmation": "作业提交确认", "Job submission confirmation": "作业提交确认", "Job-specific settings": "作业特定设置", "Known asset paths": "已知资产路径", "Language": "语言", "Language will change next time the submitter is opened": "语言将在下次打开提交器时更改", "Load Bundle": "加载捆绑包", + "Load from Disk": "从磁盘加载", "Log in": "登录", "Log in to AWS Deadline Cloud": "登录 AWS Deadline Cloud", "Logging you in...": "正在登录...", @@ -106,6 +108,7 @@ "Require all input paths exist": "要求所有输入路径存在", "Run on all available worker hosts": "在所有可用的工作主机上运行", "Run on worker hosts that meet the following requirements": "在满足以下要求的工作主机上运行", + "Save to Disk": "保存到磁盘", "Saved the submission as a job bundle:\n{path}": "已将提交保存为作业捆绑包:\n{path}", "Scratch space": "临时空间", "Set max worker count": "设置最大工作线程数", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - 您已登出。", "{profile} doesn't have access permissions to submit a job.": "{profile} 没有提交作业的访问权限。", "{submitter} job submission": "{submitter} 作业提交" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/translations/locales/zh_TW.json b/src/deadline/client/ui/translations/locales/zh_TW.json index a44253123..cd717e03b 100644 --- a/src/deadline/client/ui/translations/locales/zh_TW.json +++ b/src/deadline/client/ui/translations/locales/zh_TW.json @@ -1,10 +1,6 @@ { "...": "...", "A configuration error was received while accessing credentials for the profile '{profile}'.": "存取設定檔 '{profile}' 的憑證時收到組態錯誤。", - "AWS Deadline Cloud API is not accessible. Check your authentication status.": "無法存取 AWS Deadline Cloud API。請檢查您的身分驗證狀態。", - "AWS Deadline Cloud submission": "AWS Deadline Cloud 提交", - "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud 工作站組態", - "AWS profile": "AWS 設定檔", "About": "關於", "Application Restart Required": "需要重新啟動應用程式", "Add": "新增", @@ -22,14 +18,19 @@ "Attach input files": "附加輸入檔案", "Attribute name": "屬性名稱", "Auto accept prompt defaults": "自動接受預設值", - "CPU architecture": "CPU 架構", + "AWS Deadline Cloud API is not accessible. Check your authentication status.": "無法存取 AWS Deadline Cloud API。請檢查您的身分驗證狀態。", + "AWS Deadline Cloud submission": "AWS Deadline Cloud 提交", + "AWS Deadline Cloud workstation configuration": "AWS Deadline Cloud 工作站組態", + "AWS profile": "AWS 設定檔", "Cancel": "取消", "Canceling submission...": "正在取消提交...", "Cannot submit job:\n\n\u2022 {issues}": "無法提交任務:\n\n\\u2022 {issues}", "Choose job bundle directory": "選擇任務套件目錄", "Close": "關閉", + "Config File": "設定檔", "Conflict resolution option": "衝突解決選項", "Copy": "複製", + "CPU architecture": "CPU 架構", "Current: {current_version} -> New: {latest_version}": "目前: {current_version} -> 新版本: {latest_version}", "Current logging level": "目前的日誌記錄層級", "Custom host requirements": "自訂主機需求", @@ -50,28 +51,29 @@ "Failed to log in to AWS Deadline Cloud:

{error}": "登入 AWS Deadline Cloud 失敗:

{error}", "Farm": "伺服器陣列", "Farm settings": "伺服器陣列設定", - "GPU memory (GiB)": "GPU 記憶體 (GiB)", - "GPUs": "GPU", "General settings": "一般設定", "General submission settings": "一般提交設定", "Global settings": "全域設定", + "GPU memory (GiB)": "GPU 記憶體 (GiB)", + "GPUs": "GPU", "Hardware requirements": "硬體需求", "Hashing progress": "雜湊進度", "Help": "說明", "Host requirements": "主機需求", "Initial state": "初始狀態", "Issue With Profile Configuration": "設定檔組態問題", - "Job Properties": "任務屬性", - "Job Submission Confirmation": "任務提交確認", "Job attachments": "任務附件", "Job attachments filesystem options": "任務附件檔案系統選項", "Job history directory": "任務歷史記錄目錄", + "Job Properties": "任務屬性", + "Job Submission Confirmation": "任務提交確認", "Job submission confirmation": "任務提交確認", "Job-specific settings": "任務特定設定", "Known asset paths": "已知資產路徑", "Language": "語言", "Language will change next time the submitter is opened": "語言將在下次開啟提交器時變更", "Load Bundle": "載入套件", + "Load from Disk": "從磁碟載入", "Log in": "登入", "Log in to AWS Deadline Cloud": "登入 AWS Deadline Cloud", "Logging you in...": "正在登入...", @@ -106,6 +108,7 @@ "Require all input paths exist": "要求所有輸入路徑存在", "Run on all available worker hosts": "在所有可用的工作者主機上執行", "Run on worker hosts that meet the following requirements": "在符合下列需求的工作者主機上執行", + "Save to Disk": "儲存到磁碟", "Saved the submission as a job bundle:\n{path}": "已將提交儲存為任務套件:\n{path}", "Scratch space": "暫存空間", "Set max worker count": "設定工作程序數上限", @@ -142,4 +145,4 @@ "{profile} - You are logged out.": "{profile} - 您已登出。", "{profile} doesn't have access permissions to submit a job.": "{profile} 沒有提交任務的存取許可。", "{submitter} job submission": "{submitter} 任務提交" -} \ No newline at end of file +} diff --git a/src/deadline/client/ui/widgets/shared_job_settings_tab.py b/src/deadline/client/ui/widgets/shared_job_settings_tab.py index fb873fb47..5ff04151c 100644 --- a/src/deadline/client/ui/widgets/shared_job_settings_tab.py +++ b/src/deadline/client/ui/widgets/shared_job_settings_tab.py @@ -7,6 +7,7 @@ from __future__ import annotations import sys +from configparser import ConfigParser from typing import Any, Dict, List, Optional from qtpy.QtCore import Qt, Signal # type: ignore @@ -58,11 +59,14 @@ def __init__( *, initial_settings: Any, initial_shared_parameter_values: dict[str, Any], + config: Optional[ConfigParser] = None, parent: Optional[QWidget] = None, ): super().__init__(parent=parent) layout = QVBoxLayout(self) + self._config = config + # This is a dictionary {: } containing values to # override the queue parameter defaults. self.initial_shared_parameter_values = initial_shared_parameter_values @@ -72,7 +76,9 @@ def __init__( ) layout.addWidget(self.shared_job_properties_box) - self.deadline_cloud_settings_box = DeadlineCloudSettingsWidget(parent=self) + self.deadline_cloud_settings_box = DeadlineCloudSettingsWidget( + config=self._config, parent=self + ) layout.addWidget(self.deadline_cloud_settings_box) self.queue_parameters_box = OpenJDParametersWidget( @@ -84,12 +90,14 @@ def __init__( ) # Track current farm/queue IDs for change detection - self.farm_id = get_setting("defaults.farm_id") - self.queue_id = get_setting("defaults.queue_id") + self.farm_id = get_setting("defaults.farm_id", config=self._config) + self.queue_id = get_setting("defaults.queue_id", config=self._config) self.__valid_queue = False # Connect to the controller for queue parameters self._controller = DeadlineUIController.getInstance() + if self._config is not None: + self._controller.set_config(self._config) self._controller.queue_parameters_updated.connect( self._handle_queue_parameters_update, Qt.QueuedConnection ) @@ -109,6 +117,14 @@ def __init__( if name.startswith("deadline:"): self.set_parameter_value({"name": name, "value": value}) + def set_session_config(self, session_config: ConfigParser) -> None: + """Update the session config used by this widget and its children.""" + self._config = session_config + self._controller.set_config(session_config) + self.deadline_cloud_settings_box._config = session_config + self.deadline_cloud_settings_box.farm_box._config = session_config + self.deadline_cloud_settings_box.queue_box._config = session_config + def refresh_ui(self, job_settings: Any, load_new_bundle: bool = False): # Refresh the job settings in the UI self.shared_job_properties_box.refresh_ui(job_settings) @@ -127,8 +143,8 @@ def refresh_queue_parameters(self, load_new_bundle: bool = False): """ If the default queue id or job bundle has changed, refresh the queue parameters. """ - farm_id = get_setting("defaults.farm_id") - queue_id = get_setting("defaults.queue_id") + farm_id = get_setting("defaults.farm_id", config=self._config) + queue_id = get_setting("defaults.queue_id", config=self._config) if not farm_id or not queue_id: self.queue_parameters_box.rebuild_ui(async_loading_state="") return # If the user has not selected a farm or queue ID, don't try to load @@ -164,8 +180,8 @@ def _start_load_queue_parameters(self): """ Triggers the controller to load queue parameters. """ - self.farm_id = farm_id = get_setting("defaults.farm_id") - self.queue_id = queue_id = get_setting("defaults.queue_id") + self.farm_id = farm_id = get_setting("defaults.farm_id", config=self._config) + self.queue_id = queue_id = get_setting("defaults.queue_id", config=self._config) if not self.farm_id or not self.queue_id: # If the user has not selected a farm or queue ID, don't bother loading return @@ -442,8 +458,14 @@ class DeadlineCloudSettingsWidget(QGroupBox): UI component for the Deadline Cloud settings. """ - def __init__(self, *, parent: Optional[QWidget] = None): + def __init__( + self, + *, + config: Optional[ConfigParser] = None, + parent: Optional[QWidget] = None, + ): super().__init__(tr("Deadline Cloud settings"), parent=parent) + self._config = config self.deadline_settings: Dict[str, Any] = {"counter": -1} self.layout = QFormLayout(self) self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) @@ -460,11 +482,11 @@ def _build_ui(self): Build the UI for the Deadline settings """ self.farm_box_label = QLabel(tr("Farm")) - self.farm_box = DeadlineFarmDisplay() + self.farm_box = DeadlineFarmDisplay(config=self._config) self.layout.addRow(self.farm_box_label, self.farm_box) self.queue_box_label = QLabel(tr("Queue")) - self.queue_box = DeadlineQueueDisplay() + self.queue_box = DeadlineQueueDisplay(config=self._config) self.layout.addRow(self.queue_box_label, self.queue_box) def refresh_setting_controls(self, deadline_authorized): @@ -499,12 +521,20 @@ class _DeadlineNamedResourceDisplay(QWidget): # provides (operation_name, BaseException) background_exception = Signal(str, BaseException) - def __init__(self, *, resource_name, setting_name, parent: Optional[QWidget] = None): + def __init__( + self, + *, + resource_name, + setting_name, + config: Optional[ConfigParser] = None, + parent: Optional[QWidget] = None, + ): super().__init__(parent=parent) self.resource_name = resource_name self.setting_name = setting_name - self.item_id = get_setting(self.setting_name) + self._config = config + self.item_id = get_setting(self.setting_name, config=self._config) self.item_name = "" self.item_description = "" @@ -544,7 +574,7 @@ def refresh(self, deadline_authorized): api.check_deadline_available, for example from an AWS Deadline Cloud Status Widget. """ - resource_id = get_setting(self.setting_name) + resource_id = get_setting(self.setting_name, config=self._config) if resource_id != self.item_id or not self.item_name: self.item_id = resource_id self.item_name = "" @@ -577,11 +607,13 @@ def _handle_item_update(self, item: tuple) -> None: class DeadlineFarmDisplay(_DeadlineNamedResourceDisplay): - def __init__(self, *, parent: Optional[QWidget] = None): - super().__init__(resource_name="Farm", setting_name="defaults.farm_id", parent=parent) + def __init__(self, *, config: Optional[ConfigParser] = None, parent: Optional[QWidget] = None): + super().__init__( + resource_name="Farm", setting_name="defaults.farm_id", config=config, parent=parent + ) def get_item(self): - farm_id = get_setting(self.setting_name) + farm_id = get_setting(self.setting_name, config=self._config) if farm_id: deadline = api.get_boto3_client("deadline") response = deadline.get_farm(farmId=farm_id) @@ -591,12 +623,14 @@ def get_item(self): class DeadlineQueueDisplay(_DeadlineNamedResourceDisplay): - def __init__(self, *, parent: Optional[QWidget] = None): - super().__init__(resource_name="Queue", setting_name="defaults.queue_id", parent=parent) + def __init__(self, *, config: Optional[ConfigParser] = None, parent: Optional[QWidget] = None): + super().__init__( + resource_name="Queue", setting_name="defaults.queue_id", config=config, parent=parent + ) def get_item(self): - farm_id = get_setting("defaults.farm_id") - queue_id = get_setting(self.setting_name) + farm_id = get_setting("defaults.farm_id", config=self._config) + queue_id = get_setting(self.setting_name, config=self._config) if farm_id and queue_id: deadline = api.get_boto3_client("deadline") response = deadline.get_queue(farmId=farm_id, queueId=queue_id) @@ -610,17 +644,18 @@ class DeadlineStorageProfileNameDisplay(_DeadlineNamedResourceDisplay): MAC_OS = "Macos" LINUX_OS = "Linux" - def __init__(self, *, parent: Optional[QWidget] = None): + def __init__(self, *, config: Optional[ConfigParser] = None, parent: Optional[QWidget] = None): super().__init__( resource_name="Storage profile name", setting_name="settings.storage_profile_id", + config=config, parent=parent, ) def get_item(self): - farm_id = get_setting("defaults.farm_id") - queue_id = get_setting("defaults.queue_id") - storage_profile_id = get_setting(self.setting_name) + farm_id = get_setting("defaults.farm_id", config=self._config) + queue_id = get_setting("defaults.queue_id", config=self._config) + storage_profile_id = get_setting(self.setting_name, config=self._config) if farm_id and queue_id and storage_profile_id: deadline = api.get_boto3_client("deadline") diff --git a/test/squish/suite_deadline_gui/shared/scripts/workstation_config_helpers.py b/test/squish/suite_deadline_gui/shared/scripts/workstation_config_helpers.py index 324d039e0..a09a60bf5 100644 --- a/test/squish/suite_deadline_gui/shared/scripts/workstation_config_helpers.py +++ b/test/squish/suite_deadline_gui/shared/scripts/workstation_config_helpers.py @@ -78,15 +78,6 @@ def open_settings_dialogue(): ) -def hit_apply_button(): - test.log("Hitting `Apply` button to apply selected settings.") - # hit 'Apply' button - squish.clickButton( - squish.waitForObject(workstation_config_locators.deadlinedialog_apply_button) - ) - test.log("Settings have been applied.") - - def set_farm_name(farm_name: str): # open Default farm drop down menu squish.mouseClick( diff --git a/test/squish/suite_deadline_gui/shared/scripts/workstation_config_locators.py b/test/squish/suite_deadline_gui/shared/scripts/workstation_config_locators.py index fedea6846..eaf72159d 100644 --- a/test/squish/suite_deadline_gui/shared/scripts/workstation_config_locators.py +++ b/test/squish/suite_deadline_gui/shared/scripts/workstation_config_locators.py @@ -15,14 +15,6 @@ "visible": 1, "window": deadline_config_dialog, } -# Apply button -deadlinedialog_apply_button = { - "text": "Apply", - "type": "QPushButton", - "unnamed": 1, - "visible": 1, - "window": deadline_config_dialog, -} # global settings box deadlinedialog_globalsettings_box = { diff --git a/test/unit/deadline_client/cli/test_cli_bundle_submit.py b/test/unit/deadline_client/cli/test_cli_bundle_submit.py index fd3fb0b03..abb0e00a7 100644 --- a/test/unit/deadline_client/cli/test_cli_bundle_submit.py +++ b/test/unit/deadline_client/cli/test_cli_bundle_submit.py @@ -119,7 +119,7 @@ def test_cli_bundle_explicit_parameters(fresh_deadline_config, temp_job_bundle_d ], ) - session_mock.assert_called_with(profile_name="NonDefaultProfileName", botocore_session=ANY) + session_mock.assert_any_call(profile_name="NonDefaultProfileName", botocore_session=ANY) session_mock().client().create_job.assert_called_once_with( farmId=MOCK_FARM_ID, queueId=MOCK_QUEUE_ID, diff --git a/test/unit/deadline_client/cli/test_cli_farm.py b/test/unit/deadline_client/cli/test_cli_farm.py index 682a3d18d..9d783af47 100644 --- a/test/unit/deadline_client/cli/test_cli_farm.py +++ b/test/unit/deadline_client/cli/test_cli_farm.py @@ -71,7 +71,7 @@ def test_cli_farm_list_override_profile(fresh_deadline_config): result = runner.invoke(main, ["farm", "list", "--profile", "NonDefaultProfileName"]) assert result.exit_code == 0 - session_mock.assert_called_with(profile_name="NonDefaultProfileName", botocore_session=ANY) + session_mock.assert_any_call(profile_name="NonDefaultProfileName", botocore_session=ANY) session_mock().client().list_farms.assert_called_once_with() diff --git a/test/unit/deadline_client/cli/test_cli_queue.py b/test/unit/deadline_client/cli/test_cli_queue.py index 31f10bcfb..75d10cf54 100644 --- a/test/unit/deadline_client/cli/test_cli_queue.py +++ b/test/unit/deadline_client/cli/test_cli_queue.py @@ -81,7 +81,7 @@ def test_cli_queue_list_override_profile(fresh_deadline_config): result = runner.invoke(main, ["queue", "list", "--profile", "NonDefaultProfileName"]) assert result.exit_code == 0 - session_mock.assert_called_with(profile_name="NonDefaultProfileName", botocore_session=ANY) + session_mock.assert_any_call(profile_name="NonDefaultProfileName", botocore_session=ANY) session_mock().client().list_queues.assert_called_once_with(farmId="farm-overriddenid") diff --git a/test/unit/deadline_client/config/test_config_file.py b/test/unit/deadline_client/config/test_config_file.py index 5f3f7f14d..591fa53de 100644 --- a/test/unit/deadline_client/config/test_config_file.py +++ b/test/unit/deadline_client/config/test_config_file.py @@ -371,3 +371,72 @@ def test_posix_config_file_permissions(fresh_deadline_config) -> None: config_file.set_setting("defaults.aws_profile_name", "goodguyprofile") assert config_file_path.stat().st_mode & 0o777 == 0o600 + + +def test_persist_job_id_with_default_config(fresh_deadline_config) -> None: + """persist_job_id writes the job ID under the specified farm/queue section.""" + config.set_setting("defaults.farm_id", "farm-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + config.set_setting("defaults.queue_id", "queue-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") + + config_file.persist_job_id( + "job-ccccccccccccccccccccccccccccccc", + profile="(default)", + farm_id="farm-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + queue_id="queue-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + ) + + assert config.get_setting("defaults.job_id") == "job-ccccccccccccccccccccccccccccccc" + + +def test_persist_job_id_with_overridden_farm_queue(fresh_deadline_config) -> None: + """persist_job_id writes the job ID under the overridden farm/queue section + without changing the on-disk farm/queue defaults.""" + # Set up on-disk defaults + config.set_setting("defaults.farm_id", "farm-disk1111111111111111111111111") + config.set_setting("defaults.queue_id", "queue-disk111111111111111111111111") + + # Persist job ID under a different farm/queue + config_file.persist_job_id( + "job-submitted111111111111111111111", + profile="(default)", + farm_id="farm-cli11111111111111111111111111", + queue_id="queue-cli1111111111111111111111111", + ) + + # On-disk farm/queue defaults are unchanged + assert config.get_setting("defaults.farm_id") == "farm-disk1111111111111111111111111" + assert config.get_setting("defaults.queue_id") == "queue-disk111111111111111111111111" + + # Job ID under the default farm/queue section is NOT set + assert config.get_setting("defaults.job_id") == "" + + # Job ID IS set when we read using the overridden farm/queue + from configparser import ConfigParser + + session = ConfigParser() + session.read_dict(config_file.read_config()) + config_file.set_setting( + "defaults.farm_id", "farm-cli11111111111111111111111111", config=session + ) + config_file.set_setting( + "defaults.queue_id", "queue-cli1111111111111111111111111", config=session + ) + assert ( + config_file.get_setting("defaults.job_id", config=session) + == "job-submitted111111111111111111111" + ) + + +def test_persist_job_id_creates_section_if_missing(fresh_deadline_config) -> None: + """persist_job_id creates the section if it doesn't exist yet.""" + config_file.persist_job_id( + "job-newwwwwwwwwwwwwwwwwwwwwwwwwwwwww", + profile="(default)", + farm_id="farm-newwwwwwwwwwwwwwwwwwwwwwwwwwww", + queue_id="queue-newwwwwwwwwwwwwwwwwwwwwwwwwww", + ) + + # Re-read from disk to confirm it was persisted + fresh = config_file.read_config() + section = "profile-(default) farm-newwwwwwwwwwwwwwwwwwwwwwwwwwww queue-newwwwwwwwwwwwwwwwwwwwwwwwwww defaults" + assert fresh.get(section, "job_id") == "job-newwwwwwwwwwwwwwwwwwwwwwwwwwwwww" diff --git a/test/unit/deadline_client/ui/test_session_config.py b/test/unit/deadline_client/ui/test_session_config.py new file mode 100644 index 000000000..c34b032ab --- /dev/null +++ b/test/unit/deadline_client/ui/test_session_config.py @@ -0,0 +1,527 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +""" +Tests for in-memory session config behavior. + +Validates that CLI options (--farm-id, --queue-id, etc.) create a session config +that is used by the submission dialog without affecting the on-disk config, and +that the settings dialog correctly separates session vs workstation config. +""" + +from configparser import ConfigParser +from typing import Callable, Generator, Optional +from unittest.mock import MagicMock, PropertyMock, patch + +import pytest + +try: + from qtpy.QtWidgets import QWidget + + from deadline.client.config import config_file + from deadline.client.cli._common import _apply_cli_options_to_config + from deadline.client.ui.dataclasses import JobBundleSettings + from deadline.client.job_bundle.submission import AssetReferences + from deadline.client.ui.dialogs.submit_job_to_deadline_dialog import ( + SubmitJobToDeadlineDialog, + ) + from deadline.client.ui.dialogs.deadline_config_dialog import ( + DeadlineConfigDialog, + DeadlineWorkstationConfigWidget, + ) +except ImportError: + pytest.importorskip("deadline.client.ui.dialogs.submit_job_to_deadline_dialog") + +DISK_FARM_ID = "farm-disk11111111111111111111111111" +DISK_QUEUE_ID = "queue-disk1111111111111111111111111" +CLI_FARM_ID = "farm-cli111111111111111111111111111" +CLI_QUEUE_ID = "queue-cli11111111111111111111111111" + + +class MockJobSettingsWidget(QWidget): + def __init__( + self, initial_settings: Optional[JobBundleSettings] = None, parent: Optional[QWidget] = None + ): + super().__init__(parent) + self.initial_settings = initial_settings + self.parameter_changed = MagicMock() + self.parameter_changed.connect = MagicMock() + + def update_settings(self, settings: JobBundleSettings) -> None: + pass + + +def _mock_auth_instance() -> MagicMock: + """Create a mock DeadlineAuthenticationStatus that won't trigger real API calls.""" + inst = MagicMock() + inst.api_availability = False + inst.api_availability_changed = MagicMock() + inst.api_availability_changed.connect = MagicMock() + inst.deadline_config_changed = MagicMock() + inst.deadline_config_changed.connect = MagicMock() + return inst + + +@pytest.fixture +def mock_auth_status() -> MagicMock: + mock_instance = MagicMock() + type(mock_instance).api_availability = PropertyMock(return_value=None) + type(mock_instance).creds_source = PropertyMock(return_value=None) + type(mock_instance).auth_status = PropertyMock(return_value=None) + mock_instance.config = ConfigParser() + mock_instance.api_availability_changed = MagicMock() + mock_instance.api_availability_changed.connect = MagicMock() + mock_instance.creds_source_changed = MagicMock() + mock_instance.creds_source_changed.connect = MagicMock() + mock_instance.auth_status_changed = MagicMock() + mock_instance.auth_status_changed.connect = MagicMock() + return mock_instance + + +@pytest.fixture +def disk_config() -> ConfigParser: + """A ConfigParser representing the on-disk config with known farm/queue.""" + config = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=config) + config_file.set_setting("defaults.queue_id", DISK_QUEUE_ID, config=config) + return config + + +@pytest.fixture +def submit_dialog_factory(qtbot, mock_auth_status) -> Callable: + """Factory that creates a SubmitJobToDeadlineDialog with mocked auth.""" + + def _create(session_config: Optional[ConfigParser] = None) -> "SubmitJobToDeadlineDialog": + import deadline.client.ui.deadline_authentication_status as auth_module + + auth_module._deadline_authentication_status = mock_auth_status + + with patch( + "deadline.client.ui.widgets.deadline_authentication_status_widget.DeadlineAuthenticationStatus.getInstance", + return_value=mock_auth_status, + ), patch( + "deadline.client.ui.dialogs.submit_job_to_deadline_dialog.DeadlineAuthenticationStatus.getInstance", + return_value=mock_auth_status, + ): + dialog = SubmitJobToDeadlineDialog( + job_setup_widget_type=MockJobSettingsWidget, + initial_job_settings=JobBundleSettings(), + initial_shared_parameter_values={}, + auto_detected_attachments=AssetReferences(), + attachments=AssetReferences(), + on_create_job_bundle_callback=MagicMock(), + session_config=session_config, + ) + qtbot.addWidget(dialog) + return dialog + + return _create + + +@pytest.fixture +def config_dialog_mocks() -> Generator[MagicMock, None, None]: + """Patches config_file, boto3.Session, and auth for DeadlineConfigDialog tests. + + Yields mock_config_file after setting up sane defaults. + Callers can override mock_config_file.read_config.return_value etc. as needed. + """ + with patch( + "deadline.client.ui.dialogs.deadline_config_dialog.DeadlineAuthenticationStatus.getInstance" + ) as mock_auth, patch( + "deadline.client.ui.dialogs.deadline_config_dialog.boto3.Session" + ) as mock_boto3_session, patch( + "deadline.client.ui.dialogs.deadline_config_dialog.config_file" + ) as mock_config_file: + mock_auth.return_value = _mock_auth_instance() + mock_boto3_session.return_value._session.full_config = {"profiles": {}} + mock_config_file.read_config.return_value = ConfigParser() + mock_config_file.get_setting = config_file.get_setting + mock_config_file.set_setting = config_file.set_setting + mock_config_file.write_config = MagicMock() + yield mock_config_file + + +class TestApplyCliOptionsToConfig: + """Tests for _apply_cli_options_to_config override and copy behavior.""" + + def test_returns_config_when_no_options(self) -> None: + """When no CLI options are provided, returns a copy of the disk config.""" + result = _apply_cli_options_to_config() + assert isinstance(result, ConfigParser) + + def test_overrides_farm_id(self, disk_config) -> None: + session = ConfigParser() + session.read_dict(disk_config) + result = _apply_cli_options_to_config(config=session, farm_id=CLI_FARM_ID) + assert config_file.get_setting("defaults.farm_id", config=result) == CLI_FARM_ID + # queue_id is scoped to the farm, so changing farm clears the queue + assert config_file.get_setting("defaults.queue_id", config=result) == "" + + def test_overrides_queue_id(self, disk_config) -> None: + session = ConfigParser() + session.read_dict(disk_config) + result = _apply_cli_options_to_config(config=session, queue_id=CLI_QUEUE_ID) + assert config_file.get_setting("defaults.queue_id", config=result) == CLI_QUEUE_ID + assert config_file.get_setting("defaults.farm_id", config=result) == DISK_FARM_ID + + def test_overrides_both(self, disk_config) -> None: + session = ConfigParser() + session.read_dict(disk_config) + result = _apply_cli_options_to_config( + config=session, farm_id=CLI_FARM_ID, queue_id=CLI_QUEUE_ID + ) + assert config_file.get_setting("defaults.farm_id", config=result) == CLI_FARM_ID + assert config_file.get_setting("defaults.queue_id", config=result) == CLI_QUEUE_ID + + def test_does_not_mutate_disk_cache(self) -> None: + """When no config is passed, reads from disk and returns a fresh copy.""" + with patch.object(config_file, "read_config") as mock_read: + base = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=base) + mock_read.return_value = base + + result = _apply_cli_options_to_config(farm_id=CLI_FARM_ID) + + assert config_file.get_setting("defaults.farm_id", config=result) == CLI_FARM_ID + assert result is not base + assert config_file.get_setting("defaults.farm_id", config=base) == DISK_FARM_ID + + def test_does_not_mutate_provided_config(self) -> None: + """When a config is explicitly passed, it is copied — the original is not mutated.""" + original = ConfigParser() + config_file.set_setting("defaults.queue_id", DISK_QUEUE_ID, config=original) + + result = _apply_cli_options_to_config(config=original, queue_id=CLI_QUEUE_ID) + + assert result is not original + assert config_file.get_setting("defaults.queue_id", config=result) == CLI_QUEUE_ID + assert config_file.get_setting("defaults.queue_id", config=original) == DISK_QUEUE_ID + + +class TestSubmitDialogSessionConfig: + """Tests for session config propagation through SubmitJobToDeadlineDialog.""" + + def test_no_session_config_when_no_cli_args(self, submit_dialog_factory) -> None: + dialog = submit_dialog_factory(session_config=None) + assert dialog._session_config is None + + def test_session_config_stored_when_provided(self, submit_dialog_factory, disk_config) -> None: + session = _apply_cli_options_to_config(config=disk_config, queue_id=CLI_QUEUE_ID) + dialog = submit_dialog_factory(session_config=session) + + assert dialog._session_config is not None + assert ( + config_file.get_setting("defaults.queue_id", config=dialog._session_config) + == CLI_QUEUE_ID + ) + + def test_session_config_passed_to_shared_job_settings( + self, submit_dialog_factory, disk_config + ) -> None: + session = _apply_cli_options_to_config(config=disk_config, farm_id=CLI_FARM_ID) + dialog = submit_dialog_factory(session_config=session) + + assert dialog.shared_job_settings._config is session + + def test_session_config_passed_to_display_widgets( + self, submit_dialog_factory, disk_config + ) -> None: + """Session config should reach farm_box and queue_box through the widget hierarchy.""" + session = _apply_cli_options_to_config(config=disk_config, farm_id=CLI_FARM_ID) + dialog = submit_dialog_factory(session_config=session) + + settings_box = dialog.shared_job_settings.deadline_cloud_settings_box + assert settings_box._config is session + assert settings_box.farm_box._config is session + assert settings_box.queue_box._config is session + + +@pytest.mark.usefixtures("fresh_deadline_config") +class TestWorkstationConfigWidget: + """Tests for DeadlineWorkstationConfigWidget session vs disk config behavior.""" + + def test_no_session_config_by_default(self, config_dialog_mocks, qtbot) -> None: + widget = DeadlineWorkstationConfigWidget() + qtbot.addWidget(widget) + assert widget._session_config is None + + def test_has_session_config_when_provided(self, config_dialog_mocks, qtbot) -> None: + session = ConfigParser() + config_file.set_setting("defaults.queue_id", CLI_QUEUE_ID, config=session) + + widget = DeadlineWorkstationConfigWidget(session_config=session) + qtbot.addWidget(widget) + assert widget._session_config is not None + + def test_refresh_uses_session_config(self, config_dialog_mocks, qtbot) -> None: + """In session mode, refresh() bases the config on the session config, not disk.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + mock_cf.read_config.return_value = disk + + session = ConfigParser() + config_file.set_setting("defaults.farm_id", CLI_FARM_ID, config=session) + + widget = DeadlineWorkstationConfigWidget(session_config=session) + qtbot.addWidget(widget) + assert config_file.get_setting("defaults.farm_id", config=widget.config) == CLI_FARM_ID + + def test_refresh_uses_disk_config_without_session(self, config_dialog_mocks, qtbot) -> None: + """Without session config, refresh() bases the config on disk.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + mock_cf.read_config.return_value = disk + + widget = DeadlineWorkstationConfigWidget() + qtbot.addWidget(widget) + assert config_file.get_setting("defaults.farm_id", config=widget.config) == DISK_FARM_ID + + def test_apply_with_session_does_not_write_to_disk(self, config_dialog_mocks, qtbot) -> None: + """With session config, apply() mutates in-memory without writing to disk.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + mock_cf.read_config.return_value = disk + + session = ConfigParser() + config_file.set_setting("defaults.farm_id", CLI_FARM_ID, config=session) + + widget = DeadlineWorkstationConfigWidget(session_config=session) + qtbot.addWidget(widget) + + widget.changes["defaults.farm_id"] = "farm-newvalue1111111111111111111111" + assert widget.apply() is True + mock_cf.write_config.assert_not_called() + assert ( + config_file.get_setting("defaults.farm_id", config=session) + == "farm-newvalue1111111111111111111111" + ) + + def test_apply_without_session_writes_to_disk(self, config_dialog_mocks, qtbot) -> None: + """Without session config, apply() writes to disk.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + mock_cf.read_config.return_value = disk + + widget = DeadlineWorkstationConfigWidget() + qtbot.addWidget(widget) + + widget.changes["defaults.farm_id"] = "farm-newvalue1111111111111111111111" + assert widget.apply() is True + mock_cf.write_config.assert_called_once() + + +@pytest.mark.usefixtures("fresh_deadline_config") +class TestConfigDialog: + """Tests for DeadlineConfigDialog button bar and disk I/O actions.""" + + def test_disk_actions_disabled_without_session(self, config_dialog_mocks, qtbot) -> None: + """Without session_config, disk actions start disabled.""" + dialog = DeadlineConfigDialog() + qtbot.addWidget(dialog) + assert not dialog._load_from_disk_action.isEnabled() + + def test_config_file_button_exists_with_session(self, config_dialog_mocks, qtbot) -> None: + """With session_config, Config File button is present.""" + session = ConfigParser() + dialog = DeadlineConfigDialog(session_config=session) + qtbot.addWidget(dialog) + assert dialog._config_file_button is not None + + def test_session_mode_does_not_write_to_disk(self, config_dialog_mocks, qtbot) -> None: + """With session_config, the widget is in session mode and doesn't write to disk.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + mock_cf.read_config.return_value = disk + + session = ConfigParser() + session.read_dict(disk) + + dialog = DeadlineConfigDialog(session_config=session) + qtbot.addWidget(dialog) + + assert dialog.config_box._session_config is not None + mock_cf.write_config.assert_not_called() + + def test_session_config_shows_cli_override(self, config_dialog_mocks, qtbot) -> None: + """CLI --queue-id override is visible in the dialog's config.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + config_file.set_setting("defaults.queue_id", DISK_QUEUE_ID, config=disk) + mock_cf.read_config.return_value = disk + + session = ConfigParser() + session.read_dict(disk) + config_file.set_setting("defaults.queue_id", CLI_QUEUE_ID, config=session) + + dialog = DeadlineConfigDialog(session_config=session) + qtbot.addWidget(dialog) + + assert ( + config_file.get_setting("defaults.queue_id", config=dialog.config_box.config) + == CLI_QUEUE_ID + ) + + def test_save_to_disk_with_session_writes_config(self, config_dialog_mocks, qtbot) -> None: + """Save to Disk applies pending changes to session config and writes to disk.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + mock_cf.read_config.return_value = disk + + session = ConfigParser() + session.read_dict(disk) + dialog = DeadlineConfigDialog(session_config=session) + qtbot.addWidget(dialog) + + dialog.config_box.changes["defaults.farm_id"] = CLI_FARM_ID + dialog._on_save_to_disk() + + mock_cf.write_config.assert_called_once() + assert config_file.get_setting("defaults.farm_id", config=session) == CLI_FARM_ID + assert not dialog.config_box.changes + + def test_save_to_disk_without_session_writes_config(self, config_dialog_mocks, qtbot) -> None: + """Save to Disk reads from disk, applies changes, and writes back.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + mock_cf.read_config.return_value = disk + + dialog = DeadlineConfigDialog() + qtbot.addWidget(dialog) + + dialog.config_box.changes["defaults.farm_id"] = CLI_FARM_ID + dialog._on_save_to_disk() + + mock_cf.write_config.assert_called_once() + assert not dialog.config_box.changes + + def test_save_to_disk_blocks_on_not_valid(self, config_dialog_mocks, qtbot) -> None: + """Save to Disk should not write when changes contain NOT_VALID_MARKER.""" + mock_cf = config_dialog_mocks + + dialog = DeadlineConfigDialog() + qtbot.addWidget(dialog) + + dialog.config_box.changes["defaults.aws_profile_name"] = "[NOT VALID] bad" + with patch("deadline.client.ui.dialogs.deadline_config_dialog.QMessageBox.warning"): + dialog._on_save_to_disk() + + mock_cf.write_config.assert_not_called() + + def test_load_from_disk_with_session_replaces_session(self, config_dialog_mocks, qtbot) -> None: + """Load from Disk replaces the session config with disk contents.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + mock_cf.read_config.return_value = disk + + session = ConfigParser() + session.read_dict(disk) + config_file.set_setting("defaults.farm_id", CLI_FARM_ID, config=session) + + dialog = DeadlineConfigDialog(session_config=session) + qtbot.addWidget(dialog) + + dialog._on_load_from_disk() + + assert ( + config_file.get_setting("defaults.farm_id", config=dialog._session_config) + == DISK_FARM_ID + ) + + def test_load_from_disk_without_session_clears_changes( + self, config_dialog_mocks, qtbot + ) -> None: + """Load from Disk discards pending changes.""" + mock_cf = config_dialog_mocks + disk = ConfigParser() + config_file.set_setting("defaults.farm_id", DISK_FARM_ID, config=disk) + mock_cf.read_config.return_value = disk + + dialog = DeadlineConfigDialog() + qtbot.addWidget(dialog) + + dialog.config_box.changes["defaults.farm_id"] = CLI_FARM_ID + dialog._on_load_from_disk() + + assert not dialog.config_box.changes + + def test_accept_blocks_on_not_valid_with_session(self, config_dialog_mocks, qtbot) -> None: + """Ok should not close the dialog when session-mode changes are not valid.""" + session = ConfigParser() + dialog = DeadlineConfigDialog(session_config=session) + qtbot.addWidget(dialog) + + dialog.config_box.changes["defaults.aws_profile_name"] = "[NOT VALID] bad" + with patch("deadline.client.ui.dialogs.deadline_config_dialog.QMessageBox.warning"): + with patch.object(type(dialog).mro()[1], "accept") as mock_super_accept: + dialog.accept() + mock_super_accept.assert_not_called() + + def test_accept_blocks_on_not_valid_without_session(self, config_dialog_mocks, qtbot) -> None: + """Ok should not close the dialog when workstation-mode changes are not valid.""" + mock_cf = config_dialog_mocks + + dialog = DeadlineConfigDialog() + qtbot.addWidget(dialog) + + dialog.config_box.changes["defaults.aws_profile_name"] = "[NOT VALID] bad" + with patch("deadline.client.ui.dialogs.deadline_config_dialog.QMessageBox.warning"): + with patch.object(type(dialog).mro()[1], "accept") as mock_super_accept: + dialog.accept() + mock_super_accept.assert_not_called() + mock_cf.write_config.assert_not_called() + + +class TestSubmitDialogPersistJobId: + """Tests for job ID persistence after successful GUI submission.""" + + def test_persist_job_id_uses_session_config_values( + self, submit_dialog_factory, disk_config + ) -> None: + """After submission, persist_job_id is called with profile/farm/queue from session config.""" + session = _apply_cli_options_to_config( + config=disk_config, farm_id=CLI_FARM_ID, queue_id=CLI_QUEUE_ID + ) + dialog = submit_dialog_factory(session_config=session) + + with patch( + "deadline.client.ui.dialogs.submit_job_to_deadline_dialog.persist_job_id" + ) as mock_persist: + dialog._submission_succeeded_signal_receiver("job-test1111111111111111111111111") + + mock_persist.assert_called_once_with( + "job-test1111111111111111111111111", + profile=config_file.get_setting("defaults.aws_profile_name", config=session), + farm_id=CLI_FARM_ID, + queue_id=CLI_QUEUE_ID, + ) + + +class TestSetSessionConfigPropagation: + """Tests that set_session_config propagates to child display widgets.""" + + def test_set_session_config_propagates_to_child_widgets( + self, submit_dialog_factory, disk_config + ) -> None: + """set_session_config should update config on farm_box and queue_box.""" + session = _apply_cli_options_to_config(config=disk_config, farm_id=CLI_FARM_ID) + dialog = submit_dialog_factory(session_config=session) + + new_session = ConfigParser() + new_session.read_dict(disk_config) + config_file.set_setting( + "defaults.farm_id", "farm-new111111111111111111111111111", config=new_session + ) + + dialog.shared_job_settings.set_session_config(new_session) + + settings_box = dialog.shared_job_settings.deadline_cloud_settings_box + assert settings_box._config is new_session + assert settings_box.farm_box._config is new_session + assert settings_box.queue_box._config is new_session