synology_apm_repo.sdk.profiles package

Saved S3/Azure/SMB connection profiles — shared, identically, by the CLI and TUI, exactly like presentation/ is shared for render-identically output.

This is cross-cutting infrastructure outside the layer stack: it produces inputs to storage/’s S3Store/AzureStore, it does no ObjectStore I/O itself, and it has nothing to do with Session/keys/ NodeRef.

Every function below is async def and a plain module-level function (not a class method) — async because the underlying work (profiles.json file I/O, OS keyring access) is exactly the kind of blocking call LocalFsStore already wraps in asyncio.to_thread() (there is no async-native alternative, same as pread/fstat), and blocking a TUI’s event loop on a keyring prompt would freeze the whole UI; plain functions so tests (and callers) can monkeypatch.setattr(this_module, "list_profiles", fake) exactly like storage.s3.list_buckets already supports. A config_dir keyword lets tests point at a temp directory instead of the real per-user config path, standing in for constructor-based dependency injection.

Two secret-aware entry points read differently, deliberately: get_profile() never touches the keyring (so list/show-shaped callers structurally cannot leak a secret), while load_profile() does, returning every field flattened into one dict for refilling a connection form.

class synology_apm_repo.sdk.profiles.AzureProfileConfig(container, account_url=None, verify_tls=True)

Bases: object

Non-secret AzureStore constructor inputs. credential is not a field here — see secrets.py.

container: str
account_url: str | None = None
verify_tls: bool = True
property client_kwargs: dict[str, Any]

Everything but container and the secret field, already in AzureStore/azure-storage-blob’s own kwarg names — note connection_verify, not verify (azure.core.Configuration’s own name for this, deliberately different from boto3’s).

property display_fields: dict[str, str | None]

Same role as S3ProfileConfig.display_fields.

class synology_apm_repo.sdk.profiles.BackendKind(value)

Bases: Enum

Which backend a profile targets.

S3 = 's3'
AZURE = 'azure'
SMB = 'smb'
class synology_apm_repo.sdk.profiles.Profile(name, kind, config)

Bases: object

One saved profile, keyed by its user-chosen name (unique across every backend kind). Never carries secret values.

name: str
kind: BackendKind
config: S3ProfileConfig | AzureProfileConfig | SmbProfileConfig
class synology_apm_repo.sdk.profiles.ProfileFieldSpec(name, strip=True, is_checkbox=False)

Bases: object

One profile field’s widget-binding shape, in canonical collection/ display order, for one backend — the single place a connection form (the TUI’s connect/saved-profile tabs today) reads a backend’s full field list from, instead of privately re-enumerating it. strip is False for a value whose leading/trailing whitespace might be significant (secret_key/credential/password) — not simply every secret field, since access_key (also keyring-backed, per secret_fields_for) has no such concern and is stripped like an ordinary field.

name: str
strip: bool = True
is_checkbox: bool = False
class synology_apm_repo.sdk.profiles.ProfileSummary(name, kind)

Bases: object

Cheap listing entry — derived from profiles.json alone, no keyring access needed to produce it.

name: str
kind: BackendKind
class synology_apm_repo.sdk.profiles.S3ProfileConfig(bucket, endpoint=None, region=None, verify_tls=True)

Bases: object

Non-secret S3Store constructor inputs. access_key/ secret_key are not fields here — see secrets.py.

bucket: str
endpoint: str | None = None
region: str | None = None
verify_tls: bool = True
property client_kwargs: dict[str, Any]

Everything but bucket and the secret fields, already in S3Store/aioboto3’s own kwarg names. Callers merge the resolved secrets in on top of this dict.

property display_fields: dict[str, str | None]

Ordered field-name -> value pairs for CLI/TUI display (a saved profile’s JSON and human-readable show output alike) — the one place this backend’s own display field list is spelled out, so both renderers read it from here instead of each re-declaring it.

class synology_apm_repo.sdk.profiles.SmbProfileConfig(server, share, port=445, username=None)

Bases: object

Non-secret SmbStore constructor inputs. password is not a field here — see secrets.py.

username takes the Windows-native DOMAIN\username (or user@domain UPN) form directly — smbprotocol’s own NTLM/SPNEGO layer splits the domain back out of that single string, so there is no separate domain field to keep in sync with it. No path/sub-root field either, matching S3ProfileConfig/AzureProfileConfig: a share, like a bucket/container, is the whole scope one profile names.

server: str
share: str
port: int = 445
username: str | None = None
property client_kwargs: dict[str, Any]

Everything but share and the secret field, already in SmbStore’s own kwarg names. Callers merge the resolved secret in on top of this dict.

property display_fields: dict[str, str | int | None]

Same role as S3ProfileConfig.display_fields.

