hotcodepush 0.2.0__tar.gz → 0.2.1__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 (121) hide show
  1. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/PKG-INFO +11 -23
  2. hotcodepush-0.2.1/README.md +55 -0
  3. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/_http.py +8 -45
  4. hotcodepush-0.2.1/src/hotcodepush/_types.py +12 -0
  5. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/_upload.py +4 -13
  6. hotcodepush-0.2.1/src/hotcodepush/_version.py +1 -0
  7. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/client.py +9 -13
  8. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/exceptions.py +3 -6
  9. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/models.py +105 -82
  10. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/_base.py +0 -2
  11. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/apps.py +6 -5
  12. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/audit_logs.py +6 -4
  13. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/binaries.py +5 -6
  14. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/bundle_deltas.py +5 -3
  15. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/bundle_files.py +4 -4
  16. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/bundle_pack.py +3 -3
  17. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/bundle_pack_upload_parts.py +3 -3
  18. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/bundle_pack_uploads.py +9 -9
  19. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/bundles.py +12 -9
  20. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/channel_audience.py +3 -3
  21. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/channel_qr.py +4 -2
  22. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/channel_releases.py +9 -7
  23. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/channels.py +22 -14
  24. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/checkouts.py +7 -6
  25. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/customer_portal_sessions.py +6 -4
  26. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/device_events.py +4 -3
  27. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/devices.py +9 -5
  28. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/file_upload_parts.py +3 -3
  29. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/file_uploads.py +8 -9
  30. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/files.py +6 -4
  31. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/health.py +4 -2
  32. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/invitations.py +6 -5
  33. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/limits.py +1 -1
  34. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/members.py +14 -9
  35. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/notification_preferences.py +11 -7
  36. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/notifications.py +7 -6
  37. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/organization_apps.py +6 -3
  38. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/organization_invitations.py +11 -7
  39. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/organizations.py +11 -7
  40. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/release_audience.py +3 -2
  41. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/releases.py +18 -14
  42. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/rollbacks.py +3 -2
  43. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/signing_keys.py +5 -5
  44. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/sso_provider.py +12 -7
  45. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/sso_provider_verifications.py +6 -5
  46. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/statistics_fleet.py +4 -1
  47. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/statistics_updates.py +4 -3
  48. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/statistics_usage.py +1 -1
  49. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/subscription.py +6 -6
  50. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/usage.py +14 -6
  51. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/user_password.py +3 -4
  52. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/user_sessions.py +3 -2
  53. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/user_tokens.py +3 -2
  54. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/users.py +8 -8
  55. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/webhook_deliveries.py +9 -7
  56. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/webhooks.py +20 -15
  57. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/conftest.py +1 -2
  58. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/openapi_document.py +1 -10
  59. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/stub_api.py +1 -11
  60. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_integration.py +2 -2
  61. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_openapi.py +2 -9
  62. hotcodepush-0.2.0/README.md +0 -67
  63. hotcodepush-0.2.0/src/hotcodepush/_types.py +0 -15
  64. hotcodepush-0.2.0/src/hotcodepush/_version.py +0 -1
  65. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/.gitignore +0 -0
  66. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/LICENSE +0 -0
  67. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/openapi.json +0 -0
  68. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/pyproject.toml +0 -0
  69. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/__init__.py +0 -0
  70. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/py.typed +0 -0
  71. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/__init__.py +0 -0
  72. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/src/hotcodepush/resources/statistics.py +0 -0
  73. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/__init__.py +0 -0
  74. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_apps.py +0 -0
  75. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_audit_logs.py +0 -0
  76. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_binaries.py +0 -0
  77. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_bundle_deltas.py +0 -0
  78. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_bundle_files.py +0 -0
  79. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_bundle_pack.py +0 -0
  80. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_bundle_pack_upload_parts.py +0 -0
  81. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_bundle_pack_uploads.py +0 -0
  82. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_bundles.py +0 -0
  83. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_channel_audience.py +0 -0
  84. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_channel_qr.py +0 -0
  85. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_channel_releases.py +0 -0
  86. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_channels.py +0 -0
  87. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_checkouts.py +0 -0
  88. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_customer_portal_sessions.py +0 -0
  89. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_device_events.py +0 -0
  90. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_devices.py +0 -0
  91. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_exceptions.py +0 -0
  92. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_file_upload_parts.py +0 -0
  93. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_file_uploads.py +0 -0
  94. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_files.py +0 -0
  95. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_health.py +0 -0
  96. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_http.py +0 -0
  97. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_invitations.py +0 -0
  98. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_limits.py +0 -0
  99. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_members.py +0 -0
  100. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_notification_preferences.py +0 -0
  101. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_notifications.py +0 -0
  102. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_organization_apps.py +0 -0
  103. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_organization_invitations.py +0 -0
  104. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_organizations.py +0 -0
  105. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_release_audience.py +0 -0
  106. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_releases.py +0 -0
  107. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_rollbacks.py +0 -0
  108. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_signing_keys.py +0 -0
  109. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_sso_provider.py +0 -0
  110. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_sso_provider_verifications.py +0 -0
  111. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_statistics_fleet.py +0 -0
  112. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_statistics_updates.py +0 -0
  113. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_statistics_usage.py +0 -0
  114. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_subscription.py +0 -0
  115. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_usage.py +0 -0
  116. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_user_password.py +0 -0
  117. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_user_sessions.py +0 -0
  118. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_user_tokens.py +0 -0
  119. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_users.py +0 -0
  120. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_webhook_deliveries.py +0 -0
  121. {hotcodepush-0.2.0 → hotcodepush-0.2.1}/tests/test_webhooks.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hotcodepush
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: The typed Python client for the HotCodePush REST API
5
5
  Project-URL: Homepage, https://hotcodepush.com
