pycloudedge 0.1.6.dev2__tar.gz → 0.1.6.dev4__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.
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/PKG-INFO +1 -1
- pycloudedge-0.1.6.dev4/cloudedge/_version.py +24 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/client.py +24 -22
- pycloudedge-0.1.6.dev4/cloudedge/constants.py +35 -0
- pycloudedge-0.1.6.dev4/docs/plans/2026-03-30-streaming-video.md +176 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/pycloudedge.egg-info/PKG-INFO +1 -1
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/pycloudedge.egg-info/SOURCES.txt +2 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/pyproject.toml +2 -1
- pycloudedge-0.1.6.dev4/test_3region_login.py +58 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/tests/test_basic.py +47 -46
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/tests/test_improvements.py +9 -0
- pycloudedge-0.1.6.dev2/cloudedge/_version.py +0 -34
- pycloudedge-0.1.6.dev2/cloudedge/constants.py +0 -101
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/.env.example +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/.gitignore +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/LICENSE +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/MANIFEST.in +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/README.md +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/__init__.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/cli.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/exceptions.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/image_decrypt.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/iot_parameters.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/logging_config.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/mqtt.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/utils.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/cloudedge/validators.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/examples/README.md +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/examples/basic_example.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/examples/device_control.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/examples/network_ping_status.py +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/pycloudedge.egg-info/dependency_links.txt +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/pycloudedge.egg-info/entry_points.txt +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/pycloudedge.egg-info/requires.txt +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/pycloudedge.egg-info/top_level.txt +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/requirements-dev.txt +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/requirements.txt +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/setup.cfg +0 -0
- {pycloudedge-0.1.6.dev2 → pycloudedge-0.1.6.dev4}/setup.py +0 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.6.dev4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 6, 'dev4')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'ge76f5b0fa'
|
|
@@ -37,9 +37,7 @@ from .iot_parameters import (
|
|
|
37
37
|
)
|
|
38
38
|
from .constants import (
|
|
39
39
|
CA_KEY, CA_SECRET, DEFAULT_HEADERS, DEFAULT_TIMEOUT,
|
|
40
|
-
REGION_ENDPOINTS, REGION_EU, REGION_US, REGION_AP,
|
|
41
40
|
REDIRECT_URL,
|
|
42
|
-
region_for_country,
|
|
43
41
|
)
|
|
44
42
|
from .validators import validate_email, validate_country_code, validate_phone_code
|
|
45
43
|
from .logging_config import get_logger
|
|
@@ -132,7 +130,6 @@ class CloudEdgeClient:
|
|
|
132
130
|
session_cache_file: str = ".cloudedge_session_cache",
|
|
133
131
|
enable_network_ping: bool = True,
|
|
134
132
|
ping_timeout: float = 2.0,
|
|
135
|
-
region: Optional[str] = None,
|
|
136
133
|
):
|
|
137
134
|
"""
|
|
138
135
|
Initialize CloudEdge API client.
|
|
@@ -146,10 +143,6 @@ class CloudEdgeClient:
|
|
|
146
143
|
session_cache_file (str): Path to session cache file
|
|
147
144
|
enable_network_ping (bool): Enable ping-based online status when on same network
|
|
148
145
|
ping_timeout (float): Ping timeout in seconds
|
|
149
|
-
region (str, optional): Force a specific region ("eu", "us", or "ap").
|
|
150
|
-
When *None* the region is derived automatically from *country_code*:
|
|
151
|
-
European countries → EU, Asia-Pacific/Oceania/ME/Africa → AP
|
|
152
|
-
(Singapore), everything else → US.
|
|
153
146
|
|
|
154
147
|
Raises:
|
|
155
148
|
ValidationError: If input validation fails
|
|
@@ -173,17 +166,18 @@ class CloudEdgeClient:
|
|
|
173
166
|
details={"field": "phone_code", "value": phone_code}
|
|
174
167
|
)
|
|
175
168
|
|
|
169
|
+
normalized_country_code = country_code.upper()
|
|
170
|
+
if normalized_country_code == "UK":
|
|
171
|
+
normalized_country_code = "GB"
|
|
172
|
+
|
|
176
173
|
self.username = username
|
|
177
174
|
self.password = password
|
|
178
|
-
self.country_code =
|
|
175
|
+
self.country_code = normalized_country_code
|
|
179
176
|
self.phone_code = phone_code if phone_code.startswith('+') else f'+{phone_code}'
|
|
180
177
|
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
self.region = resolved_region
|
|
185
|
-
self.BASE_URL = endpoints["base_url"]
|
|
186
|
-
self.OPENAPI_BASE_URL = endpoints["openapi_base_url"]
|
|
178
|
+
# Endpoints are discovered dynamically via _discover_endpoints()
|
|
179
|
+
self.BASE_URL: Optional[str] = None
|
|
180
|
+
self.OPENAPI_BASE_URL: Optional[str] = None
|
|
187
181
|
|
|
188
182
|
# Setup proper logging
|
|
189
183
|
self.logger = get_logger("client")
|
|
@@ -557,8 +551,11 @@ class CloudEdgeClient:
|
|
|
557
551
|
contains the regional ``apiServer`` and ``openapi`` domain that must be
|
|
558
552
|
used for the given account/country.
|
|
559
553
|
|
|
560
|
-
On success, ``self.BASE_URL`` and ``self.OPENAPI_BASE_URL`` are
|
|
561
|
-
|
|
554
|
+
On success, ``self.BASE_URL`` and ``self.OPENAPI_BASE_URL`` are set.
|
|
555
|
+
|
|
556
|
+
Raises:
|
|
557
|
+
NetworkError: If the redirect API call fails or returns an
|
|
558
|
+
unexpected response and no endpoints were previously set.
|
|
562
559
|
"""
|
|
563
560
|
import random
|
|
564
561
|
|
|
@@ -593,9 +590,6 @@ class CloudEdgeClient:
|
|
|
593
590
|
).hexdigest()
|
|
594
591
|
|
|
595
592
|
# X-Ca-Sign: HMAC-SHA1 with CA_SECRET as key
|
|
596
|
-
# The app builds: "api=/ppstrongs/" + getRealUrl(path) + "|X-Ca-Key=...|..."
|
|
597
|
-
# getRealUrl("/ppstrongs/redirect") returns "/ppstrongs/redirect" unchanged,
|
|
598
|
-
# so the full string is "api=/ppstrongs//ppstrongs/redirect|..."
|
|
599
593
|
ca_sign_data = (
|
|
600
594
|
f"api=/ppstrongs//ppstrongs/redirect"
|
|
601
595
|
f"|X-Ca-Key={CA_KEY}"
|
|
@@ -620,8 +614,9 @@ class CloudEdgeClient:
|
|
|
620
614
|
)
|
|
621
615
|
data = resp.json()
|
|
622
616
|
if data.get("resultCode") != "1001":
|
|
623
|
-
|
|
624
|
-
|
|
617
|
+
raise NetworkError(
|
|
618
|
+
f"Redirect discovery returned unexpected code: {data.get('resultCode')}"
|
|
619
|
+
)
|
|
625
620
|
|
|
626
621
|
result = data.get("result", {})
|
|
627
622
|
api_server = result.get("apiServer") or result.get("gwUrl") or ""
|
|
@@ -635,8 +630,15 @@ class CloudEdgeClient:
|
|
|
635
630
|
self.OPENAPI_BASE_URL = openapi_domain.rstrip("/")
|
|
636
631
|
self._log(f"Discovered OpenAPI server: {self.OPENAPI_BASE_URL}")
|
|
637
632
|
|
|
633
|
+
if not self.BASE_URL:
|
|
634
|
+
raise NetworkError(
|
|
635
|
+
"Redirect API did not return an apiServer"
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
except NetworkError:
|
|
639
|
+
raise
|
|
638
640
|
except Exception as exc:
|
|
639
|
-
|
|
641
|
+
raise NetworkError(f"Endpoint discovery failed: {exc}") from exc
|
|
640
642
|
|
|
641
643
|
def authenticate(self) -> bool:
|
|
642
644
|
"""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration constants for CloudEdge API
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Global redirect endpoint — used to discover the correct regional server.
|
|
6
|
+
# This is the single source of truth: the redirect API returns the correct
|
|
7
|
+
# ``apiServer`` and ``openapi`` domain for the given account/country.
|
|
8
|
+
REDIRECT_URL = "https://apis.cloudedge360.com/ppstrongs/redirect"
|
|
9
|
+
|
|
10
|
+
# API Keys (these are public keys from the mobile app)
|
|
11
|
+
CA_KEY = "bc29be30292a4309877807e101afbd51"
|
|
12
|
+
CA_SECRET = "35a69fd1-6527-4566-b190-921f9a651488"
|
|
13
|
+
|
|
14
|
+
# Default Headers
|
|
15
|
+
DEFAULT_HEADERS = {
|
|
16
|
+
"Accept-Language": "en-US,en;q=0.8",
|
|
17
|
+
"User-Agent": "Mozilla/5.0 (Linux; U; Android 10; en-us; Android SDK built for arm64 Build/QSR1.211112.002) AppleWebKit/533.1 (KHTML, like Gecko) Version/5.0 Mobile Safari/533.1",
|
|
18
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
19
|
+
"Accept-Encoding": "gzip, deflate, br"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# API Constants
|
|
23
|
+
PHONE_TYPE = "a"
|
|
24
|
+
SOURCE_APP = "8"
|
|
25
|
+
APP_VERSION = "5.5.1"
|
|
26
|
+
IOT_TYPE = "4"
|
|
27
|
+
APP_VERSION_CODE = "551"
|
|
28
|
+
DEFAULT_LANGUAGE = "en"
|
|
29
|
+
|
|
30
|
+
# Timeout values (seconds)
|
|
31
|
+
DEFAULT_TIMEOUT = 30
|
|
32
|
+
PING_TIMEOUT = 2.0
|
|
33
|
+
|
|
34
|
+
# Cache settings
|
|
35
|
+
DEFAULT_CACHE_FILE = ".cloudedge_session_cache"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Streaming Video Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Deliver a real end-to-end live video stream for the Garage camera on branch `streming_video`, first in `pycloudedge`, then through `cloudedge-ha`.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Keep `pycloudedge` as the proprietary P2P/HEVC extractor and keep `cloudedge-ha` as the Home Assistant bridge that remuxes raw HEVC into a local MPEG-TS `stream_source`. Validate the path incrementally: baseline tests, direct Garage streaming, then Home Assistant bridge integration.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Python, pytest, CloudEdge OpenAPI + proprietary P2P signaling, HEVC, ffmpeg, Home Assistant camera `stream` integration.
|
|
10
|
+
|
|
11
|
+
### Task 1: Restore a trustworthy `pycloudedge` baseline
|
|
12
|
+
|
|
13
|
+
**Files:**
|
|
14
|
+
- Modify: `tests/test_basic.py`
|
|
15
|
+
- Test: `tests/test_basic.py::TestAsyncMethods::test_client_methods_with_mock`
|
|
16
|
+
- Test: `tests/test_basic.py`
|
|
17
|
+
|
|
18
|
+
**Step 1: Confirm the failing test is a real baseline regression**
|
|
19
|
+
|
|
20
|
+
Run: `.venv/bin/pytest tests/test_basic.py::TestAsyncMethods::test_client_methods_with_mock -v`
|
|
21
|
+
Expected: FAIL with `ValidationError: Invalid email format: test`
|
|
22
|
+
|
|
23
|
+
**Step 2: Apply the minimal fix in the test**
|
|
24
|
+
|
|
25
|
+
Replace the invalid username with a valid email literal already accepted by the constructor.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
client = CloudEdgeClient("test@example.com", "test", "US", "+1")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Step 3: Re-run the targeted test**
|
|
32
|
+
|
|
33
|
+
Run: `.venv/bin/pytest tests/test_basic.py::TestAsyncMethods::test_client_methods_with_mock -v`
|
|
34
|
+
Expected: PASS
|
|
35
|
+
|
|
36
|
+
**Step 4: Re-run the full `pycloudedge` suite**
|
|
37
|
+
|
|
38
|
+
Run: `.venv/bin/pytest`
|
|
39
|
+
Expected: all tests pass, or only pre-existing warnings remain
|
|
40
|
+
|
|
41
|
+
### Task 2: Make direct Garage streaming reproducible from `pycloudedge`
|
|
42
|
+
|
|
43
|
+
**Files:**
|
|
44
|
+
- Modify: `tests/test_improvements.py` or create `tests/test_streaming_helpers.py`
|
|
45
|
+
- Modify if needed: `examples/stream_test.py`
|
|
46
|
+
- Modify if needed: `cloudedge/client.py`
|
|
47
|
+
- Modify if needed: `cloudedge/p2p/p2p_streamer.py`
|
|
48
|
+
|
|
49
|
+
**Step 1: Write a failing helper-level test for the smallest bug you need to fix**
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
- Annex B conversion for one access unit
|
|
53
|
+
- Keyframe gating behavior
|
|
54
|
+
- signaling candidate ordering
|
|
55
|
+
- phone code normalization or env parsing if that blocks the real script
|
|
56
|
+
|
|
57
|
+
Use a focused unit test instead of mocking the whole live session.
|
|
58
|
+
|
|
59
|
+
**Step 2: Run the focused test and watch it fail**
|
|
60
|
+
|
|
61
|
+
Run: `.venv/bin/pytest <targeted test path> -v`
|
|
62
|
+
Expected: FAIL for the exact missing behavior
|
|
63
|
+
|
|
64
|
+
**Step 3: Implement the minimal code change**
|
|
65
|
+
|
|
66
|
+
Touch only the production function proved by the test.
|
|
67
|
+
|
|
68
|
+
**Step 4: Re-run the focused test and then the full suite**
|
|
69
|
+
|
|
70
|
+
Run:
|
|
71
|
+
- `.venv/bin/pytest <targeted test path> -v`
|
|
72
|
+
- `.venv/bin/pytest`
|
|
73
|
+
|
|
74
|
+
Expected: PASS
|
|
75
|
+
|
|
76
|
+
**Step 5: Validate against the real Garage camera**
|
|
77
|
+
|
|
78
|
+
Run in order:
|
|
79
|
+
- `.venv/bin/pip install -e .[examples,mqtt]`
|
|
80
|
+
- `.venv/bin/python examples/stream_test.py /Users/fdaloisio/mygit/pycloudedge/.env`
|
|
81
|
+
|
|
82
|
+
Success criteria:
|
|
83
|
+
- authentication succeeds
|
|
84
|
+
- Garage is selected
|
|
85
|
+
- stream login succeeds
|
|
86
|
+
- continuous video frames are received
|
|
87
|
+
|
|
88
|
+
If playback inspection is needed and the environment supports it:
|
|
89
|
+
- `.venv/bin/python examples/stream_test.py /Users/fdaloisio/mygit/pycloudedge/.env --ffplay`
|
|
90
|
+
|
|
91
|
+
### Task 3: Capture objective evidence that the live stream is real
|
|
92
|
+
|
|
93
|
+
**Files:**
|
|
94
|
+
- Create if needed: `examples/stream_record.py`
|
|
95
|
+
- Modify if needed: `examples/stream_test.py`
|
|
96
|
+
|
|
97
|
+
**Step 1: Write a failing test for any new helper introduced for recording/probing**
|
|
98
|
+
|
|
99
|
+
If you need a new helper to write frames to `ffmpeg`, test that helper first. Keep the test offline and deterministic.
|
|
100
|
+
|
|
101
|
+
**Step 2: Implement the smallest helper needed**
|
|
102
|
+
|
|
103
|
+
Examples:
|
|
104
|
+
- write Annex B frames to ffmpeg stdin
|
|
105
|
+
- stop after first valid keyframe plus N seconds
|
|
106
|
+
- emit a non-secret summary line for verification
|
|
107
|
+
|
|
108
|
+
**Step 3: Run a real recording/probe command**
|
|
109
|
+
|
|
110
|
+
Examples:
|
|
111
|
+
- `.venv/bin/python examples/stream_test.py /Users/fdaloisio/mygit/pycloudedge/.env`
|
|
112
|
+
- `ffmpeg -hide_banner -loglevel warning -f hevc -i <captured stream> -t 10 -f null -`
|
|
113
|
+
- `ffprobe <captured artifact>`
|
|
114
|
+
|
|
115
|
+
Expected: ffmpeg/ffprobe recognizes a valid video stream and reports video packets/frames.
|
|
116
|
+
|
|
117
|
+
### Task 4: Verify and harden the Home Assistant bridge path
|
|
118
|
+
|
|
119
|
+
**Files:**
|
|
120
|
+
- Modify if needed: `/Users/fdaloisio/.config/superpowers/worktrees/cloudedge-ha/streming_video/custom_components/cloudedge/stream_bridge.py`
|
|
121
|
+
- Modify if needed: `/Users/fdaloisio/.config/superpowers/worktrees/cloudedge-ha/streming_video/custom_components/cloudedge/camera.py`
|
|
122
|
+
- Modify if needed: `/Users/fdaloisio/.config/superpowers/worktrees/cloudedge-ha/streming_video/custom_components/cloudedge/__init__.py`
|
|
123
|
+
|
|
124
|
+
**Step 1: Write a failing unit test for the smallest HA-side bug you discover**
|
|
125
|
+
|
|
126
|
+
Focus on pure logic where possible:
|
|
127
|
+
- `stream_source()` behavior
|
|
128
|
+
- keyframe gating
|
|
129
|
+
- bootstrap keyframe handling
|
|
130
|
+
- bridge lifecycle / idle stop behavior
|
|
131
|
+
|
|
132
|
+
**Step 2: Run the targeted test and watch it fail**
|
|
133
|
+
|
|
134
|
+
Use the project’s chosen Python test command once test scaffolding exists.
|
|
135
|
+
|
|
136
|
+
**Step 3: Implement the minimal bridge fix**
|
|
137
|
+
|
|
138
|
+
Keep the architecture unchanged: `pycloudedge` provides raw frames, `cloudedge-ha` remuxes them and exposes `tcp://127.0.0.1:<port>`.
|
|
139
|
+
|
|
140
|
+
**Step 4: Validate the runtime path**
|
|
141
|
+
|
|
142
|
+
At minimum verify:
|
|
143
|
+
- `async_get_stream_source()` returns a local TCP source
|
|
144
|
+
- `ffmpeg` starts successfully
|
|
145
|
+
- the bridge accepts a client and emits MPEG-TS bytes
|
|
146
|
+
|
|
147
|
+
If a local HA harness must be created, keep it minimal and non-destructive.
|
|
148
|
+
|
|
149
|
+
### Task 5: Document and verify the finished streaming path
|
|
150
|
+
|
|
151
|
+
**Files:**
|
|
152
|
+
- Modify: `README.md` in `pycloudedge` if examples change
|
|
153
|
+
- Modify: `README.md` in `cloudedge-ha` if runtime requirements or caveats change
|
|
154
|
+
- Modify: `docs/plans/2026-03-30-streaming-video.md`
|
|
155
|
+
|
|
156
|
+
**Step 1: Re-run all relevant automated checks**
|
|
157
|
+
|
|
158
|
+
Run:
|
|
159
|
+
- `.venv/bin/pytest`
|
|
160
|
+
- any new targeted HA-side tests
|
|
161
|
+
|
|
162
|
+
**Step 2: Re-run the real Garage validation**
|
|
163
|
+
|
|
164
|
+
Run the final direct streaming command again and confirm frame/stream evidence still holds.
|
|
165
|
+
|
|
166
|
+
**Step 3: Update docs with the exact supported path**
|
|
167
|
+
|
|
168
|
+
Document:
|
|
169
|
+
- branch requirements
|
|
170
|
+
- local dependency requirements (`ffmpeg`, local `pycloudedge` build)
|
|
171
|
+
- what works now
|
|
172
|
+
- what remains unsupported, especially audio if still absent
|
|
173
|
+
|
|
174
|
+
**Step 4: Record final verification evidence**
|
|
175
|
+
|
|
176
|
+
Capture the exact commands run and the observed non-secret success signals so the work can be reproduced.
|
|
@@ -7,6 +7,7 @@ pyproject.toml
|
|
|
7
7
|
requirements-dev.txt
|
|
8
8
|
requirements.txt
|
|
9
9
|
setup.py
|
|
10
|
+
test_3region_login.py
|
|
10
11
|
cloudedge/__init__.py
|
|
11
12
|
cloudedge/_version.py
|
|
12
13
|
cloudedge/cli.py
|
|
@@ -19,6 +20,7 @@ cloudedge/logging_config.py
|
|
|
19
20
|
cloudedge/mqtt.py
|
|
20
21
|
cloudedge/utils.py
|
|
21
22
|
cloudedge/validators.py
|
|
23
|
+
docs/plans/2026-03-30-streaming-video.md
|
|
22
24
|
examples/README.md
|
|
23
25
|
examples/basic_example.py
|
|
24
26
|
examples/device_control.py
|
|
@@ -66,6 +66,7 @@ cloudedge = "cloudedge.cli:main"
|
|
|
66
66
|
|
|
67
67
|
[tool.setuptools_scm]
|
|
68
68
|
write_to = "cloudedge/_version.py"
|
|
69
|
+
version_scheme = "only-version"
|
|
69
70
|
|
|
70
71
|
[tool.setuptools.packages.find]
|
|
71
72
|
where = ["."]
|
|
@@ -113,4 +114,4 @@ testpaths = [
|
|
|
113
114
|
]
|
|
114
115
|
python_files = "test_*.py"
|
|
115
116
|
python_classes = "Test*"
|
|
116
|
-
python_functions = "test_*"
|
|
117
|
+
python_functions = "test_*"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Test login with 3 region accounts after endpoint refactoring."""
|
|
3
|
+
import sys, os
|
|
4
|
+
|
|
5
|
+
PROJECT_ROOT = "/Users/fdaloisio/mygit/pycloudedge"
|
|
6
|
+
sys.path.insert(0, PROJECT_ROOT)
|
|
7
|
+
|
|
8
|
+
from cloudedge import CloudEdgeClient
|
|
9
|
+
|
|
10
|
+
ACCOUNTS = [
|
|
11
|
+
(".env", "EU/Italy"),
|
|
12
|
+
(".env_usa", "US"),
|
|
13
|
+
(".env_australia", "Australia"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
def load_env(path):
|
|
17
|
+
env = {}
|
|
18
|
+
with open(path) as f:
|
|
19
|
+
for line in f:
|
|
20
|
+
line = line.strip()
|
|
21
|
+
if line and not line.startswith("#") and "=" in line:
|
|
22
|
+
k, v = line.split("=", 1)
|
|
23
|
+
env[k] = v
|
|
24
|
+
return env
|
|
25
|
+
|
|
26
|
+
for env_file, label in ACCOUNTS:
|
|
27
|
+
env_path = os.path.join(PROJECT_ROOT, env_file)
|
|
28
|
+
env = load_env(env_path)
|
|
29
|
+
# Use a separate cache file per account to avoid cross-contamination
|
|
30
|
+
cache_file = f"/tmp/.cloudedge_cache_{env['CLOUDEDGE_COUNTRY_CODE'].lower()}"
|
|
31
|
+
# Remove old cache to force fresh discovery
|
|
32
|
+
if os.path.exists(cache_file):
|
|
33
|
+
os.remove(cache_file)
|
|
34
|
+
print(f"\n{'='*60}")
|
|
35
|
+
print(f" Testing {label} ({env['CLOUDEDGE_COUNTRY_CODE']})")
|
|
36
|
+
print(f"{'='*60}")
|
|
37
|
+
try:
|
|
38
|
+
client = CloudEdgeClient(
|
|
39
|
+
username=env["CLOUDEDGE_USERNAME"],
|
|
40
|
+
password=env["CLOUDEDGE_PASSWORD"],
|
|
41
|
+
country_code=env["CLOUDEDGE_COUNTRY_CODE"],
|
|
42
|
+
phone_code=env["CLOUDEDGE_PHONE_CODE"],
|
|
43
|
+
debug=True,
|
|
44
|
+
session_cache_file=cache_file,
|
|
45
|
+
)
|
|
46
|
+
success = client.authenticate()
|
|
47
|
+
print(f" ✅ Login OK")
|
|
48
|
+
print(f" BASE_URL: {client.BASE_URL}")
|
|
49
|
+
print(f" OPENAPI_BASE_URL: {client.OPENAPI_BASE_URL}")
|
|
50
|
+
|
|
51
|
+
# Get list of cameras
|
|
52
|
+
devices = client.get_devices()
|
|
53
|
+
print(f" Found {len(devices)} devices:")
|
|
54
|
+
for dev in devices:
|
|
55
|
+
status = "Online" if dev.get("online") else "Offline"
|
|
56
|
+
print(f" - {dev.get('name', 'Unknown')} (SN: {dev.get('serial_number', 'Unknown')}) [{status}]")
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print(f" ❌ Login FAILED: {e}")
|
|
@@ -21,32 +21,32 @@ from cloudedge.iot_parameters import get_parameter_name, format_parameter_value
|
|
|
21
21
|
|
|
22
22
|
class TestIoTParameters(unittest.TestCase):
|
|
23
23
|
"""Test IoT parameter functions."""
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
def test_get_parameter_name(self):
|
|
26
26
|
"""Test parameter name retrieval."""
|
|
27
27
|
# Test known parameter
|
|
28
28
|
self.assertEqual(get_parameter_name("167"), "FRONT_LIGHT_SWITCH")
|
|
29
29
|
self.assertEqual(get_parameter_name("103"), "LED_ENABLE")
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
# Test unknown parameter
|
|
32
32
|
self.assertEqual(get_parameter_name("99999"), "iot_99999")
|
|
33
|
-
|
|
33
|
+
|
|
34
34
|
def test_format_parameter_value(self):
|
|
35
35
|
"""Test parameter value formatting."""
|
|
36
36
|
# Test boolean parameter
|
|
37
37
|
self.assertEqual(format_parameter_value("LED_ENABLE", "1"), "Enabled")
|
|
38
38
|
self.assertEqual(format_parameter_value("LED_ENABLE", "0"), "Disabled")
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
# Test percentage parameter
|
|
41
41
|
self.assertEqual(format_parameter_value("BATTERY_PERCENT", 85), "85%")
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
# Test regular value
|
|
44
44
|
self.assertEqual(format_parameter_value("UNKNOWN_PARAM", "test"), "test")
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class TestCloudEdgeClient(unittest.TestCase):
|
|
48
48
|
"""Test CloudEdge client functionality."""
|
|
49
|
-
|
|
49
|
+
|
|
50
50
|
def setUp(self):
|
|
51
51
|
"""Set up test client."""
|
|
52
52
|
self.client = CloudEdgeClient(
|
|
@@ -54,9 +54,9 @@ class TestCloudEdgeClient(unittest.TestCase):
|
|
|
54
54
|
password="testpass",
|
|
55
55
|
country_code="US",
|
|
56
56
|
phone_code="+1",
|
|
57
|
-
debug=False
|
|
57
|
+
debug=False,
|
|
58
58
|
)
|
|
59
|
-
|
|
59
|
+
|
|
60
60
|
def test_client_initialization(self):
|
|
61
61
|
"""Test client initialization."""
|
|
62
62
|
self.assertEqual(self.client.username, "test@example.com")
|
|
@@ -64,19 +64,19 @@ class TestCloudEdgeClient(unittest.TestCase):
|
|
|
64
64
|
self.assertEqual(self.client.phone_code, "+1")
|
|
65
65
|
self.assertFalse(self.client.debug)
|
|
66
66
|
self.assertIsNone(self.client.session_data)
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
def test_format_sn(self):
|
|
69
69
|
"""Test serial number formatting."""
|
|
70
70
|
# Test 9-digit SN
|
|
71
71
|
self.assertEqual(self.client._format_sn("123456789"), "0000000123456789")
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
# Test longer SN
|
|
74
74
|
self.assertEqual(self.client._format_sn("ABCD123456789"), "123456789")
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
# Test empty SN
|
|
77
77
|
self.assertEqual(self.client._format_sn(""), "")
|
|
78
|
-
|
|
79
|
-
@patch(
|
|
78
|
+
|
|
79
|
+
@patch("cloudedge.client.requests.Session.post")
|
|
80
80
|
async def test_authenticate_success(self, mock_post):
|
|
81
81
|
"""Test successful authentication."""
|
|
82
82
|
# Mock successful login response
|
|
@@ -90,23 +90,23 @@ class TestCloudEdgeClient(unittest.TestCase):
|
|
|
90
90
|
"iot": {
|
|
91
91
|
"pfKey": {
|
|
92
92
|
"accessid": "test_access_id",
|
|
93
|
-
"accesskey": "test_access_key"
|
|
93
|
+
"accesskey": "test_access_key",
|
|
94
94
|
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
97
|
}
|
|
98
98
|
mock_response.raise_for_status.return_value = None
|
|
99
99
|
mock_post.return_value = mock_response
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
# Test authentication
|
|
102
102
|
success = await self.client.authenticate()
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
self.assertTrue(success)
|
|
105
105
|
self.assertIsNotNone(self.client.session_data)
|
|
106
106
|
self.assertEqual(self.client.session_data["userToken"], "test_token")
|
|
107
107
|
self.assertEqual(self.client.session_data["userID"], "test_user_id")
|
|
108
|
-
|
|
109
|
-
@patch(
|
|
108
|
+
|
|
109
|
+
@patch("cloudedge.client.requests.Session.post")
|
|
110
110
|
async def test_authenticate_failure(self, mock_post):
|
|
111
111
|
"""Test failed authentication."""
|
|
112
112
|
# Mock failed login response
|
|
@@ -114,29 +114,26 @@ class TestCloudEdgeClient(unittest.TestCase):
|
|
|
114
114
|
mock_response.status_code = 200
|
|
115
115
|
mock_response.json.return_value = {
|
|
116
116
|
"resultCode": "1002",
|
|
117
|
-
"resultMsg": "Invalid credentials"
|
|
117
|
+
"resultMsg": "Invalid credentials",
|
|
118
118
|
}
|
|
119
119
|
mock_response.raise_for_status.return_value = None
|
|
120
120
|
mock_post.return_value = mock_response
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
# Test authentication failure
|
|
123
123
|
with self.assertRaises(AuthenticationError):
|
|
124
124
|
await self.client.authenticate()
|
|
125
|
-
|
|
125
|
+
|
|
126
126
|
async def test_get_devices_not_authenticated(self):
|
|
127
127
|
"""Test getting devices without authentication."""
|
|
128
128
|
with self.assertRaises(AuthenticationError):
|
|
129
129
|
await self.client.get_devices()
|
|
130
|
-
|
|
131
|
-
@patch(
|
|
130
|
+
|
|
131
|
+
@patch("cloudedge.client.requests.Session.post")
|
|
132
132
|
async def test_get_devices_success(self, mock_post):
|
|
133
133
|
"""Test successful device retrieval."""
|
|
134
134
|
# Set up authenticated session
|
|
135
|
-
self.client.session_data = {
|
|
136
|
-
|
|
137
|
-
"userID": "test_user_id"
|
|
138
|
-
}
|
|
139
|
-
|
|
135
|
+
self.client.session_data = {"userToken": "test_token", "userID": "test_user_id"}
|
|
136
|
+
|
|
140
137
|
# Mock device list response
|
|
141
138
|
mock_response = Mock()
|
|
142
139
|
mock_response.status_code = 200
|
|
@@ -151,42 +148,46 @@ class TestCloudEdgeClient(unittest.TestCase):
|
|
|
151
148
|
"deviceTypeName": "Camera",
|
|
152
149
|
"devTypeID": "1",
|
|
153
150
|
"hostKey": "hostkey123",
|
|
154
|
-
"onLine": 1
|
|
151
|
+
"onLine": 1,
|
|
155
152
|
}
|
|
156
153
|
]
|
|
157
|
-
}
|
|
154
|
+
},
|
|
158
155
|
}
|
|
159
156
|
mock_response.raise_for_status.return_value = None
|
|
160
157
|
mock_post.return_value = mock_response
|
|
161
|
-
|
|
158
|
+
|
|
162
159
|
# Test device retrieval
|
|
163
160
|
devices = await self.client.get_devices()
|
|
164
|
-
|
|
161
|
+
|
|
165
162
|
self.assertEqual(len(devices), 1)
|
|
166
163
|
device = devices[0]
|
|
167
164
|
self.assertEqual(device["device_id"], "device1")
|
|
168
165
|
self.assertEqual(device["name"], "Test Camera")
|
|
169
166
|
self.assertTrue(device["online"])
|
|
170
|
-
|
|
167
|
+
|
|
171
168
|
async def test_find_device_by_name_not_authenticated(self):
|
|
172
169
|
"""Test finding device without authentication."""
|
|
173
170
|
# Mock get_devices to raise AuthenticationError
|
|
174
|
-
with patch.object(
|
|
171
|
+
with patch.object(
|
|
172
|
+
self.client,
|
|
173
|
+
"get_devices",
|
|
174
|
+
side_effect=AuthenticationError("Not authenticated"),
|
|
175
|
+
):
|
|
175
176
|
result = await self.client.find_device_by_name("Test Device")
|
|
176
177
|
self.assertIsNone(result)
|
|
177
178
|
|
|
178
179
|
|
|
179
180
|
class TestAsyncMethods(unittest.IsolatedAsyncioTestCase):
|
|
180
181
|
"""Test async methods with proper async test support."""
|
|
181
|
-
|
|
182
|
+
|
|
182
183
|
async def test_client_methods_with_mock(self):
|
|
183
184
|
"""Test client methods with mocked dependencies."""
|
|
184
|
-
client = CloudEdgeClient("test", "test", "US", "+1")
|
|
185
|
-
|
|
185
|
+
client = CloudEdgeClient("test@example.com", "test", "US", "+1")
|
|
186
|
+
|
|
186
187
|
# Test that methods require authentication
|
|
187
188
|
with self.assertRaises(AuthenticationError):
|
|
188
189
|
await client.get_devices()
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
with self.assertRaises(AuthenticationError):
|
|
191
192
|
await client.get_device_status("device1")
|
|
192
193
|
|
|
@@ -195,28 +196,28 @@ def run_tests():
|
|
|
195
196
|
"""Run all tests."""
|
|
196
197
|
loader = unittest.TestLoader()
|
|
197
198
|
suite = unittest.TestSuite()
|
|
198
|
-
|
|
199
|
+
|
|
199
200
|
# Add test classes
|
|
200
201
|
suite.addTests(loader.loadTestsFromTestCase(TestIoTParameters))
|
|
201
202
|
suite.addTests(loader.loadTestsFromTestCase(TestCloudEdgeClient))
|
|
202
203
|
suite.addTests(loader.loadTestsFromTestCase(TestAsyncMethods))
|
|
203
|
-
|
|
204
|
+
|
|
204
205
|
# Run tests
|
|
205
206
|
runner = unittest.TextTestRunner(verbosity=2)
|
|
206
207
|
result = runner.run(suite)
|
|
207
|
-
|
|
208
|
+
|
|
208
209
|
return result.wasSuccessful()
|
|
209
210
|
|
|
210
211
|
|
|
211
212
|
if __name__ == "__main__":
|
|
212
213
|
print("CloudEdge API Library - Basic Tests")
|
|
213
214
|
print("=" * 50)
|
|
214
|
-
|
|
215
|
+
|
|
215
216
|
success = run_tests()
|
|
216
|
-
|
|
217
|
+
|
|
217
218
|
if success:
|
|
218
219
|
print("\n✅ All tests passed!")
|
|
219
220
|
exit(0)
|
|
220
221
|
else:
|
|
221
222
|
print("\n❌ Some tests failed!")
|
|
222
|
-
exit(1)
|
|
223
|
+
exit(1)
|
|
@@ -59,6 +59,15 @@ class TestPhase1InputValidation:
|
|
|
59
59
|
assert client.country_code == "US"
|
|
60
60
|
assert client.phone_code == "+1"
|
|
61
61
|
|
|
62
|
+
def test_uk_country_code_normalizes_to_gb(self):
|
|
63
|
+
"""Test that UK input normalizes to GB for CloudEdge APIs."""
|
|
64
|
+
client = CloudEdgeClient("user@example.com", "password", "UK", "+44")
|
|
65
|
+
|
|
66
|
+
assert client.country_code == "GB"
|
|
67
|
+
assert client.phone_code == "+44"
|
|
68
|
+
assert client.BASE_URL is None
|
|
69
|
+
assert client.OPENAPI_BASE_URL is None
|
|
70
|
+
|
|
62
71
|
|
|
63
72
|
class TestPhase1Logging:
|
|
64
73
|
"""Test Phase 1: Proper logging implementation"""
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
# file generated by setuptools-scm
|
|
2
|
-
# don't change, don't track in version control
|
|
3
|
-
|
|
4
|
-
__all__ = [
|
|
5
|
-
"__version__",
|
|
6
|
-
"__version_tuple__",
|
|
7
|
-
"version",
|
|
8
|
-
"version_tuple",
|
|
9
|
-
"__commit_id__",
|
|
10
|
-
"commit_id",
|
|
11
|
-
]
|
|
12
|
-
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
|
-
version: str
|
|
25
|
-
__version__: str
|
|
26
|
-
__version_tuple__: VERSION_TUPLE
|
|
27
|
-
version_tuple: VERSION_TUPLE
|
|
28
|
-
commit_id: COMMIT_ID
|
|
29
|
-
__commit_id__: COMMIT_ID
|
|
30
|
-
|
|
31
|
-
__version__ = version = '0.1.6.dev2'
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1, 6, 'dev2')
|
|
33
|
-
|
|
34
|
-
__commit_id__ = commit_id = None
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Configuration constants for CloudEdge API
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
# Region identifiers
|
|
6
|
-
REGION_EU = "eu"
|
|
7
|
-
REGION_US = "us"
|
|
8
|
-
REGION_AP = "ap"
|
|
9
|
-
|
|
10
|
-
# Per-region API endpoints (static fallback when redirect discovery fails)
|
|
11
|
-
REGION_ENDPOINTS = {
|
|
12
|
-
REGION_EU: {
|
|
13
|
-
"base_url": "https://apis-eu-frankfurt.cloudedge360.com",
|
|
14
|
-
"openapi_base_url": "https://openapi-euce.mearicloud.com",
|
|
15
|
-
},
|
|
16
|
-
REGION_US: {
|
|
17
|
-
"base_url": "https://apis-us-west.cloudedge360.com",
|
|
18
|
-
"openapi_base_url": "https://openapi-uswe.mearicloud.com",
|
|
19
|
-
},
|
|
20
|
-
REGION_AP: {
|
|
21
|
-
"base_url": "https://apis-as-singapore.cloudedge360.com",
|
|
22
|
-
"openapi_base_url": "https://openapi-usce.mearicloud.com",
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
# Global redirect endpoint — used to discover the correct regional server
|
|
27
|
-
REDIRECT_URL = "https://apis.cloudedge360.com/ppstrongs/redirect"
|
|
28
|
-
|
|
29
|
-
# Default endpoints (EU) – kept for backward compatibility
|
|
30
|
-
BASE_URL = REGION_ENDPOINTS[REGION_EU]["base_url"]
|
|
31
|
-
OPENAPI_BASE_URL = REGION_ENDPOINTS[REGION_EU]["openapi_base_url"]
|
|
32
|
-
|
|
33
|
-
# European country codes (ISO 3166-1 alpha-2)
|
|
34
|
-
EU_COUNTRY_CODES = {
|
|
35
|
-
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR",
|
|
36
|
-
"DE", "GR", "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL",
|
|
37
|
-
"PL", "PT", "RO", "SK", "SI", "ES", "SE",
|
|
38
|
-
# EEA + EFTA + UK
|
|
39
|
-
"IS", "LI", "NO", "CH", "GB",
|
|
40
|
-
# Other European countries
|
|
41
|
-
"AL", "AD", "AM", "AZ", "BA", "BY", "GE", "GI", "XK", "MD",
|
|
42
|
-
"MC", "ME", "MK", "RS", "RU", "SM", "TR", "UA", "VA",
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# Asia-Pacific / Oceania / Middle East / Africa country codes
|
|
47
|
-
# These are routed through the Singapore gateway
|
|
48
|
-
AP_COUNTRY_CODES = {
|
|
49
|
-
# East Asia
|
|
50
|
-
"JP", "KR", "TW", "HK", "MO",
|
|
51
|
-
# Southeast Asia
|
|
52
|
-
"SG", "MY", "TH", "VN", "PH", "ID", "MM", "KH", "LA", "BN", "TL",
|
|
53
|
-
# South Asia
|
|
54
|
-
"IN", "PK", "BD", "LK", "NP", "BT", "MV",
|
|
55
|
-
# Oceania
|
|
56
|
-
"AU", "NZ", "FJ", "PG", "WS", "TO", "VU", "SB", "KI", "MH", "FM",
|
|
57
|
-
"PW", "NR", "TV", "CK", "NU",
|
|
58
|
-
# Middle East
|
|
59
|
-
"AE", "SA", "QA", "KW", "BH", "OM", "JO", "LB", "IQ", "IR", "YE",
|
|
60
|
-
"SY", "PS", "IL",
|
|
61
|
-
# Africa
|
|
62
|
-
"ZA", "EG", "NG", "KE", "GH", "TZ", "UG", "ET", "MA", "DZ", "TN",
|
|
63
|
-
"SN", "CI", "CM", "AO", "MZ", "ZW", "BW", "NA", "MU", "MG", "RW",
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def region_for_country(country_code: str) -> str:
|
|
68
|
-
"""Return the region key for a given ISO 3166-1 alpha-2 country code."""
|
|
69
|
-
cc = country_code.upper()
|
|
70
|
-
if cc in EU_COUNTRY_CODES:
|
|
71
|
-
return REGION_EU
|
|
72
|
-
if cc in AP_COUNTRY_CODES:
|
|
73
|
-
return REGION_AP
|
|
74
|
-
return REGION_US
|
|
75
|
-
|
|
76
|
-
# API Keys (these are public keys from the mobile app)
|
|
77
|
-
CA_KEY = "bc29be30292a4309877807e101afbd51"
|
|
78
|
-
CA_SECRET = "35a69fd1-6527-4566-b190-921f9a651488"
|
|
79
|
-
|
|
80
|
-
# Default Headers
|
|
81
|
-
DEFAULT_HEADERS = {
|
|
82
|
-
"Accept-Language": "en-US,en;q=0.8",
|
|
83
|
-
"User-Agent": "Mozilla/5.0 (Linux; U; Android 10; en-us; Android SDK built for arm64 Build/QSR1.211112.002) AppleWebKit/533.1 (KHTML, like Gecko) Version/5.0 Mobile Safari/533.1",
|
|
84
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
85
|
-
"Accept-Encoding": "gzip, deflate, br"
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
# API Constants
|
|
89
|
-
PHONE_TYPE = "a"
|
|
90
|
-
SOURCE_APP = "8"
|
|
91
|
-
APP_VERSION = "5.5.1"
|
|
92
|
-
IOT_TYPE = "4"
|
|
93
|
-
APP_VERSION_CODE = "551"
|
|
94
|
-
DEFAULT_LANGUAGE = "en"
|
|
95
|
-
|
|
96
|
-
# Timeout values (seconds)
|
|
97
|
-
DEFAULT_TIMEOUT = 30
|
|
98
|
-
PING_TIMEOUT = 2.0
|
|
99
|
-
|
|
100
|
-
# Cache settings
|
|
101
|
-
DEFAULT_CACHE_FILE = ".cloudedge_session_cache"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|