async synology_apm_repo.sdk.profiles.build_store(name, *, config_dir=None)

The end-to-end convenience form: profile name’s config and keyring secrets, resolved into an already-constructed S3Store/ AzureStore/SmbStore via store_from_config — ready for Session.discover_remote()/open_remote(). The caller still owns calling Session.close(), which calls aclose() on it.

synology_apm_repo.sdk.profiles.client_kwargs_with_secrets(config, secrets)

config.client_kwargs, overlaid with whichever of secret_fields_for(kind)’s fields secrets actually carries a truthy value for — the shared shape behind every “resolve one profile’s config plus its secrets into constructor kwargs” call site (profiles.build_store, cli/commands/profile.py::_store_from_fields, browser/screens/connect_dialog.py’s s3_client_kwargs/ azure_client_kwargs — SMB has no bucket-less/container-less “Browse” counterpart calling this directly, only smb_config_and_secrets feeding store_from_config straight from _build_smb_store). secrets may be a superset of what applies here (a not-yet-saved connection form carries every backend’s fields at once) — only the keys secret_fields_for names for config’s own kind are ever read from it; a present-but-falsy value (an empty string from an untouched form field) is treated the same as absent, matching every prior hand-rolled version of this check.

synology_apm_repo.sdk.profiles.config_from_fields(kind, fields)

fields (save_profile()’s canonical field names, e.g. as collected by a connection form or the CLI’s profile add prompts) turned into kind’s own config dataclass — the one place this per-backend branch is spelled out, so save_profile and any caller building a not-yet-saved config from raw fields (cli/commands/profile.py’s pre-persist connectivity check) share it instead of each re-deriving their own.

async synology_apm_repo.sdk.profiles.delete_profile(name, *, config_dir=None)

Remove profile name entirely.

The profiles.json entry is removed before the best-effort keyring cleanup, so a later save_profile() re-using this name never silently inherits stale secrets.

Raises:

ProfileNotFoundError – No such profile.

synology_apm_repo.sdk.profiles.form_fields_for(kind)

kind’s full field list, in canonical order — every field a connection form binds, secrets included, unlike display_fields (non-secret only, read from an already-built config instance rather than named ahead of one existing).

async synology_apm_repo.sdk.profiles.get_profile(name, *, config_dir=None)

The non-secret half of profile name.

Never touches the keyring — safe for --json/log output by construction, not by redacting something that was fetched.

Raises:

ProfileNotFoundError – No such profile.

async synology_apm_repo.sdk.profiles.list_profiles(*, config_dir=None)

Every saved profile’s name and backend kind, sorted by name. Never touches the keyring.

async synology_apm_repo.sdk.profiles.list_profiles_full(*, config_dir=None)

Every saved profile, sorted by name, as a full Profile rather than list_profiles()’s own name+kind summary — for a caller that wants every profile’s own backend fields (profile list --verbose) without one get_profile() call per name each re-reading and re-parsing the same profiles.json that a single call here already read once. Never touches the keyring, same guarantee as get_profile().

async synology_apm_repo.sdk.profiles.list_remote_items(kind, **client_kwargs)

Every bucket (S3) or container (Azure) reachable with client_kwargs (already resolved, e.g. via client_kwargs_with_secrets) — the same account-level listing the TUI’s connect dialog uses to populate its “browse buckets/containers” picker before a bucket/container name is chosen. No SMB equivalent: unlike an S3/Azure account, an SMB server has no single account-level “list every share” operation this backend relies on, so kind is BackendKind.SMB raises rather than silently falling through to one of the other two backends’ own listing call.

async synology_apm_repo.sdk.profiles.load_profile(name, *, config_dir=None)

Every field of profile name, secrets included and unmasked, flat in the canonical field names (bucket/endpoint/region/ verify_tls/access_key/secret_key for S3, container/ account_url/verify_tls/credential for Azure, server/ share/port/username/password for SMB) — for refilling a connection form. Never used for display/logging: a caller that only needs to name or describe a profile should use get_profile instead.

async synology_apm_repo.sdk.profiles.save_profile(name, kind, fields, *, config_dir=None)

Save (or overwrite — this is an upsert, no collision check) a profile under name. fields uses the same canonical names load_profile returns; secret fields are split out and written to the keyring before the non-secret half is committed to profiles.json, so a profile visible in profiles.json never references secrets that were never actually written.

async synology_apm_repo.sdk.profiles.store_from_config(kind, config, secret_source)

config’s fields and secret_source’s raw credentials, merged via client_kwargs_with_secrets and handed to the matching ObjectStore constructor — the one translation build_store, cli/commands/profile.py’s pre-persist connectivity check, and the TUI’s connect dialog all need, whether or not config has ever been saved as a profile.

Submodules