6
6
  Project-URL: Documentation, https://hotcodepush.com/docs/api/python
@@ -34,13 +34,7 @@ The typed Python client for the [HotCodePush](https://hotcodepush.com) REST API,
34
34
  ## Installation
35
35
 
36
36
  ```sh
37
- pip install hotcodepush
38
- ```
39
-
40
- Or with uv:
41
-
42
- ```sh
43
- uv add hotcodepush
37
+ pip install hotcodepush # or: uv add hotcodepush
44
38
  ```
45
39
 
46
40
  It requires Python 3.11 or later.
@@ -61,33 +55,27 @@ hot_code_push.apps.channels.pause(app_id=app.id, channel_id=channel.id)
61
55
  ```
62
56
 
63
57
  The resources mirror the API's paths, `/v1/apps/{appId}/channels` being `apps.channels`.
64
- Every method takes keyword arguments and returns pydantic models with snake_case fields.
65
- An optional argument you leave out stays out of the request; `None` sends `null`, which clears a field such as a channel's `expires_at`.
66
- `users.get(user_id="me")` answers the caller behind the token, as `me` addresses the caller wherever the API takes a `{userId}`.
67
-
68
- An upload takes `bytes` or a file object with its `content_length`, and streams it.
69
- Bytes above 64 MiB go up in parts, and a retry sends them again; a file object is read once, so a failed upload of one is not retried.
70
-
71
- Creating an organization, an invitation, an app, a channel, a release, a rollback, a bundle or a binary sends an `Idempotency-Key`, a UUID per call kept across the client's own retries; pass `idempotency_key` to reuse one when you retry the call yourself, and the API answers the first result again for 24 hours.
72
-
73
- A failed request raises a `HotCodePushError` with the API's `code`, `message` and `details` and the HTTP `status`.
58
+ Every method takes keyword arguments and returns pydantic models with snake_case fields; an argument left out stays out of the request, and `None` sends `null`.
59
+ Uploads take `bytes` or a file object with its `content_length` and stream it.
60
+ Creating calls send an `Idempotency-Key` and are retried; pass `idempotency_key` to reuse one across your own retries.
61
+ A failed request raises a `HotCodePushError` with the API's `code`, `message`, `details` and the HTTP `status`.
74
62
 
75
63
  ## Documentation
76
64
 
77
- The documentation lives at [hotcodepush.com/docs/api/python](https://hotcodepush.com/docs/api/python).
65
+ See [hotcodepush.com/docs/api/python](https://hotcodepush.com/docs/api/python).
78
66
 
79
67
  ## Development
80
68
 
81
69
  ```sh
82
70
  uv sync
83
- uv run ruff format --check .
84
- uv run ruff check .
85
- uv run mypy
86
71
  uv run pytest
72
+ uv run ruff check . && uv run mypy
73
+ uv run ruff format . # run before every commit
87
74
  uv build
88
75
  ```
89
76
 
90
- `uv run ruff format .` formats the code.
77
+ `uv run scripts/sync_openapi.py` fetches the API's OpenAPI document into `openapi.json`, which the contract test checks the client against.
78
+ `CLAUDE.md` holds the layout and the rules.
91
79
 
92
80
  ## License
93
81
 
@@ -0,0 +1,55 @@
1
+ # hotcodepush
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/hotcodepush)](https://pypi.org/project/hotcodepush/)
4
+
5
+ The typed Python client for the [HotCodePush](https://hotcodepush.com) REST API, the over-the-air (OTA) update platform for React Native, Expo, Capacitor and Cordova.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pip install hotcodepush # or: uv add hotcodepush
11
+ ```
12
+
13
+ It requires Python 3.11 or later.
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from hotcodepush import HotCodePush
19
+
20
+ hot_code_push = HotCodePush() # reads the API token from HOTCODEPUSH_TOKEN
21
+
22
+ organization, *_ = hot_code_push.organizations.list()
23
+ app = hot_code_push.organizations.apps.create(
24
+ framework="capacitor", name="Demo", organization_id=organization.id
25
+ )
26
+ channel = hot_code_push.apps.channels.create(app_id=app.id, name="staging")
27
+ hot_code_push.apps.channels.pause(app_id=app.id, channel_id=channel.id)
28
+ ```
29
+
30
+ The resources mirror the API's paths, `/v1/apps/{appId}/channels` being `apps.channels`.
31
+ Every method takes keyword arguments and returns pydantic models with snake_case fields; an argument left out stays out of the request, and `None` sends `null`.
32
+ Uploads take `bytes` or a file object with its `content_length` and stream it.
33
+ Creating calls send an `Idempotency-Key` and are retried; pass `idempotency_key` to reuse one across your own retries.
34
+ A failed request raises a `HotCodePushError` with the API's `code`, `message`, `details` and the HTTP `status`.
35
+
36
+ ## Documentation
37
+
38
+ See [hotcodepush.com/docs/api/python](https://hotcodepush.com/docs/api/python).
39
+
40
+ ## Development
41
+
42
+ ```sh
43
+ uv sync
44
+ uv run pytest
45
+ uv run ruff check . && uv run mypy
46
+ uv run ruff format . # run before every commit
47
+ uv build
48
+ ```
49
+
50
+ `uv run scripts/sync_openapi.py` fetches the API's OpenAPI document into `openapi.json`, which the contract test checks the client against.
51
+ `CLAUDE.md` holds the layout and the rules.
52
+
53
+ ## License
54
+
55
+ See [LICENSE](./LICENSE).
@@ -1,5 +1,3 @@
1
- """The one place that sends a request: the headers, the timeout, the retry, JSON in and out."""
2
-
3
1
  from __future__ import annotations
4
2
 
5
3
  import re
@@ -25,19 +23,19 @@ IDEMPOTENT_METHODS = frozenset({"DELETE", "GET", "PUT"})
25
23
  INITIAL_RETRY_DELAY_SECONDS = 0.5
26
24
  JSON_TIMEOUT_SECONDS = 60.0
27
25
  MAX_ATTEMPTS = 3
26
+ # The API counts its rate limits in sixty-second windows.
28
27
  MAX_RETRY_AFTER_SECONDS = 60.0
29
- """The longest wait a `Retry-After` is followed for: the API's rate limits count in sixty-second
30
- windows."""
31
28
  PATH_PARAMETER_PATTERN = re.compile(r"\{(\w+)\}")
32
29
  RETRY_AFTER_STATUSES = frozenset({429, 503})
30
+ # Enough for the single-upload limit at about 100 kB a second.
33
31
  UPLOAD_TIMEOUT_SECONDS = 600.0
34
- """Ten minutes per attempt: the single-upload limit at about a hundred kilobytes a second."""
35
32
 
36
33
  HttpMethod = Literal["DELETE", "GET", "PATCH", "POST", "PUT"]
37
34
  JsonBody = Mapping[str, object]
38
35
  Query = Mapping[str, bool | date | datetime | int | str | Sequence[str] | NotGiven]
39
36
  UploadBody = bytes | BinaryIO
40
- """Bytes, read again when a retry needs them, or a file object, read once and never retried."""
37
+ """The body of an upload: bytes, which a failed attempt sends again, or a file object, read once
38
+ and never retried."""
41
39
 
42
40
  T = TypeVar("T")
43
41
 
@@ -60,14 +58,10 @@ class HttpClient:
60
58
  self._json_timeout_seconds = JSON_TIMEOUT_SECONDS if timeout is None else timeout
61
59
 
62
60
  def close(self) -> None:
63
- """Closes the connection pool, unless the caller passed its own `http_client`."""
64
61
  if self._is_httpx_client_owned:
65
62
  self._httpx_client.close()
66
63
 
67
64
  def fetch_bytes(self, method: HttpMethod, path: str, *, query: Query | None = None) -> bytes:
68
- """A request answered with a binary body, such as a CSV download or a channel's QR image,
69
- read whole.
70
- """
71
65
  return self._fetch_response(method, path, query=query).content
72
66
 
73
67
  def fetch_creating_post(
@@ -78,12 +72,10 @@ class HttpClient:
78
72
  body: JsonBody,
79
73
  idempotency_key: str | NotGiven,
80
74
  ) -> T:
81
- """A creating `POST`, sent with an `Idempotency-Key`: the caller's, or one generated
82
- for this call and kept across its retries, which the API answers with the first result.
83
- """
84
75
  resolved_idempotency_key = (
85
76
  str(uuid4()) if isinstance(idempotency_key, NotGiven) else idempotency_key
86
77
  )
78
+ # The API answers a repeated key with the first result, so a retry is safe.
87
79
  return self.fetch_json(
88
80
  "POST",
89
81
  path,
@@ -104,12 +96,6 @@ class HttpClient:
104
96
  is_retryable: bool | None = None,
105
97
  query: Query | None = None,
106
98
  ) -> T:
107
- """A request answered with JSON, validated into `response_type`.
108
-
109
- `is_retryable` defaults to true for `DELETE`, `GET` and `PUT`, false for `PATCH` and
110
- `POST`; a `POST` sets it where the API makes a repeat safe, through a stored idempotency
111
- key or a transition that answers a repeat unchanged.
112
- """
113
99
  response = self._fetch_response(
114
100
  method, path, body=body, headers=headers, is_retryable=is_retryable, query=query
115
101
  )
@@ -124,9 +110,6 @@ class HttpClient:
124
110
  is_retryable: bool | None = None,
125
111
  query: Query | None = None,
126
112
  ) -> None:
127
- """A request whose status is the whole answer: the body, such as `/health`'s
128
- plain-text `ok`, is discarded. `is_retryable` defaults as `fetch_json`'s does.
129
- """
130
113
  self._fetch_response(method, path, body=body, is_retryable=is_retryable, query=query)
131
114
 
132
115
  def fetch_upload(
@@ -138,9 +121,6 @@ class HttpClient:
138
121
  content_length: int | None,
139
122
  content_type: str,
140
123
  ) -> T:
141
- """A binary `PUT`, streamed as it is read; a file object is read once, so only bytes
142
- are retried.
143
- """
144
124
  request = self._httpx_client.build_request(
145
125
  "PUT",
146
126
  self._base_url + path,
@@ -210,9 +190,6 @@ def resolve_headers(*, client: str, token: str | None, user_agent: str | None) -
210
190
 
211
191
 
212
192
  def resolve_json_body(body: JsonBody) -> dict[str, object]:
213
- """The body without its `NOT_GIVEN` values, models by their API names without their `None`
214
- fields, since no nested field of the API takes `null`, and timestamps in UTC.
215
- """
216
193
  return {
217
194
  name: resolve_json_value(value)
218
195
  for name, value in body.items()
@@ -222,6 +199,7 @@ def resolve_json_body(body: JsonBody) -> dict[str, object]:
222
199
 
223
200
  def resolve_json_value(value: object) -> object:
224
201
  if isinstance(value, BaseModel):
202
+ # No nested field of the API takes `null`.
225
203
  return value.model_dump(by_alias=True, exclude_none=True, mode="json")
226
204
  if isinstance(value, datetime):
227
205
  return resolve_utc_timestamp(value)
@@ -231,10 +209,6 @@ def resolve_json_value(value: object) -> object:
231
209
 
232
210
 
233
211
  def resolve_path(template: str, **parameters: int | str) -> str:
234
- """Fills a path template of the OpenAPI snapshot, `/v1/apps/{appId}`, with its encoded
235
- parameters.
236
- """
237
-
238
212
  def resolve_parameter(match: re.Match[str]) -> str:
239
213
  name = match.group(1)
240
214
  if name not in parameters:
@@ -245,15 +219,12 @@ def resolve_path(template: str, **parameters: int | str) -> str:
245
219
 
246
220
 
247
221
  def resolve_query_params(query: Query) -> tuple[tuple[str, str], ...]:
248
- """The query as name and value pairs: `NOT_GIVEN`, empty strings and empty lists are left
249
- out; `ids` and `relations` are the API's comma lists, and every other list repeats its
250
- parameter, `?binary=…&binary=…`, since its values may hold a comma.
251
- """
252
222
  params: list[tuple[str, str]] = []
253
223
  for name, value in query.items():
254
224
  if isinstance(value, NotGiven) or value == "" or value == []:
255
225
  continue
256
226
  if isinstance(value, list):
227
+ # Other lists repeat their parameter, since their values may hold a comma.
257
228
  values = [",".join(value)] if name in COMMA_LIST_QUERY_NAMES else value
258
229
  params.extend((name, item) for item in values)
259
230
  elif isinstance(value, bool):
@@ -266,10 +237,6 @@ def resolve_query_params(query: Query) -> tuple[tuple[str, str], ...]:
266
237
 
267
238
 
268
239
  def resolve_retry_after_seconds(response: httpx.Response) -> float | None:
269
- """The wait a 429 or a 503 asks for in `Retry-After`, delay seconds or an HTTP date, capped
270
- at `MAX_RETRY_AFTER_SECONDS`; None for any other status, a missing header or one that does
271
- not parse.
272
- """
273
240
  retry_after = response.headers.get("Retry-After", "").strip()
274
241
  if response.status_code not in RETRY_AFTER_STATUSES or not retry_after:
275
242
  return None
@@ -285,15 +252,11 @@ def resolve_retry_after_seconds(response: httpx.Response) -> float | None:
285
252
 
286
253
 
287
254
  def resolve_utc_timestamp(value: datetime) -> str:
288
- """ISO 8601 in UTC with a `Z`, the only offset the API accepts."""
255
+ # The API accepts only the `Z` offset.
289
256
  return value.astimezone(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
290
257
 
291
258
 
292
259
  def send_with_retry(send: Callable[[], httpx.Response]) -> httpx.Response:
293
- """Sends up to `MAX_ATTEMPTS` times on a transport error, 408, 429 and 5xx, backing off
294
- exponentially from `INITIAL_RETRY_DELAY_SECONDS`, or waiting a 429's or a 503's
295
- `Retry-After`; the last attempt's response or error is the call's.
296
- """
297
260
  attempt = 1
298
261
  while True:
299
262
  is_last_attempt = attempt == MAX_ATTEMPTS
@@ -0,0 +1,12 @@
1
+ class NotGiven:
2
+ """The type of `NOT_GIVEN`, the default of every optional argument.
3
+
4
+ An argument left at `NOT_GIVEN` stays out of the request. `None` sends JSON `null` where the
5
+ API takes it, for example to clear a channel's `expires_at`.
6
+ """
7
+
8
+ def __repr__(self) -> str:
9
+ return "NOT_GIVEN"
10
+
11
+
12
+ NOT_GIVEN = NotGiven()
@@ -1,5 +1,3 @@
1
- """The single-or-parts rule of an upload: bytes above the single-upload limit go up in parts."""
2
-
3
1
  from __future__ import annotations
4
2
 
5
3
  from collections.abc import Callable
@@ -10,17 +8,16 @@ from ._http import UploadBody
10
8
  from .models import MultipartUpload, UploadedPart
11
9
 
12
10
  PART_SIZE_BYTES = 10 * 1024 * 1024
13
- """Every part but the last is this size: at least five mebibytes and all of one size, the bucket's
14
- rule."""
11
+ """The size of every part but the last. The bucket needs parts of one size, at least 5 MiB."""
15
12
  SINGLE_UPLOAD_LIMIT_BYTES = 64 * 1024 * 1024
16
- """Bytes go up in one request up to this size and in parts above it, well under the request-body
17
- cap of the API's plan."""
13
+ """The largest bytes body sent in one request; a larger one goes up in parts. It stays well under
14
+ the API's request body limit."""
18
15
 
19
16
  T = TypeVar("T")
20
17
 
21
18
 
22
19
  def is_above_single_upload_limit(body: UploadBody) -> TypeGuard[bytes]:
23
- """A file object is read once and cannot be split, so only bytes go up in parts."""
20
+ # A file object is read once and cannot be split.
24
21
  return isinstance(body, bytes) and len(body) > SINGLE_UPLOAD_LIMIT_BYTES
25
22
 
26
23
 
@@ -32,12 +29,6 @@ def upload_in_parts(
32
29
  delete: Callable[[str], None],
33
30
  upload_part: Callable[[str, int, bytes], UploadedPart],
34
31
  ) -> T:
35
- """Uploads bytes in parts of `PART_SIZE_BYTES`, numbered from 1, then completes the upload;
36
- a failed part or completion deletes the upload and re-raises.
37
-
38
- Each callable is one call of the multipart upload the files and the pack share, bound to the
39
- path parameters of its target.
40
- """
41
32
  upload_id = create().upload_id
42
33
  try:
43
34
  uploaded_parts = [
@@ -0,0 +1 @@
1
+ __version__ = "0.2.1" # x-release-please-version
@@ -1,5 +1,3 @@
1
- """The client for the HotCodePush REST API."""
2
-
3
1
  from __future__ import annotations
4
2
 
5
3
  import os
@@ -23,20 +21,18 @@ class HotCodePush:
23
21
  """The client for the HotCodePush REST API.
24
22
 
25
23
  Resources mirror the paths: `/v1/organizations/{organizationId}/apps` is
26
- `organizations.apps`, `/v1/apps/{appId}/channels` is `apps.channels`.
24
+ `organizations.apps`, and `/v1/apps/{appId}/channels` is `apps.channels`.
27
25
 
28
26
  Args:
29
- token: The API token, sent as `Authorization: Bearer <token>`; falls back to the
30
- `HOTCODEPUSH_TOKEN` environment variable and stays optional, since `/health` needs
31
- none.
27
+ token: The API token, sent as `Authorization: Bearer <token>`. Falls back to the
28
+ `HOTCODEPUSH_TOKEN` environment variable. Optional, since `/health` needs none.
32
29
  base_url: The base URL of the API.
33
30
  client: The `X-HotCodePush-Client` header, `<name>/<version>`.
34
- http_client: The `httpx.Client` that sends the requests in place of the client's own;
35
- `close` leaves it open.
36
- timeout: The seconds every phase of a JSON call's attempt may take, httpx's connect,
37
- read, write and pool timeouts; sixty when unset. An upload attempt gets ten minutes.
38
- user_agent: The `User-Agent` header, sent only when set, since a session keeps it and
39
- the sessions page names the client by it.
31
+ http_client: An `httpx.Client` to send the requests with. `close` leaves it open.
32
+ timeout: The seconds each phase of a JSON request attempt may take: connect, read,
33
+ write and pool. Sixty when unset. An upload attempt always gets ten minutes.
34
+ user_agent: The `User-Agent` header, sent only when set. A session stores it, and the
35
+ sessions page names the client by it.
40
36
  """
41
37
 
42
38
  def __init__(
@@ -66,7 +62,7 @@ class HotCodePush:
66
62
  self.users = UsersResource(self._http_client)
67
63
 
68
64
  def close(self) -> None:
69
- """Closes the connection pool, unless the requests go through the caller's `http_client`."""
65
+ """Closes the connection pool, unless the caller passed its own `http_client`."""
70
66
  self._http_client.close()
71
67
 
72
68
  def __enter__(self) -> HotCodePush:
@@ -1,5 +1,3 @@
1
- """The error raised for an API answer outside 2xx."""
2
-
3
1
  from __future__ import annotations
4
2
 
5
3
  import json
@@ -8,9 +6,9 @@ import json
8
6
  class HotCodePushError(Exception):
9
7
  """Raised when the API answers with a status outside 2xx.
10
8
 
11
- `code`, `message` and `details` are the API's, verbatim, from its one error shape;
12
- a response outside that shape, such as a page of the platform in front of the API,
13
- is `unexpected_response` with the status, its body left out.
9
+ `code`, `message` and `details` come verbatim from the API's error body. A response without
10
+ that shape, such as a page from a proxy in front of the API, raises `unexpected_response` with
11
+ the status and without the body.
14
12
  """
15
13
 
16
14
  def __init__(self, *, code: str, details: object, message: str, status: int) -> None:
@@ -37,7 +35,6 @@ def resolve_hot_code_push_error(status: int, text: str) -> HotCodePushError:
37
35
 
38
36
 
39
37
  def resolve_json_object(text: str) -> dict[str, object]:
40
- """The JSON object the text holds; empty when it holds none, such as an HTML page."""
41
38
  try:
42
39
  body: object = json.loads(text)
43
40
  except ValueError: