pydanalock-cloud 0.5.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.
- pydanalock_cloud-0.5.0/.github/pull_request_template.md +18 -0
- pydanalock_cloud-0.5.0/.github/workflows/ci.yml +51 -0
- pydanalock_cloud-0.5.0/.github/workflows/publish.yml +46 -0
- pydanalock_cloud-0.5.0/.github/workflows/testpypi.yml +47 -0
- pydanalock_cloud-0.5.0/.gitignore +26 -0
- pydanalock_cloud-0.5.0/AGENTS.md +116 -0
- pydanalock_cloud-0.5.0/LICENSE +21 -0
- pydanalock_cloud-0.5.0/PKG-INFO +99 -0
- pydanalock_cloud-0.5.0/README.md +66 -0
- pydanalock_cloud-0.5.0/pyproject.toml +75 -0
- pydanalock_cloud-0.5.0/specs/0001-oauth2-auth.md +107 -0
- pydanalock_cloud-0.5.0/specs/0002-devices-keys.md +116 -0
- pydanalock_cloud-0.5.0/specs/0003-client-api.md +65 -0
- pydanalock_cloud-0.5.0/specs/0004-client-lifecycle.md +80 -0
- pydanalock_cloud-0.5.0/specs/0005-key-refetch-force.md +83 -0
- pydanalock_cloud-0.5.0/specs/0006-firmware-latest.md +127 -0
- pydanalock_cloud-0.5.0/specs/0007-protocol-constants-audit.md +105 -0
- pydanalock_cloud-0.5.0/specs/0009-pypi-publication.md +94 -0
- pydanalock_cloud-0.5.0/src/pydanalock/cloud/__init__.py +30 -0
- pydanalock_cloud-0.5.0/src/pydanalock/cloud/aclient.py +220 -0
- pydanalock_cloud-0.5.0/src/pydanalock/cloud/auth.py +161 -0
- pydanalock_cloud-0.5.0/src/pydanalock/cloud/client.py +222 -0
- pydanalock_cloud-0.5.0/src/pydanalock/cloud/errors.py +31 -0
- pydanalock_cloud-0.5.0/src/pydanalock/cloud/models.py +213 -0
- pydanalock_cloud-0.5.0/src/pydanalock/cloud/py.typed +0 -0
- pydanalock_cloud-0.5.0/tests/conftest.py +107 -0
- pydanalock_cloud-0.5.0/tests/fixtures/firmware_latest.json +6 -0
- pydanalock_cloud-0.5.0/tests/fixtures/login_token_single.json +26 -0
- pydanalock_cloud-0.5.0/tests/fixtures/login_tokens_v1.json +28 -0
- pydanalock_cloud-0.5.0/tests/fixtures/login_tokens_v1_expired.json +28 -0
- pydanalock_cloud-0.5.0/tests/fixtures/oauth2_token_password.json +5 -0
- pydanalock_cloud-0.5.0/tests/fixtures/oauth2_token_refresh.json +5 -0
- pydanalock_cloud-0.5.0/tests/test_auth.py +335 -0
- pydanalock_cloud-0.5.0/tests/test_client.py +398 -0
- pydanalock_cloud-0.5.0/tests/test_devices.py +417 -0
- pydanalock_cloud-0.5.0/tests/test_firmware.py +221 -0
- pydanalock_cloud-0.5.0/tests/test_lifecycle.py +62 -0
- pydanalock_cloud-0.5.0/tests/test_live.py +57 -0
- pydanalock_cloud-0.5.0/tests/test_package.py +41 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
<!-- What does this PR change? Link the feature spec: specs/NNNN-slug.md -->
|
|
4
|
+
|
|
5
|
+
## Spec-driven checklist
|
|
6
|
+
|
|
7
|
+
- [ ] Feature spec exists (`specs/NNNN-slug.md`) and has status `approved`
|
|
8
|
+
- [ ] Tests written before implementation (TDD), failing first
|
|
9
|
+
- [ ] API responses covered by recorded fixtures (no live cloud in unit tests)
|
|
10
|
+
- [ ] Live tests stay behind the `live` marker
|
|
11
|
+
- [ ] `ruff check .`, `ruff format --check .`, `mypy`, `pytest -m "not live"` pass
|
|
12
|
+
- [ ] Skeptic review completed: verdict `APPROVED` (or `NITS` only)
|
|
13
|
+
- [ ] No secrets committed (tokens, keys, credentials); placeholders only in examples
|
|
14
|
+
- [ ] Logs never contain tokens or keys
|
|
15
|
+
|
|
16
|
+
## Commit style
|
|
17
|
+
|
|
18
|
+
Conventional Commits (`feat:`, `fix:`, `docs:`, `test:`, `chore:`, `refactor:`).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
strategy:
|
|
16
|
+
fail-fast: false
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python-version }}
|
|
24
|
+
- name: Install
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
pip install -e ".[dev]"
|
|
28
|
+
- name: Ruff lint
|
|
29
|
+
run: ruff check .
|
|
30
|
+
- name: Ruff format
|
|
31
|
+
run: ruff format --check .
|
|
32
|
+
- name: Mypy
|
|
33
|
+
run: mypy
|
|
34
|
+
- name: Tests
|
|
35
|
+
run: pytest -m "not live"
|
|
36
|
+
|
|
37
|
+
build:
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v4
|
|
41
|
+
- uses: actions/setup-python@v5
|
|
42
|
+
with:
|
|
43
|
+
python-version: "3.12"
|
|
44
|
+
- name: Install
|
|
45
|
+
run: |
|
|
46
|
+
python -m pip install --upgrade pip
|
|
47
|
+
pip install -e ".[release]"
|
|
48
|
+
- name: Build
|
|
49
|
+
run: python -m build
|
|
50
|
+
- name: Twine check
|
|
51
|
+
run: twine check dist/*
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Publishes to PyPI via Trusted Publishing (OIDC) when a GitHub Release is published.
|
|
2
|
+
name: Publish
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
- name: Install
|
|
20
|
+
run: |
|
|
21
|
+
python -m pip install --upgrade pip
|
|
22
|
+
pip install -U "build>=1.2" "twine>=5"
|
|
23
|
+
- name: Build
|
|
24
|
+
run: python -m build
|
|
25
|
+
- name: Twine check
|
|
26
|
+
run: twine check dist/*
|
|
27
|
+
- name: Upload artifacts
|
|
28
|
+
uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: dist
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
publish:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment: pypi
|
|
37
|
+
permissions:
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- name: Download artifacts
|
|
41
|
+
uses: actions/download-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: dist
|
|
44
|
+
path: dist/
|
|
45
|
+
- name: Publish to PyPI
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Publishes to TestPyPI via Trusted Publishing (OIDC); manual dry run before a release.
|
|
2
|
+
name: TestPyPI
|
|
3
|
+
|
|
4
|
+
on:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
- name: Install
|
|
19
|
+
run: |
|
|
20
|
+
python -m pip install --upgrade pip
|
|
21
|
+
pip install -U "build>=1.2" "twine>=5"
|
|
22
|
+
- name: Build
|
|
23
|
+
run: python -m build
|
|
24
|
+
- name: Twine check
|
|
25
|
+
run: twine check dist/*
|
|
26
|
+
- name: Upload artifacts
|
|
27
|
+
uses: actions/upload-artifact@v4
|
|
28
|
+
with:
|
|
29
|
+
name: dist
|
|
30
|
+
path: dist/
|
|
31
|
+
|
|
32
|
+
publish:
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment: testpypi
|
|
36
|
+
permissions:
|
|
37
|
+
id-token: write
|
|
38
|
+
steps:
|
|
39
|
+
- name: Download artifacts
|
|
40
|
+
uses: actions/download-artifact@v4
|
|
41
|
+
with:
|
|
42
|
+
name: dist
|
|
43
|
+
path: dist/
|
|
44
|
+
- name: Publish to TestPyPI
|
|
45
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
46
|
+
with:
|
|
47
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
|
|
8
|
+
# Environments
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
|
|
12
|
+
# Tooling caches
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
coverage.xml
|
|
18
|
+
htmlcov/
|
|
19
|
+
|
|
20
|
+
# Local secrets (never commit)
|
|
21
|
+
*.local.json
|
|
22
|
+
|
|
23
|
+
# Editors / OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# AGENTS.md — pydanalock-cloud
|
|
2
|
+
|
|
3
|
+
Working agreement for humans and AI agents contributing to this repository.
|
|
4
|
+
|
|
5
|
+
## 1. Role in the group
|
|
6
|
+
|
|
7
|
+
This repository is part of a group of projects that build a self-hosted
|
|
8
|
+
control stack for Danalock V3 smart locks:
|
|
9
|
+
|
|
10
|
+
| Repository | Role | Visibility | Language |
|
|
11
|
+
|---|---|---|---|
|
|
12
|
+
| `pydanalock-ble` | BLE client library for the Danalock V3 lock | public | English |
|
|
13
|
+
| `pydanalock-cloud` (this repo) | Cloud API client (OAuth2, devices, keys) | public | English |
|
|
14
|
+
| `danalock-ble-ha-integration` | Home Assistant custom integration, consumes both libraries | public | English |
|
|
15
|
+
|
|
16
|
+
`pydanalock-ble` and `pydanalock-cloud` are independent of each other; key
|
|
17
|
+
material is passed between them as opaque bytes (group contract type
|
|
18
|
+
`DeviceKey`: serial, login_blob, broadcast_key, validity, permissions). This
|
|
19
|
+
repository is the producer side of `DeviceKey`.
|
|
20
|
+
|
|
21
|
+
## 2. Language
|
|
22
|
+
|
|
23
|
+
All repository text is English: code, comments, docstrings, documentation,
|
|
24
|
+
commit messages, and review notes.
|
|
25
|
+
|
|
26
|
+
## 3. Stack and commands
|
|
27
|
+
|
|
28
|
+
- Python >= 3.11, hatchling, src-layout (`src/pydanalock/cloud/`), PEP 420
|
|
29
|
+
namespace package `pydanalock` shared with `pydanalock-ble`.
|
|
30
|
+
- Runtime dependency: `httpx>=0.28`; dev tools: pytest, pytest-asyncio, ruff,
|
|
31
|
+
mypy (strict for `src/`).
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
35
|
+
.venv/bin/ruff check .
|
|
36
|
+
.venv/bin/ruff format --check .
|
|
37
|
+
.venv/bin/mypy
|
|
38
|
+
.venv/bin/pytest -m "not live"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Live tests (real cloud, real credentials) carry the `live` marker and are run
|
|
42
|
+
manually only: `.venv/bin/pytest -m live`. Credentials come from the
|
|
43
|
+
environment: `DANALOCK_USERNAME`, `DANALOCK_PASSWORD`.
|
|
44
|
+
|
|
45
|
+
## 4. Spec-driven development (SDD)
|
|
46
|
+
|
|
47
|
+
- Every feature starts with a spec in `specs/NNNN-slug.md` (zero-padded
|
|
48
|
+
number, short slug). Status lifecycle: `draft → approved → implemented →
|
|
49
|
+
superseded`.
|
|
50
|
+
- Implementation without an `approved` spec is forbidden. Specs marked
|
|
51
|
+
"implementation pending research" must not be implemented until they move to
|
|
52
|
+
`approved`.
|
|
53
|
+
- Spec template: Summary / Motivation / Requirements (MUST/SHOULD) / Design /
|
|
54
|
+
API / Test plan / Acceptance criteria / Out of scope / Status.
|
|
55
|
+
- Moving a spec from `draft` to `approved` is a reviewable change (PR or
|
|
56
|
+
recorded decision in the conversation).
|
|
57
|
+
|
|
58
|
+
## 5. Test-driven development (TDD)
|
|
59
|
+
|
|
60
|
+
- Tests are written first and must fail before the implementation exists.
|
|
61
|
+
- API responses are covered by recorded fixtures via an httpx mock transport;
|
|
62
|
+
unit tests never touch the production API.
|
|
63
|
+
- A merge requires a fully green run of all gate commands (§3).
|
|
64
|
+
|
|
65
|
+
## 6. Git process
|
|
66
|
+
|
|
67
|
+
- Default branch: `master`. Direct commits to `master` are forbidden (the
|
|
68
|
+
initial scaffold import is the only exception).
|
|
69
|
+
- One feature = one branch `feat/NNNN-slug` containing the spec, the tests,
|
|
70
|
+
and the implementation together.
|
|
71
|
+
- Commit messages follow Conventional Commits (`feat:`, `fix:`, `docs:`,
|
|
72
|
+
`test:`, `chore:`, `refactor:`).
|
|
73
|
+
- Merge into `master` only after the review gate (§7), squash-merge, then
|
|
74
|
+
delete the feature branch.
|
|
75
|
+
|
|
76
|
+
## 7. Review gate (mandatory)
|
|
77
|
+
|
|
78
|
+
A feature branch may merge into `master` only when both hold:
|
|
79
|
+
|
|
80
|
+
1. A full local run is green (§3/§5).
|
|
81
|
+
2. A skeptic review returns no `BLOCKING` findings. Invoke the skeptic agent
|
|
82
|
+
(Task tool, subagent `skeptic`, definition in `.kilo/agent/skeptic.md`)
|
|
83
|
+
with a prompt such as:
|
|
84
|
+
|
|
85
|
+
> Review branch `feat/NNNN-slug` against its spec `specs/NNNN-slug.md`
|
|
86
|
+
> (diff base: `master`). Follow the checklist in
|
|
87
|
+
> `.kilo/agent/skeptic.md` and answer in the verdict format
|
|
88
|
+
> (`BLOCKING: ...` / `NITS: ...` / `APPROVED`).
|
|
89
|
+
|
|
90
|
+
`BLOCKING` findings forbid the merge; fix and re-review.
|
|
91
|
+
|
|
92
|
+
## 8. Secrets and safety
|
|
93
|
+
|
|
94
|
+
- Your own device and account data are confidential: never include them in
|
|
95
|
+
this public repository — in code, docs, examples, fixtures, or commit
|
|
96
|
+
history. This covers lock serial numbers, device addresses, key material,
|
|
97
|
+
and account data (usernames, passwords, tokens). Use synthetic
|
|
98
|
+
placeholders (`<serial>`, `<username>`); real values live only in
|
|
99
|
+
environment variables or gitignored `*.local.json` files.
|
|
100
|
+
- Never log tokens, keys, or Authorization headers; tests must verify log
|
|
101
|
+
redaction where logging is involved.
|
|
102
|
+
|
|
103
|
+
## 9. Publication policy (public repository)
|
|
104
|
+
|
|
105
|
+
- Do not copy text or material from non-public sources into this repo.
|
|
106
|
+
- Public documentation is an English description of API behavior. Cite only
|
|
107
|
+
this repository's own specs; do not name non-public or third-party sources,
|
|
108
|
+
and do not state or claim how the API was determined.
|
|
109
|
+
- Everything in this repo is English (§2); keep the disclaimers in place
|
|
110
|
+
(unofficial, not affiliated with Danalock AS, own devices only).
|
|
111
|
+
|
|
112
|
+
## 10. References
|
|
113
|
+
|
|
114
|
+
- Specs: `specs/` (0001 OAuth2 authentication is the first deliverable).
|
|
115
|
+
- Group contract: `DeviceKey` (specs 0002/0003).
|
|
116
|
+
- Python >= 3.11; versioning: SemVer, 0.x while the API is unstable.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 The pydanalock-cloud contributors
|
|
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,99 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pydanalock-cloud
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Unofficial Python client for the Danalock cloud API (OAuth2, devices, keys)
|
|
5
|
+
Project-URL: Homepage, https://github.com/buggy-shep/pydanalock-cloud
|
|
6
|
+
Project-URL: Repository, https://github.com/buggy-shep/pydanalock-cloud
|
|
7
|
+
Project-URL: Issues, https://github.com/buggy-shep/pydanalock-cloud/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/buggy-shep/pydanalock-cloud/releases
|
|
9
|
+
Author: The pydanalock-cloud contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cloud,danalock,home-automation,oauth2,smart-lock
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Home Automation
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Requires-Dist: httpx>=0.28
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
29
|
+
Provides-Extra: release
|
|
30
|
+
Requires-Dist: build>=1.2; extra == 'release'
|
|
31
|
+
Requires-Dist: twine>=5; extra == 'release'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# pydanalock-cloud
|
|
35
|
+
|
|
36
|
+
[](https://pypi.org/project/pydanalock-cloud/)
|
|
37
|
+
[](https://github.com/buggy-shep/pydanalock-cloud/actions/workflows/ci.yml)
|
|
38
|
+
|
|
39
|
+
Unofficial Python client for the Danalock cloud API: OAuth2 authentication,
|
|
40
|
+
device listing, and retrieval of the per-device key material (login token
|
|
41
|
+
blob, broadcast key) used by BLE clients such as
|
|
42
|
+
[pydanalock-ble](https://github.com/buggy-shep/pydanalock-ble).
|
|
43
|
+
|
|
44
|
+
> **Disclaimer.** This project is unofficial and is not affiliated with,
|
|
45
|
+
> endorsed by, or sponsored by Danalock AS or Poly-Control. It is built for
|
|
46
|
+
> interoperability with devices you own. Use it at your own risk; only control
|
|
47
|
+
> devices you are authorized to control.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install pydanalock-cloud
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Status
|
|
56
|
+
|
|
57
|
+
OAuth2 authentication, device/key retrieval, forced key refresh, client
|
|
58
|
+
lifecycle, and latest firmware lookup are implemented (specs 0001–0006);
|
|
59
|
+
0.x — the API may still change. See [`specs/`](specs/) for the
|
|
60
|
+
specifications and their status.
|
|
61
|
+
|
|
62
|
+
Capabilities:
|
|
63
|
+
|
|
64
|
+
- OAuth2 authentication (refresh + password grants) with pluggable token
|
|
65
|
+
storage — [spec 0001](specs/0001-oauth2-auth.md)
|
|
66
|
+
- Device listing and key retrieval with automatic refresh —
|
|
67
|
+
[spec 0002](specs/0002-devices-keys.md)
|
|
68
|
+
- Public sync + async client API (`DanalockCloud`, `AsyncDanalockCloud`,
|
|
69
|
+
`DeviceKey`) — [spec 0003](specs/0003-client-api.md)
|
|
70
|
+
- Client lifecycle close methods — [spec 0004](specs/0004-client-lifecycle.md)
|
|
71
|
+
- Forced key refetch (`get_key(..., force=True)`) —
|
|
72
|
+
[spec 0005](specs/0005-key-refetch-force.md)
|
|
73
|
+
- Latest firmware lookup (unauthenticated firmware service; the download
|
|
74
|
+
URL in the result is a short-lived signed link and must not be
|
|
75
|
+
persisted) — [spec 0006](specs/0006-firmware-latest.md)
|
|
76
|
+
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- Python >= 3.11
|
|
80
|
+
- [httpx](https://pypi.org/project/httpx/) >= 0.28
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python3 -m venv .venv
|
|
86
|
+
.venv/bin/pip install -e ".[dev]"
|
|
87
|
+
.venv/bin/ruff check .
|
|
88
|
+
.venv/bin/ruff format --check .
|
|
89
|
+
.venv/bin/mypy
|
|
90
|
+
.venv/bin/pytest -m "not live"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Live tests against the production API stay behind the `live` marker and are
|
|
94
|
+
run manually only, with credentials from the environment
|
|
95
|
+
(`DANALOCK_USERNAME`, `DANALOCK_PASSWORD`).
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# pydanalock-cloud
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pydanalock-cloud/)
|
|
4
|
+
[](https://github.com/buggy-shep/pydanalock-cloud/actions/workflows/ci.yml)
|
|
5
|
+
|
|
6
|
+
Unofficial Python client for the Danalock cloud API: OAuth2 authentication,
|
|
7
|
+
device listing, and retrieval of the per-device key material (login token
|
|
8
|
+
blob, broadcast key) used by BLE clients such as
|
|
9
|
+
[pydanalock-ble](https://github.com/buggy-shep/pydanalock-ble).
|
|
10
|
+
|
|
11
|
+
> **Disclaimer.** This project is unofficial and is not affiliated with,
|
|
12
|
+
> endorsed by, or sponsored by Danalock AS or Poly-Control. It is built for
|
|
13
|
+
> interoperability with devices you own. Use it at your own risk; only control
|
|
14
|
+
> devices you are authorized to control.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install pydanalock-cloud
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Status
|
|
23
|
+
|
|
24
|
+
OAuth2 authentication, device/key retrieval, forced key refresh, client
|
|
25
|
+
lifecycle, and latest firmware lookup are implemented (specs 0001–0006);
|
|
26
|
+
0.x — the API may still change. See [`specs/`](specs/) for the
|
|
27
|
+
specifications and their status.
|
|
28
|
+
|
|
29
|
+
Capabilities:
|
|
30
|
+
|
|
31
|
+
- OAuth2 authentication (refresh + password grants) with pluggable token
|
|
32
|
+
storage — [spec 0001](specs/0001-oauth2-auth.md)
|
|
33
|
+
- Device listing and key retrieval with automatic refresh —
|
|
34
|
+
[spec 0002](specs/0002-devices-keys.md)
|
|
35
|
+
- Public sync + async client API (`DanalockCloud`, `AsyncDanalockCloud`,
|
|
36
|
+
`DeviceKey`) — [spec 0003](specs/0003-client-api.md)
|
|
37
|
+
- Client lifecycle close methods — [spec 0004](specs/0004-client-lifecycle.md)
|
|
38
|
+
- Forced key refetch (`get_key(..., force=True)`) —
|
|
39
|
+
[spec 0005](specs/0005-key-refetch-force.md)
|
|
40
|
+
- Latest firmware lookup (unauthenticated firmware service; the download
|
|
41
|
+
URL in the result is a short-lived signed link and must not be
|
|
42
|
+
persisted) — [spec 0006](specs/0006-firmware-latest.md)
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Python >= 3.11
|
|
47
|
+
- [httpx](https://pypi.org/project/httpx/) >= 0.28
|
|
48
|
+
|
|
49
|
+
## Development
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 -m venv .venv
|
|
53
|
+
.venv/bin/pip install -e ".[dev]"
|
|
54
|
+
.venv/bin/ruff check .
|
|
55
|
+
.venv/bin/ruff format --check .
|
|
56
|
+
.venv/bin/mypy
|
|
57
|
+
.venv/bin/pytest -m "not live"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Live tests against the production API stay behind the `live` marker and are
|
|
61
|
+
run manually only, with credentials from the environment
|
|
62
|
+
(`DANALOCK_USERNAME`, `DANALOCK_PASSWORD`).
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pydanalock-cloud"
|
|
7
|
+
version = "0.5.0"
|
|
8
|
+
description = "Unofficial Python client for the Danalock cloud API (OAuth2, devices, keys)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.11"
|
|
13
|
+
authors = [{ name = "The pydanalock-cloud contributors" }]
|
|
14
|
+
keywords = ["danalock", "cloud", "oauth2", "smart-lock", "home-automation"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Home Automation",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.28",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/buggy-shep/pydanalock-cloud"
|
|
32
|
+
Repository = "https://github.com/buggy-shep/pydanalock-cloud"
|
|
33
|
+
Issues = "https://github.com/buggy-shep/pydanalock-cloud/issues"
|
|
34
|
+
Changelog = "https://github.com/buggy-shep/pydanalock-cloud/releases"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"mypy>=1.11",
|
|
39
|
+
"pytest>=8.2",
|
|
40
|
+
"pytest-asyncio>=0.24",
|
|
41
|
+
"ruff>=0.6",
|
|
42
|
+
]
|
|
43
|
+
release = [
|
|
44
|
+
"build>=1.2",
|
|
45
|
+
"twine>=5",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/pydanalock"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
exclude = [".kilo"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = "-m 'not live'"
|
|
57
|
+
asyncio_mode = "auto"
|
|
58
|
+
markers = [
|
|
59
|
+
"live: talks to a real lock or the real cloud; run manually only",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.mypy]
|
|
63
|
+
python_version = "3.11"
|
|
64
|
+
strict = true
|
|
65
|
+
files = ["src"]
|
|
66
|
+
mypy_path = "src"
|
|
67
|
+
explicit_package_bases = true
|
|
68
|
+
namespace_packages = true
|
|
69
|
+
|
|
70
|
+
[tool.ruff]
|
|
71
|
+
target-version = "py311"
|
|
72
|
+
line-length = 100
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["B", "E", "F", "I", "RUF", "SIM", "UP", "W"]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# 0001 — OAuth2 authentication
|
|
2
|
+
|
|
3
|
+
- **Status:** implemented
|
|
4
|
+
- **Scope:** `pydanalock.cloud` authentication against the Danalock cloud API
|
|
5
|
+
|
|
6
|
+
## Summary
|
|
7
|
+
|
|
8
|
+
The Danalock cloud (base URL `https://api.danalock.com`) issues OAuth2
|
|
9
|
+
tokens. This spec defines the token flows the client supports, token storage,
|
|
10
|
+
and automatic refresh.
|
|
11
|
+
|
|
12
|
+
## Motivation
|
|
13
|
+
|
|
14
|
+
Every device/keys endpoint requires a bearer access token; keys are the
|
|
15
|
+
contract payload shared with the BLE library, so authentication is the cloud
|
|
16
|
+
client's first deliverable.
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- R1 (MUST) Token endpoint: `POST /oauth2/token`,
|
|
21
|
+
`application/x-www-form-urlencoded`. Supported grants:
|
|
22
|
+
- `refresh_token` — used automatically;
|
|
23
|
+
- `password` (resource-owner password credentials) — fallback for scripted
|
|
24
|
+
or hosted use; requires username and password.
|
|
25
|
+
|
|
26
|
+
The `authorization_code` grant is deferred to a future spec (recorded
|
|
27
|
+
decision, 2026-09-06): obtaining the code requires a browser redirect
|
|
28
|
+
owned by the host application, which no current consumer drives.
|
|
29
|
+
- R2 (MUST) `client_id` is required for every grant; the library default is
|
|
30
|
+
`danalock-android` (a value observed working against the production API);
|
|
31
|
+
it is configurable.
|
|
32
|
+
- R3 (MUST) Token response fields used: `access_token`, `refresh_token`,
|
|
33
|
+
`expires_in`. The client computes `expires_at` locally (epoch seconds).
|
|
34
|
+
- R4 (MUST) Automatic refresh: a 401 response, or a token expiring within a
|
|
35
|
+
60-second safety margin, triggers one `refresh_token` grant and one retry
|
|
36
|
+
of the original request. Refresh failure surfaces as `AuthError`.
|
|
37
|
+
- R5 (MUST) Token storage is pluggable:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
class TokenStorage(Protocol):
|
|
41
|
+
def load(self) -> TokenData | None: ...
|
|
42
|
+
def save(self, token: TokenData) -> None: ...
|
|
43
|
+
def clear(self) -> None: ...
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
An in-memory implementation MUST be provided; host applications supply
|
|
47
|
+
persistent ones. Tokens and credentials MUST NOT be logged or committed.
|
|
48
|
+
- R6 (MUST) Typed errors: `AuthError` (invalid credentials/grant), `ApiError`
|
|
49
|
+
(unexpected HTTP status), transport errors propagated from httpx.
|
|
50
|
+
- R7 (SHOULD) All requests set `Accept: application/json` and use a
|
|
51
|
+
configurable timeout (default 30 seconds).
|
|
52
|
+
|
|
53
|
+
## Design
|
|
54
|
+
|
|
55
|
+
- One httpx client instance per `DanalockCloud`; the base URL and transport
|
|
56
|
+
are overridable for tests (MockTransport).
|
|
57
|
+
- The password grant exists because the authorization-code flow needs a
|
|
58
|
+
browser/redirect endpoint that standalone scripts and servers do not have.
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class TokenData:
|
|
65
|
+
access_token: str
|
|
66
|
+
refresh_token: str
|
|
67
|
+
expires_at: float # epoch seconds
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DanalockCloud:
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
*,
|
|
74
|
+
client_id: str = "danalock-android",
|
|
75
|
+
base_url: str = "https://api.danalock.com",
|
|
76
|
+
storage: TokenStorage | None = None,
|
|
77
|
+
) -> None: ...
|
|
78
|
+
def login_password(self, username: str, password: str) -> None: ...
|
|
79
|
+
def refresh(self) -> None: ...
|
|
80
|
+
def access_token(self) -> str: ... # valid token, refreshing as needed
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
(Async variants are defined in spec 0003.)
|
|
84
|
+
|
|
85
|
+
## Test plan
|
|
86
|
+
|
|
87
|
+
- httpx MockTransport fixtures: successful password grant; refresh grant;
|
|
88
|
+
401 → refresh → retry; refresh failure → `AuthError`; storage save/load.
|
|
89
|
+
- Expired-token margin: a token within the 60-second margin triggers refresh
|
|
90
|
+
before the request.
|
|
91
|
+
- No secrets in logged output (test asserts redaction).
|
|
92
|
+
|
|
93
|
+
## Acceptance criteria
|
|
94
|
+
|
|
95
|
+
- Unit tests green (mock transport); gate green; skeptic without `BLOCKING`.
|
|
96
|
+
- Live (manual, `live` marker): password login against the production API
|
|
97
|
+
succeeds with real credentials from environment variables.
|
|
98
|
+
|
|
99
|
+
## Out of scope
|
|
100
|
+
|
|
101
|
+
- Authorization-code grant and its redirect UI (deferred to a future spec;
|
|
102
|
+
see R1); key retrieval (spec 0002); public API surface (spec 0003).
|
|
103
|
+
|
|
104
|
+
## Status
|
|
105
|
+
|
|
106
|
+
`implemented` (2026-09-06: password-grant authentication with token
|
|
107
|
+
storage, refresh and expiry tracking merged, version 0.1.0).
|