pysnapdog 0.1.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 (45) hide show
  1. pysnapdog-0.1.0/.editorconfig +19 -0
  2. pysnapdog-0.1.0/.github/CODEOWNERS +1 -0
  3. pysnapdog-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  4. pysnapdog-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +16 -0
  5. pysnapdog-0.1.0/.github/dependabot.yml +48 -0
  6. pysnapdog-0.1.0/.github/pull_request_template.md +18 -0
  7. pysnapdog-0.1.0/.github/workflows/ci.yml +145 -0
  8. pysnapdog-0.1.0/.github/workflows/release-please.yml +47 -0
  9. pysnapdog-0.1.0/.github/workflows/release.yml +92 -0
  10. pysnapdog-0.1.0/.gitignore +56 -0
  11. pysnapdog-0.1.0/.python-version +1 -0
  12. pysnapdog-0.1.0/.release-please-manifest.json +3 -0
  13. pysnapdog-0.1.0/CHANGELOG.md +15 -0
  14. pysnapdog-0.1.0/CONTRIBUTING.md +59 -0
  15. pysnapdog-0.1.0/LICENSE +202 -0
  16. pysnapdog-0.1.0/PKG-INFO +218 -0
  17. pysnapdog-0.1.0/README.md +193 -0
  18. pysnapdog-0.1.0/SECURITY.md +26 -0
  19. pysnapdog-0.1.0/lefthook.yml +32 -0
  20. pysnapdog-0.1.0/pyproject.toml +91 -0
  21. pysnapdog-0.1.0/release-please-config.json +21 -0
  22. pysnapdog-0.1.0/scripts/check_coverage.py +37 -0
  23. pysnapdog-0.1.0/scripts/hooks/check-commit-message.sh +65 -0
  24. pysnapdog-0.1.0/scripts/hooks/check-push-secrets.sh +33 -0
  25. pysnapdog-0.1.0/scripts/hooks/check-staged.sh +48 -0
  26. pysnapdog-0.1.0/spec/asyncapi.json +657 -0
  27. pysnapdog-0.1.0/spec/openapi.json +6356 -0
  28. pysnapdog-0.1.0/src/pysnapdog/__init__.py +97 -0
  29. pysnapdog-0.1.0/src/pysnapdog/client.py +575 -0
  30. pysnapdog-0.1.0/src/pysnapdog/events.py +257 -0
  31. pysnapdog-0.1.0/src/pysnapdog/exceptions.py +44 -0
  32. pysnapdog-0.1.0/src/pysnapdog/listener.py +195 -0
  33. pysnapdog-0.1.0/src/pysnapdog/models.py +525 -0
  34. pysnapdog-0.1.0/src/pysnapdog/py.typed +0 -0
  35. pysnapdog-0.1.0/tests/__init__.py +0 -0
  36. pysnapdog-0.1.0/tests/conftest.py +147 -0
  37. pysnapdog-0.1.0/tests/coverage.py +97 -0
  38. pysnapdog-0.1.0/tests/spec.py +67 -0
  39. pysnapdog-0.1.0/tests/test_client.py +284 -0
  40. pysnapdog-0.1.0/tests/test_coverage.py +37 -0
  41. pysnapdog-0.1.0/tests/test_events.py +93 -0
  42. pysnapdog-0.1.0/tests/test_listener.py +419 -0
  43. pysnapdog-0.1.0/tests/test_models.py +158 -0
  44. pysnapdog-0.1.0/tests/test_operations.py +300 -0
  45. pysnapdog-0.1.0/uv.lock +999 -0
