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.
- ulogger_cloud-0.3.0/.dockerignore +29 -0
- ulogger_cloud-0.3.0/.github/workflows/build.yml +301 -0
- ulogger_cloud-0.3.0/.gitignore +26 -0
- ulogger_cloud-0.3.0/Dockerfile +44 -0
- ulogger_cloud-0.3.0/LICENSE +21 -0
- ulogger_cloud-0.3.0/PKG-INFO +205 -0
- ulogger_cloud-0.3.0/README.md +175 -0
- ulogger_cloud-0.3.0/docker-compose.yml +17 -0
- ulogger_cloud-0.3.0/pyproject.toml +52 -0
- ulogger_cloud-0.3.0/scripts/build_all.ps1 +57 -0
- ulogger_cloud-0.3.0/scripts/build_all.sh +52 -0
- ulogger_cloud-0.3.0/setup.cfg +4 -0
- ulogger_cloud-0.3.0/tests/test_smoke.py +28 -0
- ulogger_cloud-0.3.0/ulogger_cloud/__init__.py +33 -0
- ulogger_cloud-0.3.0/ulogger_cloud/_version.py +24 -0
- ulogger_cloud-0.3.0/ulogger_cloud/api.py +94 -0
- ulogger_cloud-0.3.0/ulogger_cloud/checksum.py +67 -0
- ulogger_cloud-0.3.0/ulogger_cloud/device_info.py +29 -0
- ulogger_cloud-0.3.0/ulogger_cloud/mqtt.py +284 -0
- ulogger_cloud-0.3.0/ulogger_cloud/session_store.py +230 -0
- ulogger_cloud-0.3.0/ulogger_cloud.egg-info/PKG-INFO +205 -0
- ulogger_cloud-0.3.0/ulogger_cloud.egg-info/SOURCES.txt +23 -0
- ulogger_cloud-0.3.0/ulogger_cloud.egg-info/dependency_links.txt +1 -0
- ulogger_cloud-0.3.0/ulogger_cloud.egg-info/requires.txt +4 -0
- ulogger_cloud-0.3.0/ulogger_cloud.egg-info/top_level.txt +1 -0
|
@@ -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
|
+
```
|