Skip to content

Commit 66b7078

Browse files
authored
v0.2.0 (#7)
* new release * rename workflow * unit tests * fix unit tests * version bump * fix workflow
1 parent a7e16f2 commit 66b7078

37 files changed

+4961
-505
lines changed

.github/workflows/pypi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Publish Python 🐍 distribution 📦 to PyPI and TestPyPI
1+
name: Build and Publish
22

33
on: [push, workflow_dispatch]
44

.github/workflows/unit_tests.yml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: Unit tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
11+
code-format:
12+
runs-on: ubuntu-latest
13+
defaults:
14+
run:
15+
shell: bash -l {0}
16+
steps:
17+
18+
- name: Checkout Repository
19+
uses: actions/checkout@v4
20+
21+
- name: Set up Python
22+
uses: actions/setup-python@v5
23+
with:
24+
python-version: '3.10'
25+
cache: 'pip' # caching pip dependencies
26+
27+
- name: Pip install
28+
run: pip install black[jupyter]==24.2.0 blacken-docs
29+
30+
- name: Pip list
31+
run: pip list
32+
33+
- name: Code Formatting
34+
run: black . --check
35+
36+
browsergym-workarena-fast:
37+
runs-on: ubuntu-latest
38+
39+
defaults:
40+
run:
41+
shell: bash -l {0}
42+
43+
steps:
44+
45+
- name: Checkout Repository
46+
uses: actions/checkout@v4
47+
48+
- name: Set up Python
49+
uses: actions/setup-python@v5
50+
with:
51+
python-version: '3.10'
52+
cache: 'pip' # caching pip dependencies
53+
54+
- name: Pip install
55+
run: pip install -r requirements.txt
56+
57+
- name: Pip list
58+
run: pip list
59+
60+
- name: Install Playwright
61+
run: playwright install --with-deps
62+
63+
- name: Run non-slow browsergym-workarena Unit Tests
64+
env:
65+
SNOW_INSTANCE_URL: ${{ secrets.SNOW_INSTANCE_URL }}
66+
SNOW_INSTANCE_UNAME: ${{ secrets.SNOW_INSTANCE_UNAME }}
67+
SNOW_INSTANCE_PWD: ${{ secrets.SNOW_INSTANCE_PWD }}
68+
run: pytest -n 5 --durations=10 -m 'not slow and not pricy' --slowmo 1000 -v tests
69+
70+
browsergym-workarena-slow:
71+
runs-on: ubuntu-latest
72+
73+
defaults:
74+
run:
75+
shell: bash -l {0}
76+
77+
steps:
78+
79+
- name: Checkout Repository
80+
uses: actions/checkout@v4
81+
82+
- name: Set up Python
83+
uses: actions/setup-python@v5
84+
with:
85+
python-version: '3.10'
86+
cache: 'pip' # caching pip dependencies
87+
88+
- name: Pip install
89+
run: pip install -r requirements.txt
90+
91+
- name: Pip list
92+
run: pip list
93+
94+
- name: Install Playwright
95+
run: playwright install --with-deps
96+
97+
- name: Run slow browsergym-workarena Unit Tests
98+
env:
99+
SNOW_INSTANCE_URL: ${{ secrets.SNOW_INSTANCE_URL }}
100+
SNOW_INSTANCE_UNAME: ${{ secrets.SNOW_INSTANCE_UNAME }}
101+
SNOW_INSTANCE_PWD: ${{ secrets.SNOW_INSTANCE_PWD }}
102+
run: pytest -n 5 --durations=10 -m 'slow and not pricy' --slowmo 1000 -v tests

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.DS_store
2+
__pycache__/
3+
*.py[cod]

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
browsergym-core==0.1.0rc7
1+
browsergym-core==0.2.0
22
english-words>=2.0.1
3+
faker>=24.11.0
34
numpy>=1.14
45
requests>=2.31
56
tenacity>=8.2.3 # only used in cheat() -> move to tests?
7+
tqdm>=4.66.2

src/browsergym/workarena/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1-
__version__ = "0.1.0rc7"
1+
__version__ = "0.2.0"
22

33
from browsergym.core.registration import register_task
44

5+
from .tasks.dashboard import __TASKS__ as DASHBOARD_TASKS
56
from .tasks.form import __TASKS__ as FORM_TASKS
67
from .tasks.knowledge import __TASKS__ as KB_TASKS
78
from .tasks.list import __TASKS__ as LIST_TASKS
89
from .tasks.navigation import __TASKS__ as NAVIGATION_TASKS
910
from .tasks.service_catalog import __TASKS__ as SERVICE_CATALOG_TASKS
1011

1112
ALL_WORKARENA_TASKS = [
13+
*DASHBOARD_TASKS,
1214
*FORM_TASKS,
1315
*KB_TASKS,
1416
*LIST_TASKS,
@@ -21,5 +23,4 @@
2123
register_task(
2224
task.get_task_id(),
2325
task,
24-
kwargs={"viewport": {"width": 1280, "height": 720}, "timeout": 10000},
2526
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
Utility functions for UI themes
3+
4+
"""
5+
6+
from .utils import table_api_call
7+
8+
9+
def get_workarena_theme_variants(instance):
10+
"""
11+
Get the list of available WorkArena UI themes
12+
13+
Parameters:
14+
-----------
15+
instance: SNowInstance
16+
The ServiceNow instance to get the UI themes from
17+
18+
Returns:
19+
--------
20+
list[dict]
21+
The list of available WorkArena UI themes and their information
22+
23+
"""
24+
themes = table_api_call(
25+
instance=instance,
26+
table="m2m_theme_style",
27+
params={
28+
"sysparm_query": "style.type=variant",
29+
"sysparm_fields": "theme.name,theme.sys_id,style.name,style.sys_id",
30+
"sysparm_display_value": True,
31+
},
32+
method="GET",
33+
)["result"]
34+
themes = [t for t in themes if t["theme.name"] == "WorkArena"]
35+
return themes
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import random
2+
from faker import Faker
3+
import time
4+
5+
fake = Faker()
6+
7+
from ..instance import SNowInstance
8+
from .ui_themes import get_workarena_theme_variants
9+
from .utils import table_api_call
10+
11+
12+
def create_user(
13+
instance: SNowInstance,
14+
first_name: str = None,
15+
last_name: str = None,
16+
user_name: str = None,
17+
admin=True,
18+
) -> list[str]:
19+
"""
20+
Create a user with a random username and password with an admin role
21+
22+
Parameters:
23+
-----------
24+
first_name: str
25+
The first name of the user, defaults to a random first name
26+
last_name: str
27+
The last name of the user, defaults to a random last name
28+
user_name: str
29+
The user name of the user, defaults to first_name.last_name
30+
admin: bool
31+
Whether to give the user admin permissions
32+
33+
Returns:
34+
--------
35+
username, password, sys_id
36+
37+
"""
38+
user_idx = str(random.randint(1000, 9999))
39+
user_password = "aStrongPassword!"
40+
first_name = fake.first_name() if not first_name else first_name
41+
last_name = fake.last_name() if not last_name else last_name
42+
43+
# Create user
44+
user_data = {
45+
"user_name": f"{first_name}.{last_name}.{user_idx}" if not user_name else user_name,
46+
"first_name": first_name,
47+
"last_name": last_name,
48+
"email": f"{first_name}.{last_name}.{user_idx}@workarena.com".lower(),
49+
"user_password": user_password,
50+
"active": True,
51+
}
52+
user_params = {"sysparm_input_display_value": True}
53+
user_response = table_api_call(
54+
instance=instance, table="sys_user", json=user_data, params=user_params, method="POST"
55+
)["result"]
56+
user_name = user_response["user_name"]
57+
user_sys_id = user_response["sys_id"]
58+
59+
# Get admin role sys_id
60+
if admin:
61+
role_sys_id = table_api_call(
62+
instance=instance,
63+
table="sys_user_role",
64+
params={"sysparm_query": "name=admin", "sysparm_fields": "sys_id"},
65+
method="GET",
66+
)["result"][0]["sys_id"]
67+
68+
# Give admin permissions
69+
association_data = {"user": user_sys_id, "role": role_sys_id}
70+
table_api_call(
71+
instance=instance, table="sys_user_has_role", json=association_data, method="POST"
72+
)
73+
74+
# Randomly pick a UI theme and set it for the user
75+
themes = get_workarena_theme_variants(instance)
76+
theme = random.choice(themes)
77+
set_user_preference(
78+
instance, "glide.ui.polaris.theme.variant", theme["style.sys_id"], user=user_sys_id
79+
)
80+
81+
return user_name, user_password, user_sys_id
82+
83+
84+
def set_user_preference(instance: SNowInstance, key: str, value: str, user=None) -> dict:
85+
"""
86+
Set a user preference in the ServiceNow instance
87+
88+
Parameters:
89+
-----------
90+
key: str
91+
The name of the preference
92+
value: str
93+
The value of the preference
94+
user: str
95+
The sys_id of the user. If None, the preference will be set globally.
96+
97+
Returns:
98+
--------
99+
dict
100+
The preference that was set
101+
102+
"""
103+
if user is None:
104+
# make it global
105+
user = ""
106+
system = True
107+
else:
108+
system = False
109+
110+
# Try to get the preference's sys_id
111+
preference = table_api_call(
112+
instance=instance,
113+
table="sys_user_preference",
114+
params={"sysparm_query": f"name={key},user={user}", "sysparm_fields": "sys_id"},
115+
)["result"]
116+
117+
if not preference:
118+
# ... The preference key doesn't exist, create it
119+
pref_sysid = ""
120+
method = "POST"
121+
else:
122+
# ... The preference key exists, update it
123+
pref_sysid = "/" + preference[0]["sys_id"]
124+
method = "PUT"
125+
126+
property = table_api_call(
127+
instance=instance,
128+
table=f"sys_user_preference{pref_sysid}",
129+
method=method,
130+
json={
131+
"name": key,
132+
"value": value,
133+
"user": user,
134+
"system": system,
135+
"description": "Updated by WorkArena",
136+
},
137+
)["result"]
138+
139+
# Verify that the property was updated
140+
property["user"] = (
141+
property["user"].get("value") if isinstance(property["user"], dict) else property["user"]
142+
)
143+
assert (
144+
property["value"] == value
145+
), f"Error setting system property {key}, incorrect value {property['value']}, while expecting {value}."
146+
assert (
147+
property["user"] == user
148+
), f"Error setting system property {key}, incorrect user {property['user']}, while expecting {user}."
149+
assert (
150+
property["system"] == str(system).lower()
151+
), f"Error setting {key}, incorrect system {property['system']}, while expecting {system}."
152+
153+
return property

src/browsergym/workarena/api/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def table_column_info(instance: SNowInstance, table: str) -> dict:
8686

8787
# Clean column value choices
8888
for info in meta_info.values():
89-
if "choices" in info:
89+
if info.get("choices", None):
9090
info["choices"] = {c["value"]: c["label"] for c in info["choices"]}
9191

9292
# Query the sys_dictionnary table to find more info (e.g., is this column dependent on another)

0 commit comments

Comments
 (0)