PyAgoraRTC 0.2.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 (162) hide show
  1. pyagorartc-0.2.0/.claude/agents/code-reviewer.md +43 -0
  2. pyagorartc-0.2.0/.claude/agents/test-reviewer.md +48 -0
  3. pyagorartc-0.2.0/.github/workflows/ci.yml +20 -0
  4. pyagorartc-0.2.0/.github/workflows/release.yml +73 -0
  5. pyagorartc-0.2.0/.gitignore +207 -0
  6. pyagorartc-0.2.0/.pre-commit-config.yaml +21 -0
  7. pyagorartc-0.2.0/.python-version +1 -0
  8. pyagorartc-0.2.0/CLAUDE.md +56 -0
  9. pyagorartc-0.2.0/CONSTITUTION.md +89 -0
  10. pyagorartc-0.2.0/CONTRIBUTING.md +23 -0
  11. pyagorartc-0.2.0/LICENSE +674 -0
  12. pyagorartc-0.2.0/PKG-INFO +151 -0
  13. pyagorartc-0.2.0/README.md +128 -0
  14. pyagorartc-0.2.0/docs/analysis/divergence.md +416 -0
  15. pyagorartc-0.2.0/docs/analysis/legacy/README.md +6 -0
  16. pyagorartc-0.2.0/docs/analysis/legacy/agora_api.py +852 -0
  17. pyagorartc-0.2.0/docs/analysis/legacy/agora_sdp.py +527 -0
  18. pyagorartc-0.2.0/docs/analysis/legacy/agora_websockets.py +1775 -0
  19. pyagorartc-0.2.0/docs/architecture.md +181 -0
  20. pyagorartc-0.2.0/docs/backlog.md +45 -0
  21. pyagorartc-0.2.0/docs/code_style.md +119 -0
  22. pyagorartc-0.2.0/docs/decisions.md +241 -0
  23. pyagorartc-0.2.0/docs/migration.md +683 -0
  24. pyagorartc-0.2.0/docs/open_questions.md +138 -0
  25. pyagorartc-0.2.0/docs/protocol.md +1166 -0
  26. pyagorartc-0.2.0/docs/testing.md +167 -0
  27. pyagorartc-0.2.0/pyagorartc/__init__.py +67 -0
  28. pyagorartc-0.2.0/pyagorartc/ap/__init__.py +17 -0
  29. pyagorartc-0.2.0/pyagorartc/ap/client.py +244 -0
  30. pyagorartc-0.2.0/pyagorartc/ap/password.py +14 -0
  31. pyagorartc-0.2.0/pyagorartc/ap/response.py +329 -0
  32. pyagorartc-0.2.0/pyagorartc/const.py +55 -0
  33. pyagorartc-0.2.0/pyagorartc/exceptions.py +61 -0
  34. pyagorartc-0.2.0/pyagorartc/models.py +223 -0
  35. pyagorartc-0.2.0/pyagorartc/py.typed +0 -0
  36. pyagorartc-0.2.0/pyagorartc/rtm/__init__.py +5 -0
  37. pyagorartc-0.2.0/pyagorartc/rtm/client.py +170 -0
  38. pyagorartc-0.2.0/pyagorartc/sdp/__init__.py +26 -0
  39. pyagorartc-0.2.0/pyagorartc/sdp/answer.py +320 -0
  40. pyagorartc-0.2.0/pyagorartc/sdp/candidates.py +103 -0
  41. pyagorartc-0.2.0/pyagorartc/sdp/offer.py +260 -0
  42. pyagorartc-0.2.0/pyagorartc/session/__init__.py +87 -0
  43. pyagorartc-0.2.0/pyagorartc/session/messages.py +570 -0
  44. pyagorartc-0.2.0/pyagorartc/session/recovery.py +166 -0
  45. pyagorartc-0.2.0/pyagorartc/session/session.py +696 -0
  46. pyagorartc-0.2.0/pyagorartc/session/transport.py +130 -0
  47. pyagorartc-0.2.0/pyproject.toml +140 -0
  48. pyagorartc-0.2.0/tests/__init__.py +1 -0
  49. pyagorartc-0.2.0/tests/_helpers.py +63 -0
  50. pyagorartc-0.2.0/tests/conftest.py +31 -0
  51. pyagorartc-0.2.0/tests/fakegateway/__init__.py +6 -0
  52. pyagorartc-0.2.0/tests/fakegateway/__main__.py +20 -0
  53. pyagorartc-0.2.0/tests/fakegateway/_common.py +93 -0
  54. pyagorartc-0.2.0/tests/fakegateway/ap.py +155 -0
  55. pyagorartc-0.2.0/tests/fakegateway/gateway.py +360 -0
  56. pyagorartc-0.2.0/tests/fakegateway/rtm.py +81 -0
  57. pyagorartc-0.2.0/tests/fakegateway/scheduler.py +102 -0
  58. pyagorartc-0.2.0/tests/fakegateway/server.py +165 -0
  59. pyagorartc-0.2.0/tests/fakegateway/state.py +222 -0
  60. pyagorartc-0.2.0/tests/fixtures/README.md +61 -0
  61. pyagorartc-0.2.0/tests/fixtures/ap/choose_server_all_failed.json +43 -0
  62. pyagorartc-0.2.0/tests/fixtures/ap/choose_server_gateway_failed.json +56 -0
  63. pyagorartc-0.2.0/tests/fixtures/ap/choose_server_irregular.json +44 -0
  64. pyagorartc-0.2.0/tests/fixtures/ap/choose_server_one_failed.json +56 -0
  65. pyagorartc-0.2.0/tests/fixtures/ap/choose_server_request_expected.json +1 -0
  66. pyagorartc-0.2.0/tests/fixtures/ap/choose_server_response.json +51 -0
  67. pyagorartc-0.2.0/tests/fixtures/ap/update_ticket_response.json +31 -0
  68. pyagorartc-0.2.0/tests/fixtures/gateway/error.json +6 -0
  69. pyagorartc-0.2.0/tests/fixtures/gateway/join_failed.json +8 -0
  70. pyagorartc-0.2.0/tests/fixtures/gateway/join_failed_code_only.json +7 -0
  71. pyagorartc-0.2.0/tests/fixtures/gateway/join_ok.json +116 -0
  72. pyagorartc-0.2.0/tests/fixtures/gateway/join_ok_no_ortc.json +10 -0
  73. pyagorartc-0.2.0/tests/fixtures/gateway/join_ok_rtx.json +109 -0
  74. pyagorartc-0.2.0/tests/fixtures/gateway/join_v3_expected.json +124 -0
  75. pyagorartc-0.2.0/tests/fixtures/gateway/join_v3_inputs.json +79 -0
  76. pyagorartc-0.2.0/tests/fixtures/gateway/leave_expected.json +4 -0
  77. pyagorartc-0.2.0/tests/fixtures/gateway/non_object_message.json +6 -0
  78. pyagorartc-0.2.0/tests/fixtures/gateway/on_add_video_stream.json +13 -0
  79. pyagorartc-0.2.0/tests/fixtures/gateway/on_add_video_stream_h265_pt0.json +13 -0
  80. pyagorartc-0.2.0/tests/fixtures/gateway/on_add_video_stream_minimal.json +9 -0
  81. pyagorartc-0.2.0/tests/fixtures/gateway/on_add_video_stream_no_ssrc.json +7 -0
  82. pyagorartc-0.2.0/tests/fixtures/gateway/on_notification_quit.json +8 -0
  83. pyagorartc-0.2.0/tests/fixtures/gateway/on_notification_warn.json +7 -0
  84. pyagorartc-0.2.0/tests/fixtures/gateway/on_p2p_lost.json +7 -0
  85. pyagorartc-0.2.0/tests/fixtures/gateway/on_p2p_lost_top_level.json +9 -0
  86. pyagorartc-0.2.0/tests/fixtures/gateway/on_p2p_lost_top_level_only.json +5 -0
  87. pyagorartc-0.2.0/tests/fixtures/gateway/on_p2p_ok.json +7 -0
  88. pyagorartc-0.2.0/tests/fixtures/gateway/on_rtp_capability_change.json +11 -0
  89. pyagorartc-0.2.0/tests/fixtures/gateway/on_rtp_capability_change_irregular.json +12 -0
  90. pyagorartc-0.2.0/tests/fixtures/gateway/on_token_privilege_did_expire.json +4 -0
  91. pyagorartc-0.2.0/tests/fixtures/gateway/on_token_privilege_will_expire.json +4 -0
  92. pyagorartc-0.2.0/tests/fixtures/gateway/on_user_offline.json +7 -0
  93. pyagorartc-0.2.0/tests/fixtures/gateway/on_user_offline_no_uid.json +6 -0
  94. pyagorartc-0.2.0/tests/fixtures/gateway/on_user_online.json +6 -0
  95. pyagorartc-0.2.0/tests/fixtures/gateway/ping_back.json +5 -0
  96. pyagorartc-0.2.0/tests/fixtures/gateway/ping_expected.json +4 -0
  97. pyagorartc-0.2.0/tests/fixtures/gateway/renew_token_expected.json +7 -0
  98. pyagorartc-0.2.0/tests/fixtures/gateway/set_client_role_expected.json +9 -0
  99. pyagorartc-0.2.0/tests/fixtures/gateway/subscribe_expected.json +15 -0
  100. pyagorartc-0.2.0/tests/fixtures/gateway/unknown_event.json +11 -0
  101. pyagorartc-0.2.0/tests/fixtures/gateway/unsubscribe_expected.json +9 -0
  102. pyagorartc-0.2.0/tests/fixtures/rtm/ack_delivered.json +1 -0
  103. pyagorartc-0.2.0/tests/fixtures/rtm/ack_failed.json +1 -0
  104. pyagorartc-0.2.0/tests/fixtures/rtm/ack_failed_sent.json +1 -0
  105. pyagorartc-0.2.0/tests/fixtures/rtm/ack_mixed_case.json +1 -0
  106. pyagorartc-0.2.0/tests/fixtures/rtm/ack_offline.json +1 -0
  107. pyagorartc-0.2.0/tests/fixtures/rtm/ack_sent.json +1 -0
  108. pyagorartc-0.2.0/tests/fixtures/rtm/peer_message_request_expected.json +17 -0
  109. pyagorartc-0.2.0/tests/fixtures/sdp/chrome_answer_shipped.sdp +46 -0
  110. pyagorartc-0.2.0/tests/fixtures/sdp/chrome_offer_ortc_shipped.json +308 -0
  111. pyagorartc-0.2.0/tests/fixtures/sdp/chrome_recvonly_offer.sdp +70 -0
  112. pyagorartc-0.2.0/tests/fixtures/sdp/gateway_join_ortc.json +22 -0
  113. pyagorartc-0.2.0/tests/fixtures/sdp/gateway_ortc_vp8.json +38 -0
  114. pyagorartc-0.2.0/tests/fixtures/sdp/go2rtc_offer.sdp +45 -0
  115. pyagorartc-0.2.0/tests/fixtures/sdp/whep_trickle_fragment.sdpfrag +13 -0
  116. pyagorartc-0.2.0/tests/integration/__init__.py +1 -0
  117. pyagorartc-0.2.0/tests/integration/_helpers.py +285 -0
  118. pyagorartc-0.2.0/tests/integration/conftest.py +86 -0
  119. pyagorartc-0.2.0/tests/integration/test_ap_client.py +266 -0
  120. pyagorartc-0.2.0/tests/integration/test_fakegateway_contract_ap.py +188 -0
  121. pyagorartc-0.2.0/tests/integration/test_fakegateway_contract_control.py +314 -0
  122. pyagorartc-0.2.0/tests/integration/test_fakegateway_contract_gateway_events.py +218 -0
  123. pyagorartc-0.2.0/tests/integration/test_fakegateway_contract_gateway_join.py +220 -0
  124. pyagorartc-0.2.0/tests/integration/test_fakegateway_contract_rtm.py +122 -0
  125. pyagorartc-0.2.0/tests/integration/test_rtm_client.py +195 -0
  126. pyagorartc-0.2.0/tests/integration/test_session_endings.py +120 -0
  127. pyagorartc-0.2.0/tests/integration/test_session_join.py +167 -0
  128. pyagorartc-0.2.0/tests/integration/test_session_secrets.py +69 -0
  129. pyagorartc-0.2.0/tests/integration/test_session_streams.py +130 -0
  130. pyagorartc-0.2.0/tests/integration/test_session_timers.py +124 -0
  131. pyagorartc-0.2.0/tests/integration/test_transport.py +44 -0
  132. pyagorartc-0.2.0/tests/meta/__init__.py +1 -0
  133. pyagorartc-0.2.0/tests/meta/_helpers.py +232 -0
  134. pyagorartc-0.2.0/tests/meta/test_conventions.py +844 -0
  135. pyagorartc-0.2.0/tests/meta/test_guards.py +23 -0
  136. pyagorartc-0.2.0/tests/regression/__init__.py +1 -0
  137. pyagorartc-0.2.0/tests/unit/__init__.py +1 -0
  138. pyagorartc-0.2.0/tests/unit/_fakes.py +246 -0
  139. pyagorartc-0.2.0/tests/unit/ap/__init__.py +1 -0
  140. pyagorartc-0.2.0/tests/unit/ap/test_client.py +369 -0
  141. pyagorartc-0.2.0/tests/unit/ap/test_password.py +22 -0
  142. pyagorartc-0.2.0/tests/unit/ap/test_response.py +427 -0
  143. pyagorartc-0.2.0/tests/unit/conftest.py +28 -0
  144. pyagorartc-0.2.0/tests/unit/rtm/__init__.py +1 -0
  145. pyagorartc-0.2.0/tests/unit/rtm/test_client.py +361 -0
  146. pyagorartc-0.2.0/tests/unit/sdp/__init__.py +1 -0
  147. pyagorartc-0.2.0/tests/unit/sdp/_helpers.py +58 -0
  148. pyagorartc-0.2.0/tests/unit/sdp/test_answer.py +594 -0
  149. pyagorartc-0.2.0/tests/unit/sdp/test_candidates.py +172 -0
  150. pyagorartc-0.2.0/tests/unit/sdp/test_offer.py +364 -0
  151. pyagorartc-0.2.0/tests/unit/session/__init__.py +1 -0
  152. pyagorartc-0.2.0/tests/unit/session/_helpers.py +182 -0
  153. pyagorartc-0.2.0/tests/unit/session/test_messages.py +514 -0
  154. pyagorartc-0.2.0/tests/unit/session/test_recovery.py +249 -0
  155. pyagorartc-0.2.0/tests/unit/session/test_session.py +491 -0
  156. pyagorartc-0.2.0/tests/unit/session/test_session_recovery.py +176 -0
  157. pyagorartc-0.2.0/tests/unit/session/test_session_streams.py +325 -0
  158. pyagorartc-0.2.0/tests/unit/session/test_session_teardown.py +76 -0
  159. pyagorartc-0.2.0/tests/unit/session/test_session_timers.py +242 -0
  160. pyagorartc-0.2.0/tests/unit/session/test_transport.py +30 -0
  161. pyagorartc-0.2.0/tests/unit/test_models.py +197 -0
  162. pyagorartc-0.2.0/uv.lock +908 -0
