Skip to content

Commit b5d3151

Browse files
committed
Merge branch 'main' into migrate-js-clients
2 parents 84a9fef + 992289f commit b5d3151

File tree

16 files changed

+390
-35
lines changed

16 files changed

+390
-35
lines changed

.github/workflows/python-build.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ jobs:
2323
with:
2424
python-version: '3.10'
2525
- name: Install Python build utilities
26-
run: pip install -r requirements/build.txt
26+
run: pip install -r requirements/dev.txt
2727
- name: Build
2828
run: python -m build
29+
- name: Test
30+
run: python -m pytest

.github/workflows/python-release.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ jobs:
2626
with:
2727
python-version: '3.10'
2828
- name: Install Python build utilities
29-
run: pip install -r requirements/build.txt
29+
run: pip install -r requirements/dev.txt
3030
- name: Build
3131
run: python -m build
32+
- name: Test
33+
run: python -m pytest
3234

3335
pypi-publish:
3436
runs-on: ubuntu-latest

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ This repository contains the SDKs for Sauce Labs Visual.
66

77
- [C#](./visual-dotnet)
88
- [Java](./visual-java)
9+
- [Python](./visual-python)

visual-dotnet/SauceLabs.Visual/BuildFactory.cs

+10
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ private static async Task<VisualBuild> FindBuildById(VisualApi api, string build
9393
try
9494
{
9595
var build = (await api.Build(buildId)).EnsureValidResponse().Result;
96+
if (build == null)
97+
{
98+
throw new VisualClientException($@"build {buildId} was not found");
99+
}
100+
96101
return new VisualBuild(build.Id, build.Url, build.Mode);
97102
}
98103
catch (VisualClientException)
@@ -113,6 +118,11 @@ private static async Task<VisualBuild> FindBuildById(VisualApi api, string build
113118
try
114119
{
115120
var build = (await api.BuildByCustomId(customId)).EnsureValidResponse().Result;
121+
if (build == null)
122+
{
123+
throw new VisualClientException($@"build identified by {customId} was not found");
124+
}
125+
116126
return new VisualBuild(build.Id, build.Url, build.Mode);
117127
}
118128
catch (VisualClientException)

visual-dotnet/SauceLabs.Visual/SauceLabs.Visual.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<language>en-US</language>
88
<NeutralLanguage>en</NeutralLanguage>
99
<PackageId>SauceLabs.Visual</PackageId>
10-
<Version>0.3.0</Version>
10+
<Version>0.3.1</Version>
1111
<Title>Sauce Labs Visual Binding</Title>
1212
<PackageTags>saucelabs sauce labs visual testing screenshot capture dom</PackageTags>
1313
<RepositoryUrl>https://github.com/saucelabs/visual-sdks</RepositoryUrl>

visual-python/.bumpversion.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tool.bumpversion]
2-
current_version = "0.0.6"
2+
current_version = "0.0.8"
33
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
44
serialize = ["{major}.{minor}.{patch}"]
55
search = "{current_version}"

visual-python/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
# Sauce Labs Visual for Python
22

33
Sauce Labs Visual for Python exposes Sauce Labs Visual Testing for your Python project with Selenium.
4+
5+
## Installation & Usage
6+
7+
View installation and usage instructions on the [Sauce Docs website](https://docs.saucelabs.com/visual-testing/integrations/python/).

visual-python/pyproject.toml

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
[project]
22
name = "saucelabs_visual"
3-
version = "0.0.6"
3+
version = "0.0.8"
44
description = "Python bindings for Sauce Labs Visual"
55
dependencies=[
66
"requests",
77
"requests-toolbelt",
88
"gql",
9+
"tabulate",
910
]
1011
readme = "README.md"
1112

@@ -24,3 +25,9 @@ include = [
2425
[project.urls]
2526
Homepage = "https://github.com/saucelabs/visual-sdks/tree/main/visual-python"
2627
Issues = "https://github.com/saucelabs/visual-sdks/issues"
28+
29+
[tool.pytest.ini_options]
30+
pythonpath = "src"
31+
addopts = [
32+
"--import-mode=importlib",
33+
]

visual-python/requirements/dev.txt

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
-r build.txt
44
robotframework==7.0.0
55
robotframework-seleniumlibrary==6.2.0
6+
pytest==8.1.1
7+
coverage==7.4.4

visual-python/requirements/user.txt

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
gql==3.5.0
33
requests>=2.1.0
44
requests-toolbelt>=1.0.0
5+
tabulate>=0.9.0

visual-python/src/saucelabs_visual/client.py

+88-14
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1+
from datetime import datetime, timedelta
12
from os import environ
3+
from time import sleep
24
from typing import List, Union
35

46
from gql import Client, gql
57
from gql.transport.requests import RequestsHTTPTransport
68
from requests.auth import HTTPBasicAuth
79

810
from saucelabs_visual.regions import Region
9-
from saucelabs_visual.typing import IgnoreRegion, FullPageConfig
11+
from saucelabs_visual.typing import IgnoreRegion, FullPageConfig, DiffingMethod, BuildStatus
1012

1113

1214
class SauceLabsVisual:
1315
client: Client = None
1416
build_id: Union[str, None] = None
17+
build_url: Union[str, None] = None
1518
meta_cache: dict = {}
19+
region: Region = None
1620

1721
def __init__(self):
1822
self._create_client()
@@ -27,7 +31,8 @@ def _create_client(self):
2731
'`SAUCE_USERNAME` and `SAUCE_ACCESS_KEY` environment variables.'
2832
)
2933

30-
region_url = Region.from_name(environ.get("SAUCE_REGION") or 'us-west-1').graphql_endpoint
34+
self.region = Region.from_name(environ.get("SAUCE_REGION") or 'us-west-1')
35+
region_url = self.region.graphql_endpoint
3136
transport = RequestsHTTPTransport(url=region_url, auth=HTTPBasicAuth(username, access_key))
3237
self.client = Client(transport=transport, execute_timeout=90)
3338

@@ -36,12 +41,12 @@ def get_client(self) -> Client:
3641

3742
def create_build(
3843
self,
39-
name: str = environ.get('SAUCE_VISUAL_BUILD_NAME') or None,
40-
project: str = environ.get('SAUCE_VISUAL_PROJECT') or None,
41-
branch: str = environ.get('SAUCE_VISUAL_BRANCH') or None,
42-
default_branch: str = environ.get('SAUCE_VISUAL_DEFAULT_BRANCH') or None,
43-
custom_id: str = environ.get('SAUCE_VISUAL_CUSTOM_ID') or None,
44-
keep_alive_timeout: int = None
44+
name: Union[str, None] = environ.get('SAUCE_VISUAL_BUILD_NAME'),
45+
project: Union[str, None] = environ.get('SAUCE_VISUAL_PROJECT'),
46+
branch: Union[str, None] = environ.get('SAUCE_VISUAL_BRANCH'),
47+
default_branch: Union[str, None] = environ.get('SAUCE_VISUAL_DEFAULT_BRANCH'),
48+
custom_id: Union[str, None] = environ.get('SAUCE_VISUAL_CUSTOM_ID'),
49+
keep_alive_timeout: Union[int, None] = None
4550
):
4651
query = gql(
4752
# language=GraphQL
@@ -78,6 +83,7 @@ def create_build(
7883
}
7984
build = self.client.execute(query, variable_values=values)
8085
self.build_id = build['createBuild']['id']
86+
self.build_url = build['createBuild']['url']
8187
return build
8288

8389
def finish_build(self):
@@ -95,7 +101,6 @@ def finish_build(self):
95101
)
96102
values = {"id": self.build_id}
97103
self.meta_cache.clear()
98-
self.build_id = None
99104
return self.client.execute(query, variable_values=values)
100105

101106
def get_selenium_metadata(self, session_id: str) -> str:
@@ -137,12 +142,13 @@ def create_snapshot_from_webdriver(
137142
self,
138143
name: str,
139144
session_id: str,
140-
test_name: str = None,
141-
suite_name: str = None,
145+
test_name: Union[str, None] = None,
146+
suite_name: Union[str, None] = None,
142147
capture_dom: bool = False,
143-
clip_selector: str = None,
144-
ignore_regions: List[IgnoreRegion] = None,
145-
full_page_config: FullPageConfig = None,
148+
clip_selector: Union[str, None] = None,
149+
ignore_regions: Union[List[IgnoreRegion], None] = None,
150+
full_page_config: Union[FullPageConfig, None] = None,
151+
diffing_method: DiffingMethod = DiffingMethod.SIMPLE,
146152
):
147153
query = gql(
148154
# language=GraphQL
@@ -158,6 +164,7 @@ def create_snapshot_from_webdriver(
158164
$clipSelector: String,
159165
$ignoreRegions: [RegionIn!],
160166
$fullPageConfig: FullPageConfigIn,
167+
$diffingMethod: DiffingMethod,
161168
) {
162169
createSnapshotFromWebDriver(input: {
163170
name: $name,
@@ -170,6 +177,7 @@ def create_snapshot_from_webdriver(
170177
clipSelector: $clipSelector,
171178
ignoreRegions: $ignoreRegions,
172179
fullPageConfig: $fullPageConfig,
180+
diffingMethod: $diffingMethod,
173181
}){
174182
id
175183
}
@@ -191,5 +199,71 @@ def create_snapshot_from_webdriver(
191199
"delayAfterScrollMs": full_page_config.get('delay_after_scroll_ms'),
192200
"hideAfterFirstScroll": full_page_config.get('hide_after_first_scroll'),
193201
} if full_page_config is not None else None,
202+
"diffingMethod": (diffing_method or DiffingMethod.SIMPLE).value,
194203
}
195204
return self.client.execute(query, variable_values=values)
205+
206+
def get_build_status(
207+
self,
208+
wait: bool = True,
209+
timeout: int = 60,
210+
):
211+
query = gql(
212+
# language=GraphQL
213+
"""
214+
query buildStatus($buildId: UUID!) {
215+
result: build(id: $buildId) {
216+
id
217+
name
218+
url
219+
mode
220+
status
221+
unapprovedCount: diffCountExtended(input: { status: UNAPPROVED })
222+
approvedCount: diffCountExtended(input: { status: APPROVED })
223+
rejectedCount: diffCountExtended(input: { status: REJECTED })
224+
equalCount: diffCountExtended(input: { status: EQUAL })
225+
erroredCount: diffCountExtended(input: { status: ERRORED })
226+
queuedCount: diffCountExtended(input: { status: QUEUED })
227+
}
228+
}
229+
"""
230+
)
231+
values = {
232+
"buildId": self.build_id,
233+
}
234+
235+
if not wait:
236+
return self.client.execute(query, variable_values=values)
237+
238+
cutoff_time = datetime.now() + timedelta(seconds=timeout)
239+
build = None
240+
result = None
241+
242+
while build is None and datetime.now() < cutoff_time:
243+
result = self.client.execute(query, variable_values=values)
244+
245+
if result['result'] is None:
246+
raise ValueError(
247+
'Sauce Visual build has been deleted or you do not have access to view it.'
248+
)
249+
250+
if result['result']['status'] != BuildStatus.RUNNING.value:
251+
build = result
252+
else:
253+
sleep(min(10, timeout))
254+
255+
# Return the successful build if available, else, the last run
256+
return build if build is not None else result
257+
258+
def get_build_link(self) -> str:
259+
"""
260+
Get the dashboard build link for viewing the current build on Sauce Labs.
261+
:return:
262+
"""
263+
return self.build_url
264+
265+
def get_build_created_link(self) -> str:
266+
return f'Sauce Labs Visual build created:\t{self.get_build_link()}'
267+
268+
def get_build_finished_link(self) -> str:
269+
return f'Sauce Labs Visual build finished:\t{self.get_build_link()}'

0 commit comments

Comments
 (0)