Metadata-Version: 2.4
Name: sc92-public-define
Version: 0.3.1
Summary: Personal shared utilities: file lock, logging, MQTT publishing and YAML config helpers
Author: sc92
License-Expression: MIT
Keywords: filelock,logging,mqtt,yaml,utility
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: filelock>=4.0
Requires-Dist: paho-mqtt>=2.0
Requires-Dist: PyYAML>=6.0
Dynamic: license-file

# sc92-public-define

Personal shared utilities extracted from everyday scripts:

1. **Single-instance file lock** — `check_running_lock()`: if the script is
   already running elsewhere, exit (or return False) immediately.
2. **Rotating logging setup** — `init_logging_config()`: one call to get
   `*_debug.log` + `*_info.log` (10 MB × 7 backups) + console output. If another
   library already configured the root logger it now warns on stderr instead of
   silently doing nothing (pass `force=True` to take over); with no console
   (`pythonw.exe`, PyInstaller `--noconsole`) the console handler is skipped.
3. **Global exception hooks** — `enable_exception_logging()`: uncaught
   exceptions are written to the log instead of only printed to stderr — for the
   main thread **and for worker threads** (`threading.excepthook`, which
   `sys.excepthook` never covers).
4. **MQTT publisher** — `MqttPublisher.publish()`: one-shot connect →
   publish → disconnect. Credentials are taken from **explicit constructor
   args only** (no hardcoded defaults, no environment variables) — keep them
   in your local YAML config file and pass the values in.
5. **YAML config helpers** — `read_yaml()` / `write_yaml()`: first run writes
   a default template into `configs/` next to the script and exits for review;
   Chinese keys and insertion order are preserved, and a file saved as
   ANSI/GBK or broken YAML now fails with a readable message instead of a raw
   `UnicodeDecodeError`.

All helpers are PyInstaller-frozen aware and default to paths derived from
the entry script, so typical usage needs **zero arguments**.

The derived name is `<script dir>/…/<script name>…`, so one instance means
"one running copy of this script in this directory": a renamed or copied script
is a separate instance. `python -m pkg[.mod]` is handled as module `pkg[.mod]`
with the current working directory as its base — it never writes into
site-packages.

## Install

```bash
pip install sc92-public-define
```

Requires Python >= 3.11 (`filelock>=4.0`, `paho-mqtt>=2.0`, `PyYAML>=6.0`).

## Typical usage (top of any script)

```python
import logging
from sc92_public_define import (
    check_running_lock,     # single-instance lock
    init_logging_config,    # rotating logs + console
    enable_exception_logging,     # uncaught exception -> log
)

init_logging_config()       # logs/<script name>_debug.log / _info.log
check_running_lock()        # <script name>.running.lock next to the script;
                            # exits with status 1 when another copy is running
enable_exception_logging()

logging.info("main started")
```

### MQTT publishing (credentials live in your YAML config, never in code)

```python
from sc92_public_define import MqttPublisher, read_yaml

server = read_yaml()["mqtt服务器"]          # nested dict in <script>.config.yaml
pub = MqttPublisher(host=server["地址"], port=server["端口"],
                    username=server["用户名"] or None,
                    password=server["密码"] or None,
                    topic=server["主题"])
pub.publish("hello")                       # uses instance topic/qos
pub.publish("hello", topic="other/t", qos=1)   # per-call override
```

### YAML config

```python
from sc92_public_define import read_yaml, write_yaml

cfg = read_yaml(default_config={"服务器": "127.0.0.1", "端口": 8080})
# first run: creates configs/<script>.config.yaml next to the script, logs a hint, exits
cfg["端口"] = 9090
write_yaml(config_dict=cfg)     # writes back to the same default file
```

## API

| Function | Default | Notes |
|---|---|---|
| `check_running_lock(lock_file=None, on_conflict="exit")` | `<script dir>/<script>.running.lock` | `on_conflict="exit"` → logs an error and exits with status 1; `"return"` → returns `False`; `True` when acquired |
| `init_logging_config(log_file=None, max_bytes=10MB, backup_count=7, force=False)` | `<script dir>/logs/<script>` | warns on stderr and does nothing if the root logger already has handlers (`force=True` takes over); console handler skipped when there is no console |
| `enable_exception_logging()` / `handle_global_exception` / `handle_thread_exception` | — | sets `sys.excepthook` **and** `threading.excepthook`, writes tracebacks via `logging.error` |
| `MqttPublisher(host, port=1883, username=None, password=None, topic=None, qos=2, client_id=None, connect_timeout=5, publish_timeout=10)` | explicit args only | raises `ValueError` if host/topic missing; `connect_timeout` caps DNS + TCP + the wait for CONNACK |
| `MqttPublisher.publish(msg, topic=None, qos=None)` | instance defaults | `True` only when the message really got through: QoS 1/2 require the broker acknowledgement, QoS 0 has none (written to the socket only). Wrong password, missing CONNACK, an unacknowledged publish or any network error → `False` plus a `logging.error`, never raises. `bytes`/`bytearray` payloads are sent as-is, anything else is `str()`-ed |
| `read_yaml(config_file=None, default_config=None, on_missing="exit", merge_defaults=False)` | `<script dir>/configs/<script>.config.yaml` | missing → write template + exit(1); empty file or `{}` → overwrite with the template + exit(1); non-UTF-8 / broken YAML / top level not a mapping → `ValueError`; `merge_defaults=True` fills keys that only exist in `default_config` into the returned dict (the file on disk is untouched); the returned template is a deep copy |
| `write_yaml(config_file=None, config_dict=None)` | same default file | `config_dict` is required (`None` or a non-dict raises `ValueError`); utf-8, Chinese keys & order preserved |
| `script_dir()` / `script_name()` | — | entry-script path/name; frozen exe → exe folder/stem; `python -m pkg` → cwd + `pkg` |
| `default_lock_file()` / `default_log_file()` / `default_config_file()` | — | expose the derived default paths |

## Notes

- The lock is released automatically by the OS when the process exits;
  the lock **file** is left on disk (harmless, reuse next time).
- The first `read_yaml()` run on a machine without a config file deliberately
  exits with status 1 after writing the template: edit it, then run again.
- `init_logging_config()` must come **before** `check_running_lock()` /
  `read_yaml()`, otherwise their "抢锁结果 / 配置已加载" INFO lines are dropped
  (only the ERROR lines still reach stderr through logging's last-resort handler).
- See the `demo/` folder in the source tree for runnable examples (it is not
  shipped inside the wheel or sdist).
