Metadata-Version: 2.4
Name: rpa-bot-sdk-core
Version: 1.7.0
Summary: Orchestrator bot-side core: server client, pause/stop, logging, config loading. Shared by rpa-bot-sdk-performer and rpa-bot-sdk-dispatcher.
Author: Kasun Perera
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: pandas
Requires-Dist: openpyxl
Provides-Extra: sqlcipher
Requires-Dist: sqlcipher3; extra == "sqlcipher"
Requires-Dist: keyring; extra == "sqlcipher"
Provides-Extra: secure
Requires-Dist: cryptography; extra == "secure"
Provides-Extra: screenshot
Requires-Dist: pyautogui; extra == "screenshot"

# rpa-bot-sdk-core

The Orchestrator bot-side code shared by every role: `DbClient` (server auth,
queue, assets, storage, human-in-the-loop over HTTP, or a local SQLite queue
in StandaloneMode), `PauseController`/`checkpoint`, `start_command_listener`,
structured logging, `init_all_settings()` (Config.xlsx loader),
`take_screenshot()`, and the `safe_*` UI-action wrappers.

## Server-issued PAUSE/STOP actually reaching the bot

`PauseController`/`checkpoint()` are purely local — they only read/write an
in-process flag. Something still has to notice the server recorded a
PAUSE/RESUME/STOP and set/clear that flag; that something is
`start_command_listener()`, a daemon thread polling once a second. Call it
once, early in `main.py`, right after `db_client.register_run_token(...)`:

```python
from rpa_bot_sdk_core import start_command_listener

start_command_listener(db_client, pause_controller, run_label=run_id,
                       on_stop=lambda: setattr(ctx, "stop_requested", True))
```

Skip this and a bot that reaches a "PAUSE received, waiting for RESUME" wait
(see `rpa-bot-sdk-dispatcher`'s `DispatchState` / `rpa-bot-sdk-performer`'s
`GetTransactionState`) has nothing to wake it back up — it sits until
`MaxPauseDuration` elapses and auto-converts to STOP, even if the server's
own record was resumed within seconds. This is a straight port of the
"ASYNC PAUSE"/"ASYNC RESUME" listener thread pattern already proven in
production bots generated by `enterprise_patch.py`; a bot built directly
from `rpa-bot-sdk-dispatcher`/`-performer` needs this one explicit call to
get the same behaviour.

Neither a Performer nor a Dispatcher project installs this directly — it's
the shared dependency of [`rpa-bot-sdk-performer`](../performer/README.md)
and [`rpa-bot-sdk-dispatcher`](../dispatcher/README.md), pulled in
automatically by whichever of those two a project actually needs.

## Secure Cloud: `SecureDbClient` (1.4.0)

A `DbClient` that encrypts PHI with the project's Fernet key before it
leaves the machine, and decrypts it on the way back in. The server stores
only the ciphertext (`queue.json_data_secure`, `actions.form_data_secure`).

```python
from rpa_bot_sdk_core import SecureDbClient, PhiKeyError

try:
    client = SecureDbClient(base_url=url, api_key=key,
                            phi_key_path="data/phi.key",   # Config.xlsx PhiKeyPath
                            secure_fields="*")             # Config.xlsx SecureFields
except PhiKeyError as e:   # missing or not a Fernet key: do not start
    ...
logger.info(f"phi.key fingerprint {client.key_fingerprint}")
```

- `secure_fields="*"` encrypts the whole `json_data`; `"First Name,Email"`
  encrypts those keys only and keeps the rest plaintext, so the server can
  chart them.
- A fetched item whose ciphertext does not decrypt with this key is not
  dropped: its data carries `_phi_error`, and the Performer fails it.
- `key_fingerprint` names the key without revealing it. Every machine of
  one project must log the same one.

## A PAUSE that nobody resumes: `PauseController(max_pause_seconds=)` (1.6.0)

```python
pause_controller = PauseController(max_pause_seconds=int_setting(config, "MaxPauseDuration", 3600))
```

A checkpoint that has waited longer than `max_pause_seconds` stops the run
as a STOP would, and `pause_controller.stop_reason` says why. Without it a
paused checkpoint waits for RESUME however long it takes -- and the command
listener pauses the controller first, so `MaxPauseDuration` was never applied.
A PAUSE that arrives after a STOP no longer blocks.

## Whole-number settings: `int_setting()` (1.6.0)

`int_setting(config, "MaxRetries", 3)` -- a blank cell is the default, a 0
stays 0, and text raises `ValueError` naming the setting.
`config.get("MaxRetries") or 3` turned MaxRetries 0 into 3, and
`int(config.get(...))` crashed on a blank cell.

## The run token and its close (1.6.0)

The run token is 32 hex characters (it was 8, which collide). `close_run_token()`
returns whether the Orchestrator recorded the close and logs a refusal; a
close that never arrived (connection refused, no healthy node) is sent again.
A timeout is not resent: the Orchestrator may have closed the run and sent
its alert already.

## Keeping a log line on the machine: `LOCAL_ONLY`

`attach_proxy_handler()` forwards every log line at INFO and above to the
Orchestrator; DEBUG lines never leave the machine, since libraries log whole
request URLs at DEBUG (1.6.0). A line logged with `extra=LOCAL_ONLY` is
written to the local log and the console only -- for detail that can quote a
cell value, such as an exception message.

## Which exception text reaches the Orchestrator: `ReportableError` (1.4.0)

The SDK states store a failed item's exception text on the server and
forward their log lines. An exception raised in a bot's steps can quote what
it was working on, so `report_exception()` keeps the message only of a
`ReportableError` -- an error the bot wrote for the server, naming IDs,
settings or files, never a field value. Any other exception reaches the
server as its type name. Derive a bot's own exceptions from `ReportableError`.

## Logging an exception safely: `describe_error()` (1.7.0)

A log file outlives the run and is read on a shared bot machine, so a bot
should not write an exception's message even to its own log. `describe_error(error)`
gives its type, a `ReportableError`'s message, and the line of the bot's own
code it came from -- `ValueError at activities/process.py:225 in process` --
taken from the innermost traceback frame in a file under the bot's folder,
outside any installed package. `report_exception()` writes that line locally
instead of the message and traceback it wrote before 1.7.0.

## Checking a queue identifier: `identifier_problem()` (1.7.0)

`identifier_problem("Unique ID", value, 128)` says what is wrong with a Unique
ID or Reference -- empty, too long, a control character -- without repeating
the value, or returns `""` when it is usable.

## Request timeout

`DbClient(timeout=15)` -- seconds to wait for one Orchestrator reply.
`QueueManager` reads it from Config.xlsx `OrchestratorTimeoutSeconds`.

Bump this package's version specifically when the server contract itself
changes (a `db_client.py` field/action). A change confined to one role's own
orchestration (e.g. `QueueManager`) belongs in that role's package instead.