@@ -0,0 +1,43 @@
1
+ ---
2
+ name: code-reviewer
3
+ description: Reviews a change in pyagorartc against CONSTITUTION.md, docs/architecture.md and docs/code_style.md. Reports findings by severity with file:line; does not rewrite. Launch over any non-trivial change before reporting it complete.
4
+ model: opus
5
+ tools: Read, Grep, Glob, Bash
6
+ ---
7
+
8
+ You review code in the `pyagorartc` repository. Read `CONSTITUTION.md`,
9
+ `docs/architecture.md` §1–§3 and `docs/code_style.md` before looking at the
10
+ change. The change is the working-tree diff unless the prompt names files.
11
+
12
+ Check, in this order:
13
+
14
+ 1. **Constitution violations** (blocking): layer direction, secrets in
15
+ logs/repr/exceptions, a non-success response returned instead of raised,
16
+ a retry timer or cooldown in auth, a second home for a concern listed in
17
+ architecture §3, host (Home Assistant) knowledge in the package.
18
+ 2. **Contract with the spec** (blocking): field names and types against
19
+ `docs/api/<group>.md` and `docs/openapi/mower.json`; required vs optional;
20
+ enum values verbatim; timestamps as `*_ms: int`.
21
+ 3. **Error mapping** (blocking): every status/envelope path maps to exactly
22
+ the exception in architecture §2.3; `TransportError` never mutates auth
23
+ state; `CredentialsRejectedError` is terminal and fails fast.
24
+ 4. **Style** (major/minor): comments that restate code, docstrings missing
25
+ on public names, `Any` in public signatures, magic numbers, imports inside
26
+ functions, dead code, naming.
27
+ 5. **Docs drift** (major): a change to behaviour without the matching edit in
28
+ `docs/`; a new choice without a `Dn`; a new unknown without a `Qn`.
29
+
30
+ Report as:
31
+
32
+ ```
33
+ BLOCKING
34
+ - path:line — finding. Why it matters. What the fix is (one line).
35
+ MAJOR
36
+ - ...
37
+ MINOR
38
+ - ...
39
+ OK — what is good and should stay.
40
+ ```
41
+
42
+ Do not edit files. Do not pad with praise. If there is nothing blocking, say so
43
+ in the first line.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: test-reviewer
3
+ description: Reviews tests in pyagorartc against docs/testing.md. Reports findings by severity with file:line; does not rewrite. Launch over every test file written or modified before reporting the work complete.
4
+ model: opus
5
+ tools: Read, Grep, Glob, Bash
6
+ ---
7
+
8
+ You review tests in the `pyagorartc` repository against `docs/testing.md`,
9
+ which is the testing constitution. Read it first. The files under review are
10
+ named in the prompt; otherwise review every test file in the working-tree
11
+ diff.
12
+
13
+ Check:
14
+
15
+ 1. **Tier and placement** (blocking): a unit test importing
16
+ `tests.fakeserver` or opening a socket; a test module that does not mirror
17
+ its source module (`tests/unit/<pkg>/test_<module>.py`); a regression test
18
+ without the marker, without a docstring saying what the code did wrong, or
19
+ that could not have been red before the fix.
20
+ 2. **Doubles** (blocking): any `MagicMock`/`Mock`/`patch` on a collaborator
21
+ that `tests/unit/_fakes.py` covers; mocking the unit under test; asserting
22
+ on call plumbing where an outcome was available.
23
+ 3. **Time and concurrency** (blocking): `asyncio.sleep(x)` with `x > 0`;
24
+ unbounded waits; reading the real clock; non-deterministic interleaving.
25
+ 4. **Secrets** (blocking): a real-looking credential or hostname; a test that
26
+ does not assert redaction where the code redacts.
27
+ 5. **Quality** (major/minor): a test that asserts nothing; several behaviours
28
+ in one test; names that do not read as a sentence; builders duplicated
29
+ instead of taken from `_helpers.py` / `_fakes.py`; fixtures with side
30
+ effects; missing negative cases for every documented exception.
31
+ 6. **Coverage of the contract** (major): for each public method in the
32
+ source module, is there a test for the success path, each documented
33
+ exception, and the tolerant-decoding rule (unknown field, unknown enum)?
34
+
35
+ Run `uv run pytest <files> -q` and report the result. Report as:
36
+
37
+ ```
38
+ RESULT: <pytest summary line>
39
+ BLOCKING
40
+ - path:line — finding, why, fix.
41
+ MAJOR
42
+ - ...
43
+ MINOR
44
+ - ...
45
+ OK — what is good.
46
+ ```
47
+
48
+ Do not edit files. The author fixes.
@@ -0,0 +1,20 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ checks:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ with:
15
+ enable-cache: true
16
+ - run: uv sync --frozen
17
+ - run: uv run ruff check .
18
+ - run: uv run ruff format --check .
19
+ - run: uv run ty check pyagorartc/
20
+ - run: uv run pytest --cov --cov-report=term-missing
@@ -0,0 +1,73 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ checks:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: astral-sh/setup-uv@v5
14
+ with:
15
+ enable-cache: true
16
+ - run: uv sync --frozen
17
+ - run: uv run ruff check .
18
+ - run: uv run ruff format --check .
19
+ - run: uv run ty check pyagorartc/
20
+ - run: uv run pytest
21
+
22
+ publish:
23
+ needs: checks
24
+ runs-on: ubuntu-latest
25
+ permissions:
26
+ id-token: write # trusted publishing to PyPI
27
+ contents: write # GitHub release
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - uses: astral-sh/setup-uv@v5
31
+ with:
32
+ enable-cache: true
33
+ - run: uv sync --frozen
34
+
35
+ # The tag and the package version are compared after PEP 440 normalisation,
36
+ # so v0.2.0-beta1, v0.2.0.b1 and v0.2.0b1 all match a 0.2.0b1 package.
37
+ - name: Check package version matches tag
38
+ id: version
39
+ run: |
40
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
41
+ uv run python - "$TAG_VERSION" <<'PY'
42
+ import importlib.metadata, os, sys
43
+ from packaging.version import InvalidVersion, Version
44
+
45
+ package_version = Version(importlib.metadata.version("pyagorartc"))
46
+ try:
47
+ tag_version = Version(sys.argv[1])
48
+ except InvalidVersion:
49
+ sys.exit(f"Tag version ({sys.argv[1]}) is not a valid PEP 440 version")
50
+ if package_version != tag_version:
51
+ sys.exit(f"Package version ({package_version}) does not match tag version ({tag_version})")
52
+
53
+ with open(os.environ["GITHUB_OUTPUT"], "a") as fh:
54
+ fh.write(f"prerelease={str(package_version.is_prerelease).lower()}\n")
55
+ fh.write(f"version={package_version}\n")
56
+ PY
57
+
58
+ - run: uv build
59
+
60
+ - name: Publish package distributions to PyPI
61
+ uses: pypa/gh-action-pypi-publish@release/v1
62
+ with:
63
+ verbose: true
64
+ print-hash: true
65
+
66
+ - name: Create GitHub Release
67
+ uses: softprops/action-gh-release@v2
68
+ with:
69
+ files: dist/*
70
+ generate_release_notes: true
71
+ prerelease: ${{ steps.version.outputs.prerelease }}
72
+ env:
73
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,207 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
@@ -0,0 +1,21 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.15.7
4
+ hooks:
5
+ - id: ruff
6
+ args: [--fix]
7
+ - id: ruff-format
8
+ - repo: local
9
+ hooks:
10
+ - id: ty
11
+ name: ty
12
+ entry: uv run ty check pyagorartc/
13
+ language: system
14
+ pass_filenames: false
15
+ types: [python]
16
+ - id: unit-tests
17
+ name: unit tests
18
+ entry: uv run pytest tests/unit tests/meta -q
19
+ language: system
20
+ pass_filenames: false
21
+ types: [python]
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,56 @@
1
+ # CLAUDE.md
2
+
3
+ Guidance for agents working in this repository. Read `CONSTITUTION.md` first;
4
+ it is short and it is binding.
5
+
6
+ ## What this is
7
+
8
+ `pyagorartc`: an async Python client for Agora RTC signalling, reverse-engineered
9
+ from Agora's Web SDK and hardened by two Home Assistant integrations
10
+ (Mammotion mowers, PetKit cameras). It is host-agnostic: no Home Assistant, no
11
+ vendor library, no vendor behaviour except through documented extension
12
+ points.
13
+
14
+ ## Commands
15
+
16
+ ```bash
17
+ uv sync
18
+ uv run ruff check --fix . && uv run ruff format .
19
+ uv run ty check pyagorartc/
20
+ uv run pytest # all tiers except live
21
+ uv run pytest tests/unit # fast tier
22
+ uv run pre-commit run --all-files
23
+ ```
24
+
25
+ ## Where things are
26
+
27
+ | Need | Read |
28
+ |---|---|
29
+ | the rules | `CONSTITUTION.md` |
30
+ | the map (layers, session lifecycle, single homes, recipes) | `docs/architecture.md` |
31
+ | the protocol as we know it | `docs/protocol.md`, `tests/fixtures/` |
32
+ | how to write code / tests | `docs/code_style.md`, `docs/testing.md` |
33
+ | why something is the way it is | `docs/decisions.md` (cite as D7) |
34
+ | what we do not know | `docs/open_questions.md` (cite as Q2) |
35
+ | how a host adopts the library | `docs/migration.md` |
36
+ | what is left to do | `docs/backlog.md` |
37
+
38
+ ## Rules of work
39
+
40
+ - **Audit before adding.** Every concern has a single home
41
+ (`architecture.md` §3). Extend the existing site.
42
+ - **Traceability.** A change to what goes on the wire cites its source
43
+ (fixture, SDK behaviour, or a regression test that was red).
44
+ - **No host or vendor imports** anywhere under `pyagorartc/`. The meta tests
45
+ enforce it.
46
+ - **Tests before merge, in the right tier.** Regression tests are seen red
47
+ first and marked `regression`. Launch `test-reviewer` over any test file you
48
+ touched and fix its blocking findings before reporting done.
49
+ - **Reviews.** Non-trivial changes get `code-reviewer` before they are
50
+ reported complete. The author fixes; the reviewer does not rewrite.
51
+ - **Decisions are written down.** New choice → `Dn`; new unknown → `Qn`.
52
+ - **No secrets anywhere** in logs, reprs, exceptions, fixtures (redact), tests
53
+ or commit messages.
54
+ - **Comments say why, in one or two lines, or do not exist.**
55
+ - **Commits**: imperative subject, one change per commit, no attribution
56
+ trailers.
@@ -0,0 +1,89 @@
1
+ # Constitution
2
+
3
+ The rules that do not bend. `docs/` explains how to apply them; this file says
4
+ what they are. Breaking one needs a new numbered entry in `docs/decisions.md`
5
+ that supersedes the rule, not a quiet exception.
6
+
7
+ ## 1. Protocol fidelity is traceable
8
+
9
+ `pyagorartc` speaks a signalling protocol that Agora has not published for
10
+ Python. Every behaviour on the wire traces to one of: a recorded exchange
11
+ (`tests/fixtures/`), the documented behaviour of Agora's Web SDK, or a fix
12
+ that carries a regression test. A behaviour that traces to nothing is a guess,
13
+ and a guess is written down in `docs/open_questions.md` before it ships. The
14
+ hard-won fixes (MID extension stripping, DTLS role, no `set_client_role` after
15
+ join, msid stream id, peer recovery) are pinned by tests, not by comments.
16
+
17
+ ## 2. One library, many vendors, no host
18
+
19
+ Mammotion mowers and PetKit cameras both publish through Agora; other vendors
20
+ will. The library knows channels, tokens, edges, SDP and gateway messages. It
21
+ does not know mowers, feeders, `pymammotion`, `pypetkitapi`, or Home
22
+ Assistant. Vendor-specific behaviour (a keep-alive the device needs, a way to
23
+ re-request the stream) enters through callbacks and options on the session,
24
+ never through an import. If a vendor needs something the session cannot
25
+ express, the session grows a documented extension point.
26
+
27
+ ## 3. Layers point one way
28
+
29
+ ```
30
+ sdp ← ap ← session ← (host) rtm ← (host)
31
+ ```
32
+
33
+ `sdp/` is pure: text and dicts in, text and dicts out, no I/O (it may read
34
+ `const` values). `ap/` talks
35
+ HTTP to Agora's access points. `session/` drives one WebSocket gateway
36
+ session and composes the two. `rtm/` is independent of all three. Nothing
37
+ imports upward. The public surface is
38
+ `pyagorartc.__all__`.
39
+
40
+ ## 4. All I/O is asynchronous and owned
41
+
42
+ Every network call is `async`. Background work (ping, keep-alive, peer
43
+ recovery, restart) runs in tasks the session owns and cancels when it ends
44
+ (`close()` or any other ending); nothing is fire-and-forget. A host may supply its own aiohttp
45
+ session; the library never closes one it did not create.
46
+
47
+ ## 5. Errors are typed and scoped
48
+
49
+ A failed edge discovery, a rejected join, a lost gateway connection and a
50
+ malformed message are different exceptions with different recoveries, and
51
+ they say which without a string match. Transient failures never change
52
+ credential or session state. The session reports its lifecycle through one
53
+ state enum and one event callback, not through log lines.
54
+
55
+ ## 6. Secrets stay secret
56
+
57
+ Tokens, tickets, credentials, TURN passwords and channel encryption keys never
58
+ appear in log lines, `repr`, or exception messages. Logs may carry a
59
+ fingerprint (short hash) and nothing more.
60
+
61
+ ## 7. Wire messages are tolerant, fixtures are exact
62
+
63
+ An unknown message type, an extra field or a missing optional field never
64
+ crashes the session; it is logged once at DEBUG and ignored. Fixtures in
65
+ `tests/fixtures/` are byte-exact recordings (redacted) and tests assert
66
+ against them exactly; when the wire changes, the fixture changes with a
67
+ commit that says why.
68
+
69
+ ## 8. Tests are part of the deliverable
70
+
71
+ Nothing merges without tests in the right tier (`docs/testing.md`). Pure
72
+ code (SDP, ORTC, payload building, ICE selection) is tested directly.
73
+ The session is tested against an in-process fake gateway that speaks the
74
+ recorded protocol. Doubles are real objects or hand-written fakes; a bare
75
+ `MagicMock` is not a test double.
76
+
77
+ ## 9. Documentation lives outside the code
78
+
79
+ Design is in `docs/`. A comment states a constraint the code cannot express,
80
+ in one or two lines. Every non-obvious choice is a numbered decision; every
81
+ unknown is a numbered open question; every intended change is a backlog
82
+ entry.
83
+
84
+ ## 10. The public surface is deliberate
85
+
86
+ `pyagorartc.__all__` lists the supported API. Anything else may change without
87
+ notice. Semver; a breaking change to a listed name bumps the major version.
88
+ Both known hosts (Mammotion, PetKit) have a written migration path in
89
+ `docs/migration.md` before a release that changes the surface.
@@ -0,0 +1,23 @@
1
+ # Contributing
2
+
3
+ 1. Read `CONSTITUTION.md`. It is one page and it is not negotiable.
4
+ 2. Find the single home for your concern in `docs/architecture.md` §3 before
5
+ writing a line.
6
+ 3. New endpoint? Follow the recipe in `docs/architecture.md` §4: model, API
7
+ method, fake-server route, tests, `docs/api/` section.
8
+ 4. New non-obvious choice? Add a numbered entry to `docs/decisions.md`. New
9
+ unknown about the API? Add a `Qn` to `docs/open_questions.md`.
10
+ 5. Tests in the right tier (`docs/testing.md`), fakes from `tests/unit/_fakes.py`,
11
+ no `MagicMock`, no sleeping for synchronisation.
12
+ 6. `uv run pre-commit run --all-files` is green: ruff, ruff-format, ty, unit
13
+ tests.
14
+ 7. Commits: imperative subject line, one change per commit, no attribution
15
+ trailers.
16
+
17
+ ## Pull request checklist
18
+
19
+ - [ ] The change touches only the places the recipe names.
20
+ - [ ] Every public method has a docstring that says what the signature does not.
21
+ - [ ] No secret can reach a log, a `repr`, or an exception message.
22
+ - [ ] Tests assert behaviour, not call plumbing.
23
+ - [ ] `docs/` updated where the change alters the map, the API, or the rules.