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.
Files changed (39) hide show
  1. pydanalock_cloud-0.5.0/.github/pull_request_template.md +18 -0
  2. pydanalock_cloud-0.5.0/.github/workflows/ci.yml +51 -0
  3. pydanalock_cloud-0.5.0/.github/workflows/publish.yml +46 -0
  4. pydanalock_cloud-0.5.0/.github/workflows/testpypi.yml +47 -0
  5. pydanalock_cloud-0.5.0/.gitignore +26 -0
  6. pydanalock_cloud-0.5.0/AGENTS.md +116 -0
  7. pydanalock_cloud-0.5.0/LICENSE +21 -0
  8. pydanalock_cloud-0.5.0/PKG-INFO +99 -0
  9. pydanalock_cloud-0.5.0/README.md +66 -0
  10. pydanalock_cloud-0.5.0/pyproject.toml +75 -0
  11. pydanalock_cloud-0.5.0/specs/0001-oauth2-auth.md +107 -0
  12. pydanalock_cloud-0.5.0/specs/0002-devices-keys.md +116 -0
  13. pydanalock_cloud-0.5.0/specs/0003-client-api.md +65 -0
  14. pydanalock_cloud-0.5.0/specs/0004-client-lifecycle.md +80 -0
  15. pydanalock_cloud-0.5.0/specs/0005-key-refetch-force.md +83 -0
  16. pydanalock_cloud-0.5.0/specs/0006-firmware-latest.md +127 -0
  17. pydanalock_cloud-0.5.0/specs/0007-protocol-constants-audit.md +105 -0
  18. pydanalock_cloud-0.5.0/specs/0009-pypi-publication.md +94 -0
  19. pydanalock_cloud-0.5.0/src/pydanalock/cloud/__init__.py +30 -0
  20. pydanalock_cloud-0.5.0/src/pydanalock/cloud/aclient.py +220 -0
  21. pydanalock_cloud-0.5.0/src/pydanalock/cloud/auth.py +161 -0
  22. pydanalock_cloud-0.5.0/src/pydanalock/cloud/client.py +222 -0
  23. pydanalock_cloud-0.5.0/src/pydanalock/cloud/errors.py +31 -0
  24. pydanalock_cloud-0.5.0/src/pydanalock/cloud/models.py +213 -0
  25. pydanalock_cloud-0.5.0/src/pydanalock/cloud/py.typed +0 -0
  26. pydanalock_cloud-0.5.0/tests/conftest.py +107 -0
  27. pydanalock_cloud-0.5.0/tests/fixtures/firmware_latest.json +6 -0
  28. pydanalock_cloud-0.5.0/tests/fixtures/login_token_single.json +26 -0
  29. pydanalock_cloud-0.5.0/tests/fixtures/login_tokens_v1.json +28 -0
  30. pydanalock_cloud-0.5.0/tests/fixtures/login_tokens_v1_expired.json +28 -0
  31. pydanalock_cloud-0.5.0/tests/fixtures/oauth2_token_password.json +5 -0
  32. pydanalock_cloud-0.5.0/tests/fixtures/oauth2_token_refresh.json +5 -0
  33. pydanalock_cloud-0.5.0/tests/test_auth.py +335 -0
  34. pydanalock_cloud-0.5.0/tests/test_client.py +398 -0
  35. pydanalock_cloud-0.5.0/tests/test_devices.py +417 -0
  36. pydanalock_cloud-0.5.0/tests/test_firmware.py +221 -0
  37. pydanalock_cloud-0.5.0/tests/test_lifecycle.py +62 -0
  38. pydanalock_cloud-0.5.0/tests/test_live.py +57 -0
  39. 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
+ [![PyPI version](https://img.shields.io/pypi/v/pydanalock-cloud.svg)](https://pypi.org/project/pydanalock-cloud/)
37
+ [![CI](https://github.com/buggy-shep/pydanalock-cloud/actions/workflows/ci.yml/badge.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/pydanalock-cloud.svg)](https://pypi.org/project/pydanalock-cloud/)
4
+ [![CI](https://github.com/buggy-shep/pydanalock-cloud/actions/workflows/ci.yml/badge.svg)](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).