ulogger-cloud 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ # Version control
2
+ .git/
3
+ .gitignore
4
+
5
+ # Build outputs
6
+ build/
7
+ dist/
8
+ *.egg-info/
9
+
10
+ # Python cache
11
+ __pycache__/
12
+ *.pyc
13
+ *.pyo
14
+
15
+ # IDE / OS
16
+ .vscode/
17
+ .idea/
18
+ *.DS_Store
19
+ Thumbs.db
20
+
21
+ # Docker artifacts
22
+ Dockerfile
23
+ docker-compose.yml
24
+
25
+ # CI
26
+ .github/
27
+
28
+ # Scripts (not needed inside the build container)
29
+ scripts/
@@ -0,0 +1,301 @@
1
+ name: Build & Test
2
+
3
+ on:
4
+ push:
5
+ branches: ["main", "release/**"]
6
+ tags: ["v*"]
7
+ pull_request:
8
+ branches: ["main"]
9
+ # Allow manual trigger from the Actions tab
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ # ── Build the universal wheel + sdist ONCE ─────────────────────────────────
17
+ # ulogger-cloud is pure Python (no C extensions), so a single build produces
18
+ # a py3-none-any wheel that installs on every Python version, OS, and CPU
19
+ # architecture without recompilation.
20
+ build:
21
+ name: Build distributions
22
+ runs-on: ubuntu-latest
23
+
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ with:
27
+ # Full history is required so setuptools-scm can find the latest tag
28
+ # and compute the correct version (e.g. v1.2.3 or v1.2.3.dev4+gabcdef)
29
+ fetch-depth: 0
30
+
31
+ - uses: actions/setup-python@v5
32
+ with:
33
+ python-version: "3.12"
34
+
35
+ - name: Install build toolchain
36
+ run: pip install --upgrade pip build twine
37
+
38
+ - name: Build wheel and sdist
39
+ run: python -m build --outdir dist/
40
+
41
+ - name: Confirm wheel is universal (py3-none-any)
42
+ run: |
43
+ WHL=$(ls dist/*.whl)
44
+ echo "Built: $WHL"
45
+ echo "$WHL" | grep -E "py3-none-any" \
46
+ || { echo "ERROR: wheel is not universal — C extension may have been introduced"; exit 1; }
47
+
48
+ - name: Verify distributions
49
+ run: twine check dist/*
50
+
51
+ - name: Upload distributions
52
+ uses: actions/upload-artifact@v4
53
+ with:
54
+ name: python-distributions
55
+ path: dist/
56
+ retention-days: 90
57
+
58
+ # ── Test matrix: every supported Python × every major OS ──────────────────
59
+ # Installs the single universal wheel built above and runs pytest to confirm
60
+ # runtime compatibility across the full support matrix.
61
+ test:
62
+ name: "Test · Python ${{ matrix.python-version }} · ${{ matrix.os }}"
63
+ needs: build
64
+ runs-on: ${{ matrix.os }}
65
+
66
+ strategy:
67
+ fail-fast: false
68
+ matrix:
69
+ os:
70
+ - ubuntu-latest
71
+ - windows-latest
72
+ - macos-latest
73
+ python-version:
74
+ - "3.9"
75
+ - "3.10"
76
+ - "3.11"
77
+ - "3.12"
78
+ - "3.13"
79
+
80
+ steps:
81
+ - uses: actions/checkout@v4
82
+
83
+ - uses: actions/setup-python@v5
84
+ with:
85
+ python-version: ${{ matrix.python-version }}
86
+
87
+ - name: Download distributions
88
+ uses: actions/download-artifact@v4
89
+ with:
90
+ name: python-distributions
91
+ path: dist/
92
+
93
+ - name: Install wheel
94
+ shell: bash
95
+ run: pip install "$(ls dist/*.whl)[dev]"
96
+
97
+ - name: Run tests
98
+ run: pytest --tb=short
99
+
100
+ # ── Publish to PyPI (Trusted Publishing) ───────────────────────────────────
101
+ # Triggered only on version tags (e.g. v1.0.0).
102
+ # Uses PyPI Trusted Publishing — no API tokens or secrets required.
103
+ # See: https://docs.pypi.org/trusted-publishers/
104
+ #
105
+ # One-time setup required on pypi.org:
106
+ # 1. Go to https://pypi.org/manage/account/publishing/
107
+ # 2. Add a "pending publisher" for:
108
+ # PyPI project name: ulogger-cloud
109
+ # Owner: ulogger-ai
110
+ # Repository: py-ulogger-cloud
111
+ # Workflow name: build.yml
112
+ # Environment name: (leave blank)
113
+ publish-to-pypi:
114
+ name: Publish to PyPI
115
+ needs: [build, test]
116
+ runs-on: ubuntu-latest
117
+ if: startsWith(github.ref, 'refs/tags/v')
118
+
119
+ permissions:
120
+ id-token: write # mandatory for Trusted Publishing
121
+
122
+ steps:
123
+ - name: Download distributions
124
+ uses: actions/download-artifact@v4
125
+ with:
126
+ name: python-distributions
127
+ path: dist/
128
+
129
+ - name: Publish distribution to PyPI
130
+ uses: pypa/gh-action-pypi-publish@release/v1
131
+
132
+ # ── Publish to S3 (acts as a private PyPI simple server) ──────────────────
133
+ # Triggered only on version tags (e.g. v1.0.0).
134
+ # After uploading the new distributions the job regenerates the PEP 503
135
+ # simple-index HTML files so that `pip install --extra-index-url` works.
136
+ #
137
+ # Required repository secrets:
138
+ # AWS_ACCESS_KEY_ID – IAM key with s3:PutObject / s3:GetObject / s3:ListBucket
139
+ # AWS_SECRET_ACCESS_KEY – matching secret
140
+ # AWS_REGION – e.g. us-east-1
141
+ #
142
+ # pip usage after publishing:
143
+ # pip install ulogger-cloud \
144
+ # --extra-index-url https://pyulogger.s3.<region>.amazonaws.com/simple/
145
+ publish-s3:
146
+ name: Publish to S3 PyPI
147
+ needs: [build, test]
148
+ runs-on: ubuntu-latest
149
+ if: startsWith(github.ref, 'refs/tags/v')
150
+
151
+ permissions:
152
+ contents: read # checkout is not needed; artifact download only
153
+ id-token: write # reserved for future OIDC-based auth
154
+
155
+ steps:
156
+ - name: Download distributions
157
+ uses: actions/download-artifact@v4
158
+ with:
159
+ name: python-distributions
160
+ path: dist/
161
+
162
+ - name: Configure AWS credentials
163
+ uses: aws-actions/configure-aws-credentials@v4
164
+ with:
165
+ aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
166
+ aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
167
+ aws-region: ${{ secrets.AWS_REGION }}
168
+
169
+ # Upload wheel + sdist to s3://pyulogger/packages/
170
+ # Public read access is granted via the bucket policy (ACLs are disabled
171
+ # because the bucket uses "Bucket owner enforced" Object Ownership).
172
+ - name: Upload distributions to S3
173
+ run: |
174
+ for f in dist/*; do
175
+ echo "Uploading $f …"
176
+ aws s3 cp "$f" "s3://pyulogger/packages/$(basename "$f")"
177
+ done
178
+
179
+ # Re-generate PEP 503 simple-index HTML files that cover every file
180
+ # currently in s3://pyulogger/packages/ so that the index stays in sync
181
+ # even when multiple versions exist.
182
+ - name: Regenerate PyPI simple index
183
+ run: |
184
+ python3 - <<'PYEOF'
185
+ import hashlib
186
+ import json
187
+ import os
188
+ import pathlib
189
+ import subprocess
190
+
191
+ BUCKET = "pyulogger"
192
+ PKG = "ulogger-cloud" # canonical package name
193
+ PKG_DIR = PKG # directory name under simple/
194
+
195
+ # ── 1. List every distribution file already in s3://pyulogger/packages/ ──
196
+ result = subprocess.run(
197
+ [
198
+ "aws", "s3api", "list-objects-v2",
199
+ "--bucket", BUCKET,
200
+ "--prefix", "packages/",
201
+ ],
202
+ capture_output=True, text=True, check=True,
203
+ )
204
+ data = json.loads(result.stdout)
205
+ objects = data.get("Contents", [])
206
+ keys = sorted(
207
+ o["Key"] for o in objects
208
+ if o["Key"].endswith((".whl", ".tar.gz", ".zip"))
209
+ )
210
+
211
+ if not keys:
212
+ raise SystemExit("No distribution files found in s3://pyulogger/packages/ — aborting.")
213
+
214
+ # ── 2. Download every file to compute its sha256 digest ──────────────────
215
+ dl_dir = pathlib.Path("_s3_dists")
216
+ dl_dir.mkdir(exist_ok=True)
217
+
218
+ entries = [] # list of (filename, href_with_hash)
219
+ for key in keys:
220
+ name = pathlib.Path(key).name
221
+ local = dl_dir / name
222
+ subprocess.run(
223
+ ["aws", "s3", "cp", f"s3://{BUCKET}/{key}", str(local)],
224
+ check=True,
225
+ )
226
+ digest = hashlib.sha256(local.read_bytes()).hexdigest()
227
+ # Absolute-path href — resolves correctly regardless of how pip
228
+ # interprets the trailing-slash base URL.
229
+ href = f"/packages/{name}#sha256={digest}"
230
+ entries.append((name, href))
231
+
232
+ # ── 3. Build simple/<pkg>/index.html (PEP 503) ───────────────────────────
233
+ file_links = "\n".join(
234
+ f' <a href="{href}">{name}</a><br />' for name, href in entries
235
+ )
236
+ pkg_html = f"""<!DOCTYPE html>
237
+ <html>
238
+ <head><title>Links for {PKG}</title></head>
239
+ <body>
240
+ <h1>Links for {PKG}</h1>
241
+ {file_links}
242
+ </body>
243
+ </html>
244
+ """
245
+
246
+ out_pkg = pathlib.Path("simple") / PKG_DIR
247
+ out_pkg.mkdir(parents=True, exist_ok=True)
248
+ (out_pkg / "index.html").write_text(pkg_html)
249
+
250
+ # ── 4. Build simple/index.html (root listing) ─────────────────────────────
251
+ root_html = f"""<!DOCTYPE html>
252
+ <html>
253
+ <head><title>Simple index</title></head>
254
+ <body>
255
+ <h1>Simple index</h1>
256
+ <a href="{PKG_DIR}/">{PKG}</a><br />
257
+ </body>
258
+ </html>
259
+ """
260
+ pathlib.Path("simple").mkdir(exist_ok=True)
261
+ pathlib.Path("simple/index.html").write_text(root_html)
262
+
263
+ print("Generated index files:")
264
+ for p in pathlib.Path("simple").rglob("*.html"):
265
+ print(f" {p}")
266
+ print(p.read_text())
267
+ PYEOF
268
+
269
+ # Upload simple-index HTML files to S3.
270
+ #
271
+ # KEY TRICK: pip requests /simple/ulogger-cloud/ (trailing slash).
272
+ # `aws s3 cp dest/` appends the source filename → creates index.html, NOT "/".
273
+ # `aws s3api put-object --key "simple/ulogger-cloud/"` creates the key
274
+ # with a literal trailing slash, so pip's GET hits it directly.
275
+ - name: Upload simple index to S3
276
+ run: |
277
+ # Trailing-slash keys — exactly what pip GETs
278
+ aws s3api put-object \
279
+ --bucket pyulogger \
280
+ --key "simple/" \
281
+ --body simple/index.html \
282
+ --content-type "text/html; charset=utf-8"
283
+
284
+ aws s3api put-object \
285
+ --bucket pyulogger \
286
+ --key "simple/ulogger-cloud/" \
287
+ --body "simple/ulogger-cloud/index.html" \
288
+ --content-type "text/html; charset=utf-8"
289
+
290
+ # index.html variants — convenient for browser navigation
291
+ aws s3 cp simple/index.html \
292
+ "s3://pyulogger/simple/index.html" \
293
+ --content-type "text/html; charset=utf-8"
294
+
295
+ aws s3 cp "simple/ulogger-cloud/index.html" \
296
+ "s3://pyulogger/simple/ulogger-cloud/index.html" \
297
+ --content-type "text/html; charset=utf-8"
298
+
299
+ echo "Published. Install with:"
300
+ echo " pip install ulogger-cloud \\"
301
+ echo " --extra-index-url https://pyulogger.s3.${{ secrets.AWS_REGION }}.amazonaws.com/simple/"
@@ -0,0 +1,26 @@
1
+ # Build outputs
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+
6
+ # setuptools-scm generated version file (written at build time, not committed)
7
+ ulogger_cloud/_version.py
8
+
9
+ # Python
10
+ __pycache__/
11
+ *.pyc
12
+ *.pyo
13
+ *.pyd
14
+ .Python
15
+ *.so
16
+
17
+ # Virtual environments
18
+ .venv/
19
+ venv/
20
+ env/
21
+
22
+ # IDE / OS
23
+ .vscode/
24
+ .idea/
25
+ *.DS_Store
26
+ Thumbs.db
@@ -0,0 +1,44 @@
1
+ # ── Build image for ulogger-cloud ──────────────────────────────────────────
2
+ # ulogger-cloud is pure Python, so a single build produces a py3-none-any
3
+ # wheel that works on every Python version, OS, and CPU architecture.
4
+ #
5
+ # The version is derived from the git tag via setuptools-scm. Because the
6
+ # Docker build context contains only source files (no .git directory), the
7
+ # version must be supplied explicitly via --build-arg:
8
+ #
9
+ # VERSION=$(git describe --tags --abbrev=0 | sed 's/^v//')
10
+ # docker build --build-arg VERSION=$VERSION -t ulogger-cloud-build .
11
+ # docker run --rm -v "$(pwd)/dist:/dist" ulogger-cloud-build
12
+ #
13
+ # If VERSION is omitted the build defaults to "0.0.0.dev0".
14
+
15
+ ARG VERSION=0.0.0.dev0
16
+ FROM python:3.12-slim
17
+
18
+ # Make the version available to the build step below
19
+ ARG VERSION
20
+ ENV SETUPTOOLS_SCM_PRETEND_VERSION=${VERSION}
21
+
22
+ LABEL org.opencontainers.image.title="ulogger-cloud build"
23
+ LABEL org.opencontainers.image.description="Builds universal py3-none-any wheel + sdist"
24
+
25
+ WORKDIR /src
26
+
27
+
28
+ # ── Python build toolchain ─────────────────────────────────────────────────
29
+ RUN pip install --no-cache-dir --upgrade pip build twine
30
+
31
+ # ── Copy source ────────────────────────────────────────────────────────────
32
+ COPY pyproject.toml README.md ./
33
+ COPY ulogger_cloud/ ulogger_cloud/
34
+
35
+ # ── Build ──────────────────────────────────────────────────────────────────
36
+ # Output lands in /src/dist/
37
+ RUN python -m build --outdir /src/dist
38
+
39
+ # ── Verify the distributions look correct ─────────────────────────────────
40
+ RUN twine check /src/dist/*
41
+
42
+ # ── Runtime: copy artefacts out to /dist (bind-mounted by caller) ──────────
43
+ CMD ["sh", "-c", \
44
+ "cp -v /src/dist/* /dist/ && echo 'Build artefacts copied to /dist'"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 uLogger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: ulogger-cloud
3
+ Version: 0.3.0
4
+ Summary: uLogger cloud communication library – MQTT session management, binary log upload, and data validation.
5
+ Author-email: Eric Ibarra <support@ulogger.ai>
6
+ Maintainer-email: Eric Ibarra <support@ulogger.ai>
7
+ License: MIT
8
+ Project-URL: Homepage, https://ulogger.ai
9
+ Project-URL: Documentation, https://ulogger.ai/documentation
10
+ Project-URL: Release Notes, https://github.com/ulogger-ai/py-ulogger-cloud/releases
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Topic :: System :: Logging
23
+ Requires-Python: >=3.9
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: paho-mqtt<3,>=1.6
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7; extra == "dev"
29
+ Dynamic: license-file
30
+
31
+ # ulogger-cloud
32
+
33
+ A Python library for communicating with the [uLogger](https://ulogger.ai) cloud
34
+ platform. It handles:
35
+
36
+ * **Device registration** – MQTT boot handshake to obtain a session token.
37
+ * **Binary log upload** – publish raw log data to the cloud for server-side
38
+ parsing and visualisation.
39
+ * **Header checksum validation** – verify the integrity of a firmware log
40
+ buffer before upload.
41
+ * **Session token caching** – persist tokens to disk so re-registration is
42
+ skipped on subsequent runs (or across firmware upgrades).
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install ulogger-cloud
48
+ ```
49
+
50
+ Requires Python 3.9 or later. The only runtime dependency is
51
+ [paho-mqtt](https://pypi.org/project/paho-mqtt/) (`>=1.6`), which is
52
+ installed automatically.
53
+
54
+ ## Quick start
55
+
56
+ ### High-level API (recommended)
57
+
58
+ `upload_log` handles everything in one call: checksum validation, session-token
59
+ retrieval (with caching), header patching, and MQTT publish.
60
+
61
+ ```python
62
+ from pathlib import Path
63
+ from ulogger_cloud import DeviceInfo, MqttConfig, upload_log
64
+
65
+ # Device identification – normally parsed from a BLE characteristic
66
+ device = DeviceInfo(
67
+ application_id=42,
68
+ device_serial="ABC123",
69
+ device_type="my_board",
70
+ git_version="v1.0.0",
71
+ git_hash="abcdef0",
72
+ )
73
+
74
+ # MQTT broker configuration
75
+ mqtt_cfg = MqttConfig(
76
+ cert_file=Path("certificate.pem.crt"),
77
+ key_file=Path("private.pem.key"),
78
+ customer_id=975773647,
79
+ )
80
+
81
+ # buf is a bytearray received over BLE
82
+ success = upload_log(device, buf, mqtt_cfg)
83
+ print("Upload OK" if success else "Upload failed")
84
+ ```
85
+
86
+ ### Persistent session-token cache
87
+
88
+ By default `upload_log` uses an **in-memory** token store that is discarded
89
+ when the process exits. To avoid a new MQTT boot handshake on every run,
90
+ pass a file-backed `SessionStore`:
91
+
92
+ ```python
93
+ from ulogger_cloud import (
94
+ DeviceInfo, MqttConfig, SessionStore,
95
+ DEFAULT_FILE_STORE_PATH, upload_log,
96
+ )
97
+
98
+ store = SessionStore(path=DEFAULT_FILE_STORE_PATH) # ~/.ulogger/session_tokens.json
99
+
100
+ success = upload_log(device, buf, mqtt_cfg, store=store)
101
+ ```
102
+
103
+ Tokens are automatically invalidated whenever the device firmware's
104
+ `git_hash` changes, triggering a fresh registration transparently.
105
+
106
+ ### Low-level building blocks
107
+
108
+ If you need finer control you can call each step individually:
109
+
110
+ ```python
111
+ from pathlib import Path
112
+ from ulogger_cloud import (
113
+ DeviceInfo,
114
+ MqttConfig,
115
+ get_session_token,
116
+ patch_session_token,
117
+ publish_binary_log,
118
+ validate_checksum,
119
+ )
120
+
121
+ device = DeviceInfo(
122
+ application_id=42,
123
+ device_serial="ABC123",
124
+ device_type="my_board",
125
+ git_version="v1.0.0",
126
+ git_hash="abcdef0",
127
+ )
128
+
129
+ mqtt_cfg = MqttConfig(
130
+ cert_file=Path("certificate.pem.crt"),
131
+ key_file=Path("private.pem.key"),
132
+ customer_id=975773647,
133
+ )
134
+
135
+ buf = bytearray(raw_ble_transfer)
136
+ if validate_checksum(bytes(buf)):
137
+ token = get_session_token(device, mqtt_cfg)
138
+ if token is not None:
139
+ patch_session_token(buf, token)
140
+ publish_binary_log(device, bytes(buf), mqtt_cfg)
141
+ ```
142
+
143
+ ## API reference
144
+
145
+ ### `DeviceInfo`
146
+
147
+ Dataclass holding device identification read from the firmware:
148
+
149
+ | Field | Type | Description |
150
+ |---|---|---|
151
+ | `application_id` | `int` | Application ID (uint32) |
152
+ | `device_serial` | `str` | Unique device serial string |
153
+ | `device_type` | `str` | Board / product type string |
154
+ | `git_version` | `str` | Human-readable firmware version tag |
155
+ | `git_hash` | `str` | Short git commit hash of the firmware |
156
+
157
+ ### `MqttConfig`
158
+
159
+ Dataclass for MQTT broker connection parameters:
160
+
161
+ | Field | Type | Default | Description |
162
+ |---|---|---|---|
163
+ | `broker` | `str` | `"mqtt.ulogger.ai"` | Broker hostname |
164
+ | `port` | `int` | `8883` | Broker port (TLS) |
165
+ | `cert_file` | `Path \| None` | `None` | Path to client certificate (`.pem.crt`) |
166
+ | `key_file` | `Path \| None` | `None` | Path to private key (`.pem.key`) |
167
+ | `customer_id` | `int` | `0` | Customer account ID |
168
+ | `token_timeout` | `float` | `15.0` | Seconds to wait for session-token response |
169
+
170
+ ### `SessionStore`
171
+
172
+ Thread-safe mapping of device serial numbers to session tokens.
173
+
174
+ ```python
175
+ from ulogger_cloud import SessionStore, DEFAULT_FILE_STORE_PATH
176
+
177
+ # In-memory only (default)
178
+ store = SessionStore()
179
+
180
+ # File-backed persistence
181
+ store = SessionStore(path=DEFAULT_FILE_STORE_PATH)
182
+
183
+ # Memory-capped + file-backed (keeps the 1 000 most-recently-used tokens)
184
+ store = SessionStore(path=DEFAULT_FILE_STORE_PATH, max_entries=1000)
185
+ ```
186
+
187
+ ### Functions
188
+
189
+ | Function | Description |
190
+ |---|---|
191
+ | `upload_log(device, buf, cfg, store=None)` | Validate, patch, and publish a binary log buffer. Returns `True` on success. |
192
+ | `get_or_fetch_token(device, cfg, store=None)` | Return a cached token or perform a fresh MQTT boot registration. |
193
+ | `get_session_token(device, cfg)` | Perform an MQTT boot registration and return the server-issued token. |
194
+ | `validate_checksum(buf)` | Return `True` if the binary log header checksum is valid. |
195
+ | `patch_session_token(buf, token)` | Write a session token into the binary log header in-place. |
196
+ | `publish_binary_log(device, buf, cfg)` | Publish a raw binary log buffer to the MQTT broker. |
197
+
198
+ ## Development
199
+
200
+ ```bash
201
+ git clone https://github.com/your-org/ulogger-cloud.git
202
+ cd ulogger-cloud
203
+ pip install -e ".[dev]"
204
+ pytest
205
+ ```