cloudbeat-common 1.1.2__tar.gz → 1.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 (23) hide show
  1. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/PKG-INFO +2 -1
  2. cloudbeat_common-1.2.0/cloudbeat.py +21 -0
  3. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/cloudbeat_common.egg-info/PKG-INFO +2 -1
  4. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/cloudbeat_common.egg-info/SOURCES.txt +1 -1
  5. cloudbeat_common-1.2.0/cloudbeat_common.egg-info/requires.txt +2 -0
  6. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/setup.py +1 -0
  7. cloudbeat_common-1.2.0/src/client.py +246 -0
  8. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/src/models.py +1 -0
  9. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/src/reporter.py +46 -1
  10. cloudbeat_common-1.1.2/cloudbeat.py +0 -8
  11. cloudbeat_common-1.1.2/cloudbeat_common.egg-info/requires.txt +0 -1
  12. cloudbeat_common-1.1.2/requirements.txt +0 -1
  13. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/cloudbeat_common.egg-info/dependency_links.txt +0 -0
  14. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/cloudbeat_common.egg-info/top_level.txt +0 -0
  15. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/setup.cfg +0 -0
  16. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/src/__init__.py +0 -0
  17. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/src/cb.py +0 -0
  18. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/src/helpers.py +0 -0
  19. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/src/json_util.py +0 -0
  20. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/tests/__init__.py +0 -0
  21. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/tests/conftest.py +0 -0
  22. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/tests/test_reporter.py +0 -0
  23. {cloudbeat_common-1.1.2 → cloudbeat_common-1.2.0}/tests/test_step.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudbeat-common
3
- Version: 1.1.2
3
+ Version: 1.2.0
4
4
  Summary: Contains the common types and API client for CloudBeat
5
5
  Home-page: https://cloudbeat.io
6
6
  Author: CBNR Cloud Solutions LTD
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Requires-Python: >=3.8
19
19
  Description-Content-Type: text/markdown
20
20
  Requires-Dist: attrs>=16.0.0
21
+ Requires-Dist: requests>=2.20.0
21
22
  Dynamic: author
22
23
  Dynamic: author-email
23
24
  Dynamic: classifier