@@ -0,0 +1,19 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_style = space
7
+ indent_size = 4
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+ max_line_length = 100
11
+
12
+ [*.{toml,yml,yaml,json,jsonc}]
13
+ indent_size = 2
14
+
15
+ [*.{ts,tsx,js,mjs,cjs}]
16
+ indent_size = 2
17
+
18
+ [*.md]
19
+ trim_trailing_whitespace = false
@@ -0,0 +1 @@
1
+ * @metaneutrons
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: Bug report
3
+ about: Report something that is not working as expected
4
+ title: "[Bug] "
5
+ labels: bug
6
+ ---
7
+
8
+ **Describe the bug**
9
+ A clear, short description of what is wrong.
10
+
11
+ **To reproduce**
12
+ Steps to reproduce the behavior:
13
+ 1. ...
14
+ 2. ...
15
+
16
+ **Expected behavior**
17
+ What you expected to happen instead.
18
+
19
+ **Actual behavior**
20
+ What actually happened (error messages, logs, screenshots).
21
+
22
+ **Environment**
23
+ - pysnapdog version:
24
+ - SnapDog server version:
25
+ - Python version:
26
+ - OS / platform:
27
+ - Minimal code that shows the problem (redact the API key):
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea or enhancement for this project
4
+ title: "[Feature] "
5
+ labels: enhancement
6
+ ---
7
+
8
+ **Problem**
9
+ What problem or limitation are you running into? Why does it matter?
10
+
11
+ **Proposed solution**
12
+ What you would like to happen. Describe the feature or change you have in
13
+ mind.
14
+
15
+ **Alternatives considered**
16
+ Any alternative solutions or workarounds you have considered.
@@ -0,0 +1,48 @@
1
+ # Template. Goes to .github/dependabot.yml; delete the ecosystems the project
2
+ # does not have.
3
+ #
4
+ # The grouping is deliberately separated by update kind. One group with
5
+ # `patterns: ['*']` across all kinds bundles breaking changes with harmless
6
+ # bumps, and one broken dependency then blocks the whole stack. Minor and patch
7
+ # stay bundled, because checking them individually gains nothing there. Major
8
+ # stays ungrouped, so that each case arrives on its own.
9
+ #
10
+ # `cooldown` keeps a version that is withdrawn shortly after publication out of
11
+ # every pull request.
12
+ # Without a `timezone`, times are in UTC. Anyone needing local execution times
13
+ # enters a suitable IANA time zone per project.
14
+
15
+ version: 2
16
+
17
+ updates:
18
+ - package-ecosystem: uv
19
+ directory: /
20
+ schedule:
21
+ interval: weekly
22
+ day: monday
23
+ time: '05:00'
24
+ open-pull-requests-limit: 5
25
+ cooldown:
26
+ default-days: 3
27
+ semver-major-days: 7
28
+ groups:
29
+ python-minor-patch:
30
+ patterns: ['*']
31
+ update-types: [minor, patch]
32
+
33
+ - package-ecosystem: github-actions
34
+ directory: /
35
+ schedule:
36
+ interval: weekly
37
+ day: monday
38
+ time: '05:15'
39
+ open-pull-requests-limit: 5
40
+ cooldown:
41
+ default-days: 3
42
+ # semver-major-days is not permitted for github-actions. If it stands
43
+ # here, Dependabot rejects the ENTIRE file and not a single update runs
44
+ # any more, not even a security update.
45
+ groups:
46
+ github-actions-minor-patch:
47
+ patterns: ['*']
48
+ update-types: [minor, patch]
@@ -0,0 +1,18 @@
1
+ ## What changed
2
+
3
+ <!-- Briefly describe the change. -->
4
+
5
+ ## Why
6
+
7
+ <!-- What problem does this solve, or what does it improve? -->
8
+
9
+ ## How it was tested
10
+
11
+ <!-- e.g. `uv run pytest`, `uv run ruff check .`, `uv run mypy`, manual verification, new/updated tests. -->
12
+
13
+ ---
14
+
15
+ - [ ] The PR title follows [Conventional Commits](https://www.conventionalcommits.org/)
16
+ (`type(scope): description`, `!` or `BREAKING CHANGE:` for breaking
17
+ changes) — it becomes the commit message on `main` after squash merge
18
+ and drives the release version and changelog.
@@ -0,0 +1,145 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ quality:
18
+ name: Lint, types, tests (Python ${{ matrix.python }})
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ python: ['3.14']
23
+ runs-on: ubuntu-24.04
24
+ steps:
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
26
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
27
+ with:
28
+ python-version: ${{ matrix.python }}
29
+ enable-cache: true
30
+ # --locked fails when uv.lock does not match pyproject.toml. That is
31
+ # exactly what it is for.
32
+ - run: uv sync --locked
33
+ - run: uv run ruff check .
34
+ - run: uv run ruff format --check .
35
+ - name: Check the hook configuration
36
+ run: uv run lefthook validate
37
+ - run: uv run mypy
38
+ # The coverage floor (fail_under) lives in pyproject.toml.
39
+ - run: uv run pytest --cov
40
+ - name: Coverage of the server contracts
41
+ run: uv run python scripts/check_coverage.py
42
+ - name: Check the distribution content
43
+ run: |
44
+ set -euo pipefail
45
+ uv build --out-dir "$RUNNER_TEMP/dist"
46
+ unzip -l "$RUNNER_TEMP"/dist/*.whl | grep -E 'pysnapdog/(py.typed|client.py)'
47
+ tar -tzf "$RUNNER_TEMP"/dist/*.tar.gz | sort
48
+
49
+ security:
50
+ name: Secrets and advisories
51
+ runs-on: ubuntu-24.04
52
+ steps:
53
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
54
+ with:
55
+ fetch-depth: 0
56
+ # The gitleaks action wants a licence for organisation repositories. The CLI it wraps
57
+ # does not, so run the pinned release binary and verify its checksum.
58
+ - name: Scan the history for secrets
59
+ run: |
60
+ set -euo pipefail
61
+ version=8.30.1
62
+ archive="$RUNNER_TEMP/gitleaks.tar.gz"
63
+ curl -sSfL -o "$archive" \
64
+ "https://github.com/gitleaks/gitleaks/releases/download/v${version}/gitleaks_${version}_linux_x64.tar.gz"
65
+ echo "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb $archive" | sha256sum -c -
66
+ tar -xzf "$archive" -C "$RUNNER_TEMP" gitleaks
67
+ "$RUNNER_TEMP/gitleaks" detect --source . --redact --no-banner --verbose
68
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
69
+ with:
70
+ python-version: '3.14'
71
+ - run: uv sync --locked
72
+ - run: uv audit --preview-features audit-command
73
+
74
+ commit-hygiene:
75
+ name: Commit hygiene
76
+ if: github.event_name == 'pull_request'
77
+ runs-on: ubuntu-24.04
78
+ steps:
79
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
80
+ with:
81
+ fetch-depth: 0
82
+ - name: Check title, body and every commit
83
+ # Title and body reach the script exclusively through env, never by
84
+ # interpolation into the script text: otherwise the pull request title
85
+ # would be a shell injection vector.
86
+ env:
87
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
88
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
89
+ PR_TITLE: ${{ github.event.pull_request.title }}
90
+ PR_BODY: ${{ github.event.pull_request.body }}
91
+ run: |
92
+ set -euo pipefail
93
+ status=0
94
+ cc='^((feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._/-]+\))?!?: .+|Revert ".+")'
95
+ # grep works line by line, hence .* rather than [^\n]*: in ERE
96
+ # [^\n] would be the class "neither a backslash nor an n" and would
97
+ # exclude names containing an 'n'. In the RE2 rule of the ruleset
98
+ # [^\n] is the correct one. Anchored at the start of the line: by
99
+ # convention a trailer stands on a line of its own. Without the anchor
100
+ # the rule also fires when a pull request body merely mentions the
101
+ # forbidden trailer, for instance to describe a doctor finding, and it
102
+ # then rejects a valid input.
103
+ ban='^[[:space:]]*co-authored-by:.*(claude|anthropic)|^[[:space:]]*generated with.*claude code'
104
+
105
+ # On a squash merge the pull request title becomes the subject line
106
+ # on main and thereby determines the next version.
107
+ if ! printf '%s' "$PR_TITLE" | grep -Eq "$cc"; then
108
+ echo "::error::pull request title does not follow Conventional Commits: $PR_TITLE"
109
+ status=1
110
+ fi
111
+
112
+ # With squash_merge_commit_message=PR_BODY the body lands in the commit.
113
+ if printf '%s' "$PR_BODY" | grep -Eiq "$ban"; then
114
+ echo "::error::the pull request body contains an AI attribution trailer."
115
+ status=1
116
+ fi
117
+
118
+ for sha in $(git rev-list "${BASE_SHA}..${HEAD_SHA}"); do
119
+ if git log -1 --format=%B "$sha" | grep -Eiq "$ban"; then
120
+ echo "::error::commit $sha contains an AI attribution trailer."
121
+ status=1
122
+ fi
123
+ email=$(git log -1 --format='%ae%n%ce' "$sha")
124
+ if printf '%s' "$email" | grep -Fq 'noreply@anthropic.com'; then
125
+ echo "::error::commit $sha has an Anthropic address as author or committer."
126
+ status=1
127
+ fi
128
+ done
129
+
130
+ if [ "$status" -ne 0 ]; then
131
+ echo "::notice::switch this off for good in ~/.claude/settings.json: {\"attribution\":{\"commit\":\"\",\"pr\":\"\"}}"
132
+ exit 1
133
+ fi
134
+
135
+ ci-success:
136
+ name: CI Success
137
+ if: always()
138
+ needs: [quality, security, commit-hygiene]
139
+ runs-on: ubuntu-24.04
140
+ steps:
141
+ - name: Every preceding job must have succeeded or been skipped
142
+ run: |
143
+ set -euo pipefail
144
+ printf '%s\n' '${{ toJSON(needs) }}' |
145
+ jq -e 'all(.[]; .result == "success" or .result == "skipped")'
@@ -0,0 +1,47 @@
1
+ # The only automatic route by which a production tag comes about.
2
+ name: Release Please
3
+
4
+ on:
5
+ push:
6
+ branches: [main]
7
+
8
+ concurrency:
9
+ group: ${{ github.workflow }}-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ permissions:
13
+ contents: write
14
+ pull-requests: write
15
+ actions: write
16
+
17
+ jobs:
18
+ release-please:
19
+ runs-on: ubuntu-24.04
20
+ # Per the non-negotiable rule the app credentials live exclusively in this
21
+ # environment. Without the declaration they are invisible to the job.
22
+ environment: release
23
+ steps:
24
+ # The release pull request MUST be created with an app token. GitHub
25
+ # suppresses workflow triggers from GITHUB_TOKEN pushes as recursion
26
+ # protection; CI would then never run on the release pull request and the
27
+ # required checks would stay pending. An app token is a genuine actor.
28
+ - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3
29
+ id: app-token
30
+ with:
31
+ client-id: ${{ vars.RELEASE_PLEASE_CLIENT_ID }}
32
+ private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
33
+
34
+ - uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
35
+ id: rp
36
+ with:
37
+ token: ${{ steps.app-token.outputs.token }}
38
+ config-file: release-please-config.json
39
+ manifest-file: .release-please-manifest.json
40
+
41
+ # Hand the draft release to the publish workflow. The tag exists (force-tag-creation).
42
+ - name: Start the release workflow
43
+ if: steps.rp.outputs.release_created == 'true'
44
+ env:
45
+ GH_TOKEN: ${{ steps.app-token.outputs.token }}
46
+ TAG: ${{ steps.rp.outputs.tag_name }}
47
+ run: gh workflow run release.yml --repo "$GITHUB_REPOSITORY" -f tag="$TAG"
@@ -0,0 +1,92 @@
1
+ # Publishes a release to PyPI. Started by release-please.yml for the draft release it created,
2
+ # or by hand for a tag that exists. The first upload uses the PYPI_TOKEN of the release
3
+ # environment. Once the project exists on PyPI, a trusted publisher replaces the token
4
+ # (id-token: write, no password) and the secret is deleted.
5
+ name: Release
6
+
7
+ on:
8
+ workflow_dispatch:
9
+ inputs:
10
+ tag:
11
+ description: Release tag, for example v0.1.0
12
+ required: true
13
+ type: string
14
+
15
+ permissions:
16
+ contents: write
17
+
18
+ concurrency:
19
+ group: release-${{ inputs.tag }}
20
+ cancel-in-progress: false
21
+
22
+ jobs:
23
+ publish:
24
+ runs-on: ubuntu-24.04
25
+ environment: release
26
+ steps:
27
+ - name: Preflight
28
+ env:
29
+ TAG: ${{ inputs.tag }}
30
+ PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
31
+ GH_TOKEN: ${{ github.token }}
32
+ run: |
33
+ set -euo pipefail
34
+ [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "bad tag: $TAG"; exit 1; }
35
+ [ -n "$PYPI_TOKEN" ] || { echo "PYPI_TOKEN is empty"; exit 1; }
36
+ gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null
37
+
38
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
39
+ with:
40
+ ref: refs/tags/${{ inputs.tag }}
41
+
42
+ - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
43
+ with:
44
+ python-version: '3.14'
45
+
46
+ - name: Check that the tag and the package agree
47
+ env:
48
+ TAG: ${{ inputs.tag }}
49
+ run: |
50
+ set -euo pipefail
51
+ version=$(uv run --no-project python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
52
+ [ "v$version" = "$TAG" ] || { echo "pyproject says $version, the tag is $TAG"; exit 1; }
53
+
54
+ - name: Test and build
55
+ run: |
56
+ set -euo pipefail
57
+ uv sync --locked
58
+ uv run pytest --cov
59
+ uv build --out-dir dist
60
+
61
+ - name: Upload to PyPI
62
+ env:
63
+ UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
64
+ run: uv publish dist/*
65
+
66
+ - name: Verify that PyPI serves exactly what was built
67
+ env:
68
+ TAG: ${{ inputs.tag }}
69
+ run: |
70
+ set -euo pipefail
71
+ version="${TAG#v}"
72
+ for file in dist/*; do
73
+ name=$(basename "$file")
74
+ want=$(sha256sum "$file" | cut -d' ' -f1)
75
+ for _ in 1 2 3 4 5 6 7 8 9 10 11 12; do
76
+ got=$(curl -sSf "https://pypi.org/pypi/pysnapdog/${version}/json" \
77
+ | jq -r --arg n "$name" '.urls[] | select(.filename==$n) | .digests.sha256' || true)
78
+ [ "$got" = "$want" ] && break
79
+ sleep 10
80
+ done
81
+ [ "$got" = "$want" ] || { echo "$name: PyPI has '$got', built $want"; exit 1; }
82
+ echo "$name verified"
83
+ done
84
+
85
+ - name: Publish the GitHub release
86
+ env:
87
+ TAG: ${{ inputs.tag }}
88
+ GH_TOKEN: ${{ github.token }}
89
+ run: |
90
+ set -euo pipefail
91
+ gh release upload "$TAG" dist/* --clobber --repo "$GITHUB_REPOSITORY"
92
+ gh release edit "$TAG" --draft=false --repo "$GITHUB_REPOSITORY"
@@ -0,0 +1,56 @@
1
+ # Patterns deliberately UNANCHORED, so that they bite at every level.
2
+ # Canonical inputs and generators stay tracked; reproducible outputs do not.
3
+ # uv.lock and .python-version are tracked on purpose.
4
+
5
+ __pycache__/
6
+ *.py[cod]
7
+ .venv/
8
+ venv/
9
+ dist/
10
+ build/
11
+ *.egg-info/
12
+ .eggs/
13
+
14
+ # Tool caches and coverage
15
+ .pytest_cache/
16
+ .mypy_cache/
17
+ .ruff_cache/
18
+ .coverage
19
+ .coverage.*
20
+ coverage.xml
21
+ htmlcov/
22
+
23
+ # Environment and state
24
+ .env
25
+ .env.*
26
+ !.env.example
27
+ *.log
28
+
29
+ # Editor and operating system
30
+ .DS_Store
31
+ .AppleDouble
32
+ .LSOverride
33
+ ._*
34
+ .DocumentRevisions-V100/
35
+ .fseventsd/
36
+ .Spotlight-V100/
37
+ .TemporaryItems/
38
+ .Trashes/
39
+ .VolumeIcon.icns
40
+ .com.apple.timemachine.donotpresent
41
+ Thumbs.db
42
+ Thumbs.db:encryptable
43
+ ehthumbs.db
44
+ ehthumbs_vista.db
45
+ [Dd]esktop.ini
46
+ $RECYCLE.BIN/
47
+ *.stackdump
48
+ .fuse_hidden*
49
+ .directory
50
+ .Trash-*
51
+ .nfs*
52
+ .idea/
53
+ .vscode/
54
+ *.swp
55
+ *.swo
56
+ *~
@@ -0,0 +1 @@
1
+ 3.14
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.1.0"
3
+ }
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (2026-11-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * bootstrap the repository ([4105e40](https://github.com/SnapDogRocks/pysnapdog/commit/4105e4044bdc9d445b11a2de33d6f3b077e182d0))
9
+ * cover the REST API and the WebSocket events of the server ([72cf902](https://github.com/SnapDogRocks/pysnapdog/commit/72cf90263d862c2d5e58a8e11137cbd56d5bd492))
10
+
11
+
12
+ ### Documentation
13
+
14
+ * align the README header with the server repository ([c0c5385](https://github.com/SnapDogRocks/pysnapdog/commit/c0c538564124faf9e54837a65fbae0d968998f12))
15
+ * drop the not-yet-released notes from the README ([#4](https://github.com/SnapDogRocks/pysnapdog/issues/4)) ([2a045cf](https://github.com/SnapDogRocks/pysnapdog/commit/2a045cf860fc0beba9e58d597c98462d5e677db3))
@@ -0,0 +1,59 @@
1
+ # Contributing
2
+
3
+ Thanks for helping. The library is small on purpose, so most changes are quick to review.
4
+
5
+ ## Setup
6
+
7
+ The project uses [uv](https://docs.astral.sh/uv/) and Python 3.14.
8
+
9
+ ```bash
10
+ uv sync
11
+ uv run lefthook install # local hooks; needs gitleaks (brew install gitleaks)
12
+ ```
13
+
14
+ ## Checks
15
+
16
+ ```bash
17
+ uv run ruff check .
18
+ uv run ruff format --check .
19
+ uv run mypy
20
+ uv run pytest --cov # coverage floor 95 %
21
+ ```
22
+
23
+ CI runs the same and `uv audit`. The hooks run a subset: `pre-commit` stays under three seconds,
24
+ `pre-push` runs mypy and the tests. If a hook is in your way, tell us in the pull request instead
25
+ of reaching for `--no-verify`.
26
+
27
+ ## Commit messages: Conventional Commits
28
+
29
+ Every pull request title follows [Conventional Commits](https://www.conventionalcommits.org/):
30
+
31
+ ```text
32
+ <type>(<scope>): <description>
33
+ ```
34
+
35
+ - `scope` is optional, lower case.
36
+ - Types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`,
37
+ `revert`.
38
+ - A breaking change is marked with `!` after the type or a `BREAKING CHANGE:` footer.
39
+
40
+ Pull requests are squash-merged, so the **title** becomes the commit on `main`, and
41
+ release-please reads it to pick the next version and the changelog entry. Commit messages,
42
+ pull request texts and branch names are written in English.
43
+
44
+ One pull request carries one kind of change. A fix that travels with a `feat` is filed under
45
+ Features in the changelog, where nobody looks for it.
46
+
47
+ ## Branches
48
+
49
+ `type/short-description`, with the same types as above, for example `feat/zone-control`.
50
+
51
+ ## Tests
52
+
53
+ The tests run against a fake server (`tests/conftest.py`) whose answers follow the server's
54
+ `openapi.json`. When you add an endpoint, add the fake route, a model if the response needs one,
55
+ and a test for the success path and for each way it can fail.
56
+
57
+ ## License
58
+
59
+ By contributing you agree that your work is licensed under the [Apache License 2.0](LICENSE).