@@ -0,0 +1,21 @@
1
+ from cloudbeat_common.models import TestStatus
2
+ from cloudbeat_common.reporter import CbTestReporter
3
+ from cloudbeat_common.client import (
4
+ CbApiError,
5
+ RunStatusInfo,
6
+ CaseStatusUpdateReq,
7
+ SuiteStatusUpdateReq,
8
+ RuntimeApiV2,
9
+ )
10
+
11
+
12
+ __all__ = [
13
+ 'TestStatus',
14
+ 'CbTestReporter',
15
+ # Client API
16
+ 'CbApiError',
17
+ 'RunStatusInfo',
18
+ 'CaseStatusUpdateReq',
19
+ 'SuiteStatusUpdateReq',
20
+ 'RuntimeApiV2',
21
+ ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudbeat-common
3
- Version: 1.1.2
3
+ Version: 1.2.0
4
4
  Summary: Contains the common types and API client for CloudBeat
5
5
  Home-page: https://cloudbeat.io
6
6
  Author: CBNR Cloud Solutions LTD
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
18
18
  Requires-Python: >=3.8
19
19
  Description-Content-Type: text/markdown
20
20
  Requires-Dist: attrs>=16.0.0
21
+ Requires-Dist: requests>=2.20.0
21
22
  Dynamic: author
22
23
  Dynamic: author-email
23
24
  Dynamic: classifier
@@ -1,5 +1,4 @@
1
1
  cloudbeat.py
2
- requirements.txt
3
2
  setup.py
4
3
  cloudbeat_common.egg-info/PKG-INFO
5
4
  cloudbeat_common.egg-info/SOURCES.txt
@@ -8,6 +7,7 @@ cloudbeat_common.egg-info/requires.txt
8
7
  cloudbeat_common.egg-info/top_level.txt
9
8
  src/__init__.py
10
9
  src/cb.py
10
+ src/client.py
11
11
  src/helpers.py
12
12
  src/json_util.py
13
13
  src/models.py
@@ -0,0 +1,2 @@
1
+ attrs>=16.0.0
2
+ requests>=2.20.0
@@ -15,6 +15,7 @@ classifiers = [
15
15
 
16
16
  install_requires = [
17
17
  "attrs>=16.0.0",
18
+ "requests>=2.20.0",
18
19
  ]
19
20
 
20
21
 
@@ -0,0 +1,246 @@
1
+ """
2
+ CloudBeat API client
3
+
4
+ V2 API: authenticated via Bearer token (Authorization header).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, List, Optional
12
+
13
+ import requests
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Error
20
+ # ---------------------------------------------------------------------------
21
+
22
+
23
+ class CbApiError(Exception):
24
+ """Raised when a CloudBeat API call fails."""
25
+
26
+ def __init__(self, message_or_exc: Any = "", response: Optional[requests.Response] = None):
27
+ msg = message_or_exc if isinstance(message_or_exc, str) else str(message_or_exc)
28
+
29
+ if response is not None:
30
+ status = response.status_code
31
+ if status == 500:
32
+ msg = "Internal server error, please try again later."
33
+ elif status == 401:
34
+ msg = "Authentication failed, invalid API key."
35
+ elif status == 404:
36
+ msg = "A record or an endpoint does not exist."
37
+ elif status == 204:
38
+ msg = "A record or an endpoint does not have content."
39
+ elif status == 422:
40
+ try:
41
+ data = response.json() or {}
42
+ err_msg = data.get("errorMessage", "")
43
+ errors: List[str] = data.get("errors") or []
44
+ if err_msg:
45
+ if errors:
46
+ err_msg += ": " + " ".join(errors)
47
+ msg = err_msg
48
+ else:
49
+ msg = "Validation Failed"
50
+ except Exception:
51
+ msg = "Validation Failed"
52
+ else:
53
+ msg = response.reason or str(status)
54
+
55
+ super().__init__(msg)
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Request data models
60
+ # ---------------------------------------------------------------------------
61
+
62
+
63
+ @dataclass
64
+ class RunStatusInfo:
65
+ """V2: payload for updating run/instance status."""
66
+
67
+ run_id: Optional[str] = None
68
+ instance_id: Optional[str] = None
69
+ status: Optional[str] = None
70
+ progress: Optional[int] = None
71
+
72
+ def to_dict(self) -> Dict[str, Any]:
73
+ d: Dict[str, Any] = {}
74
+ if self.run_id is not None:
75
+ d["runId"] = self.run_id
76
+ if self.instance_id is not None:
77
+ d["instanceId"] = self.instance_id
78
+ if self.status is not None:
79
+ d["status"] = self.status
80
+ if self.progress is not None:
81
+ d["progress"] = self.progress
82
+ return d
83
+
84
+
85
+ @dataclass
86
+ class CaseStatusUpdateReq:
87
+ """V2: payload for updating a test case's runtime status."""
88
+
89
+ # Used in the request path
90
+ run_id: Optional[str] = None
91
+ instance_id: Optional[str] = None
92
+ # Request body fields
93
+ id: Optional[str] = None # internal case UUID
94
+ fqn: Optional[str] = None
95
+ parent_fqn: Optional[str] = None # parent suite fqn
96
+ parent_id: Optional[str] = None # parent suite id
97
+ name: Optional[str] = None
98
+ start_time: Optional[int] = None
99
+ end_time: Optional[int] = None
100
+ run_status: Optional[str] = None # e.g. "Running" / "Finished"
101
+ test_status: Optional[str] = None # e.g. "passed" / "failed"
102
+ framework: Optional[str] = None
103
+ language: Optional[str] = None
104
+ capabilities: Optional[Dict[str, Any]] = None
105
+ timestamp: Optional[int] = None
106
+
107
+ def to_dict(self) -> Dict[str, Any]:
108
+ d: Dict[str, Any] = {}
109
+ if self.run_id is not None:
110
+ d["runId"] = self.run_id
111
+ if self.instance_id is not None:
112
+ d["instanceId"] = self.instance_id
113
+ if self.id is not None:
114
+ d["id"] = self.id
115
+ if self.fqn is not None:
116
+ d["fqn"] = self.fqn
117
+ if self.parent_fqn is not None:
118
+ d["parentFqn"] = self.parent_fqn
119
+ if self.parent_id is not None:
120
+ d["parentId"] = self.parent_id
121
+ if self.name is not None:
122
+ d["name"] = self.name
123
+ if self.start_time is not None:
124
+ d["startTime"] = self.start_time
125
+ if self.end_time is not None:
126
+ d["endTime"] = self.end_time
127
+ if self.run_status is not None:
128
+ d["runStatus"] = self.run_status
129
+ if self.test_status is not None:
130
+ d["testStatus"] = self.test_status
131
+ if self.framework is not None:
132
+ d["framework"] = self.framework
133
+ if self.language is not None:
134
+ d["language"] = self.language
135
+ if self.capabilities is not None:
136
+ d["capabilities"] = self.capabilities
137
+ if self.timestamp is not None:
138
+ d["timestamp"] = self.timestamp
139
+ return d
140
+
141
+
142
+ @dataclass
143
+ class SuiteStatusUpdateReq:
144
+ """V2: payload for updating a test suite's runtime status."""
145
+
146
+ run_id: Optional[str] = None
147
+ instance_id: Optional[str] = None
148
+ suite_id: Optional[str] = None
149
+ status: Optional[str] = None
150
+ progress: Optional[int] = None
151
+
152
+ def to_dict(self) -> Dict[str, Any]:
153
+ d: Dict[str, Any] = {}
154
+ if self.run_id is not None:
155
+ d["runId"] = self.run_id
156
+ if self.instance_id is not None:
157
+ d["instanceId"] = self.instance_id
158
+ if self.suite_id is not None:
159
+ d["suiteId"] = self.suite_id
160
+ if self.status is not None:
161
+ d["status"] = self.status
162
+ if self.progress is not None:
163
+ d["progress"] = self.progress
164
+ return d
165
+
166
+
167
+ # ---------------------------------------------------------------------------
168
+ # Base HTTP client (internal)
169
+ # ---------------------------------------------------------------------------
170
+
171
+
172
+ class _CbRestApiClient:
173
+ """Shared HTTP client base — authenticates via Bearer token."""
174
+
175
+ def __init__(self, base_url: str, auth_token: str) -> None:
176
+ self._base_url = base_url.rstrip("/")
177
+ self._session = requests.Session()
178
+ self._session.headers["Content-Type"] = "application/json"
179
+ self._session.headers["Authorization"] = f"Bearer {auth_token}"
180
+
181
+ def _post(self, path: str, json_data: Any = None) -> requests.Response:
182
+ url = self._base_url + path
183
+ logger.debug("REQ: POST %s", url)
184
+ response = self._session.post(url, json=json_data)
185
+ logger.debug("RES: HTTP %s", response.status_code)
186
+ return response
187
+
188
+
189
+ # ---------------------------------------------------------------------------
190
+ # V2 API
191
+ # ---------------------------------------------------------------------------
192
+
193
+
194
+ class RuntimeApiV2(_CbRestApiClient):
195
+ """
196
+ V2 API for posting test results and updating runtime status.
197
+
198
+ All methods follow a fire-and-forget pattern: errors are logged but not
199
+ re-raised, matching the behaviour of the Node.js implementation.
200
+ """
201
+
202
+ def __init__(self, api_host_url: str, api_token: str) -> None:
203
+ super().__init__(base_url=api_host_url, auth_token=api_token)
204
+
205
+ def add_instance_result(self, run_id: str, instance_id: str, result: Any) -> None:
206
+ """
207
+ Post the full test result for a run instance.
208
+
209
+ *result* may be a plain ``dict`` or a
210
+ :class:`~cloudbeat_common.models.TestResult` instance (serialised via
211
+ :func:`~cloudbeat_common.json_util.to_json`).
212
+ """
213
+ path = f"/testresult/run/{run_id}/instance/{instance_id}"
214
+ try:
215
+ if isinstance(result, dict):
216
+ payload = result
217
+ else:
218
+ import json
219
+ from cloudbeat_common.json_util import to_json
220
+ payload = json.loads(to_json(result))
221
+ self._post(path, json_data=payload)
222
+ except Exception as exc:
223
+ logger.error("Failed to post new test results: %s", exc)
224
+
225
+ def update_instance_status(self, status: RunStatusInfo) -> None:
226
+ """Update the status of a run instance."""
227
+ try:
228
+ self._post("/status", json_data=status.to_dict())
229
+ except Exception as exc:
230
+ logger.error("Failed to update run status: %s", exc)
231
+
232
+ def update_case_status(self, status: CaseStatusUpdateReq) -> None:
233
+ """Update the runtime status of a specific test case."""
234
+ path = f"/runtime/run/{status.run_id}/instance/{status.instance_id}/case/status"
235
+ try:
236
+ self._post(path, json_data=status.to_dict())
237
+ except Exception as exc:
238
+ logger.error("Failed to update case runtime status: %s", exc)
239
+
240
+ def update_suite_status(self, status: SuiteStatusUpdateReq) -> None:
241
+ """Update the runtime status of a specific test suite."""
242
+ path = f"/runtime/run/{status.run_id}/instance/{status.instance_id}/suite/status"
243
+ try:
244
+ self._post(path, json_data=status.to_dict())
245
+ except Exception as exc:
246
+ logger.error("Failed to update suite runtime status: %s", exc)
@@ -36,6 +36,7 @@ class CbConfig:
36
36
  api_endpoint_url = attrib(default=None)
37
37
  selenium_url = attrib(default=None)
38
38
  appium_url = attrib(default=None)
39
+ framework = attrib(default=None)
39
40
  capabilities = attrib(default=defaultdict(OrderedDict))
40
41
  options = attrib(default=defaultdict(OrderedDict))
41
42
  metadata = attrib(default=defaultdict(OrderedDict))
@@ -1,9 +1,14 @@
1
1
  import threading
2
+ import time
2
3
  from collections import OrderedDict, defaultdict
3
4
  import platform
5
+ from typing import Optional
4
6
 
5
7
  from cloudbeat_common.models import TestResult, CbConfig, SuiteResult, CaseResult, StepResult
6
8
  from cloudbeat_common.json_util import to_json
9
+ from cloudbeat_common.client import CaseStatusUpdateReq, RuntimeApiV2
10
+
11
+ _LANGUAGE_NAME = "python"
7
12
 
8
13
 
9
14
  class ThreadContext:
@@ -57,6 +62,10 @@ class CbTestReporter:
57
62
  def __init__(self, config: CbConfig):
58
63
  self._context = ThreadContext()
59
64
  self._config = config
65
+ self._api_client: Optional[RuntimeApiV2] = None
66
+
67
+ if config.api_endpoint_url and config.api_token:
68
+ self._api_client = RuntimeApiV2(config.api_endpoint_url, config.api_token)
60
69
 
61
70
  @classmethod
62
71
  def get_instance(cls) -> 'CbTestReporter':
@@ -111,14 +120,50 @@ class CbTestReporter:
111
120
  case_result.start(name, fqn)
112
121
  suite_result.add_case(case_result)
113
122
  self._context["case"] = case_result
123
+ if self._api_client:
124
+ caps = None
125
+ if case_result.context and case_result.context.get("browserName"):
126
+ caps = {"browserName": case_result.context["browserName"]}
127
+ self._api_client.update_case_status(CaseStatusUpdateReq(
128
+ timestamp=int(time.time() * 1000),
129
+ run_id=self._config.run_id,
130
+ instance_id=self._config.instance_id,
131
+ id=case_result.id,
132
+ fqn=case_result.fqn,
133
+ parent_fqn=suite_result.fqn,
134
+ parent_id=suite_result.id,
135
+ name=case_result.name,
136
+ start_time=case_result.start_time,
137
+ run_status="Running",
138
+ framework=self._config.framework,
139
+ language=_LANGUAGE_NAME,
140
+ capabilities=caps,
141
+ ))
114
142
  return case_result
115
143
 
116
144
  def end_case(self, status=None, failure=None):
117
145
  case_result: CaseResult = self._context.get("case")
118
146
  if case_result is None:
119
147
  return None
120
- # TODO: end started steps of the case
121
148
  case_result.end(status, failure)
149
+ if self._api_client:
150
+ suite_result: SuiteResult = self._context.get("suite")
151
+ self._api_client.update_case_status(CaseStatusUpdateReq(
152
+ timestamp=int(time.time() * 1000),
153
+ run_id=self._config.run_id,
154
+ instance_id=self._config.instance_id,
155
+ id=case_result.id,
156
+ fqn=case_result.fqn,
157
+ parent_fqn=suite_result.fqn if suite_result else None,
158
+ parent_id=suite_result.id if suite_result else None,
159
+ name=case_result.name,
160
+ start_time=case_result.start_time,
161
+ end_time=case_result.end_time,
162
+ run_status="Finished",
163
+ test_status=case_result.status,
164
+ framework=self._config.framework,
165
+ language=_LANGUAGE_NAME,
166
+ ))
122
167
  return case_result
123
168
 
124
169
  def start_case_hook(self, name):
@@ -1,8 +0,0 @@
1
- from cloudbeat_common.models import TestStatus
2
- from cloudbeat_common.reporter import CbTestReporter
3
-
4
-
5
- __all__ = [
6
- 'TestStatus',
7
- 'CbTestReporter'
8
- ]
@@ -1 +0,0 @@
1
- attrs>=16.0.0
@@ -1 +0,0 @@
1
- attr==0.3.2