Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ajout des fonctions et REST endpoints pour gérer les dictionnaires job-utilisateur #186

Merged
merged 24 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
aa20f4e
Add functions to get, set and delete job-user props.
notoraptor Mar 24, 2024
5a8c319
Add routes to manage job-user dicts.
notoraptor Mar 25, 2024
ed6ea1b
Add clockwotk tools functions to call REST APIs related to job-user p…
notoraptor Mar 25, 2024
db78f6d
Set job_id as a string in mongodb collection `job_user_props`
notoraptor Apr 2, 2024
64fc63b
Do not print exceptions in error message returned by REST API.
notoraptor Apr 2, 2024
e89a5df
Update REST API error messages.
notoraptor Apr 2, 2024
1955a3a
Make set_user_props() return new updated props.
notoraptor Apr 2, 2024
cb0d2fd
Make REST API return nothing when deleting user props.
notoraptor Apr 2, 2024
7bfc643
Remove useless new pytest fixtures, just use already available app an…
notoraptor Apr 3, 2024
a626be7
Remove unused imports.
notoraptor Apr 3, 2024
5d29008
Make API endpoints set_user_props and delete_user_props as JSON PUT r…
notoraptor Apr 3, 2024
ec1c4b4
Replace `docker-compose` with `docker compose`
notoraptor Apr 3, 2024
11e3d78
Make sure to send JSON requests in REST tests for set_user_props and …
notoraptor Apr 3, 2024
db99cd5
In fake_data, create user props with student01 instead of student00
notoraptor Apr 5, 2024
6914b2f
Set send_json=True by default for clockwork tool method _request().
notoraptor Apr 5, 2024
4426490
Fix frontend tests for user props: login using student01
notoraptor Apr 5, 2024
cf4e345
Remove irrelevant script: store_huge_fake_data_in_db
notoraptor Apr 8, 2024
71fca56
Make `mila_email_username` mandatory in Python functions get/set/dele…
notoraptor Apr 9, 2024
041834e
Fix comments.
notoraptor Apr 17, 2024
73477ee
Remove useless str conversions on cluster_name.
notoraptor Apr 17, 2024
a81a073
Simplify tests for user props in clockwork_tools.
notoraptor Apr 17, 2024
87a285a
Use a specific exception for huge user props error, and return except…
notoraptor Apr 17, 2024
b28ebb0
Import user props size limit in tests, instead of hardcoding x1again.
notoraptor Apr 18, 2024
63a8503
In unit tests, make huge prop a little larger than size limit.
notoraptor Apr 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci_frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ jobs:
- name: Launch frontend tests
run: |
. ./env.sh
docker-compose run clockwork_dev scripts/launch_frontend_tests_in_clockwork_dev.sh
docker compose run clockwork_dev scripts/launch_frontend_tests_in_clockwork_dev.sh
2 changes: 1 addition & 1 deletion clockwork_frontend_test/test_jobs_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,7 @@ def test_filter_by_job_array(page: Page):

def test_filter_by_job_user_props(page: Page):
# Login
page.goto(f"{BASE_URL}/login/testing?user_id=student00@mila.quebec")
page.goto(f"{BASE_URL}/login/testing?user_id=student01@mila.quebec")
# Go to settings.
page.goto(f"{BASE_URL}/settings/")
radio_job_user_props = page.locator("input#jobs_list_job_user_props_toggle")
Expand Down
14 changes: 13 additions & 1 deletion clockwork_frontend_test/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,19 @@ def test_jobs_search_columns(page: Page):

def test_jobs_search_column_job_user_props(page: Page):
# Login
page.goto(f"{BASE_URL}/login/[email protected]")
page.goto(f"{BASE_URL}/login/[email protected]")

# Change language in settings
page.goto(f"{BASE_URL}/settings/")
# Get language select.
select = page.locator("select#language_selection")
# Check default language is French.
expect(select).to_have_value("fr")
# Switch to english.
select.select_option("en")
# Check english is selected.
expect(select).to_have_value("en")

# Check default jobs search columns.
page.goto(f"{BASE_URL}/jobs/search")
headers = page.locator("table#search_table thead tr th")
Expand Down
72 changes: 67 additions & 5 deletions clockwork_tools/clockwork_tools/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def _get_headers(self):
encoded_s = str(encoded_bytes, "utf-8")
return {"Authorization": f"Basic {encoded_s}"}

def _request(self, endpoint, params, method="GET"):
def _request(self, endpoint, params, method="GET", send_json=True):
"""Helper method for REST API calls.

Internal helper method to make the calls to the REST API endpoints
Expand All @@ -69,6 +69,9 @@ def _request(self, endpoint, params, method="GET"):
Args:
endpoint (str): The REST endpoint, omitting the server address.
params (dict): Arguments to be provided to the REST endpoint.
send_json (bool): Optional. If True and if method is PUT,
then request will be sent as a JSON request.


Returns:
Depends on the call made.
Expand All @@ -86,9 +89,14 @@ def _request(self, endpoint, params, method="GET"):
complete_address, params=params, headers=self._get_headers()
)
elif method == "PUT":
response = requests.put(
complete_address, data=params, headers=self._get_headers()
)
if send_json:
headers = self._get_headers()
headers["Content-type"] = "application/json"
response = requests.put(complete_address, json=params, headers=headers)
else:
response = requests.put(
complete_address, data=params, headers=self._get_headers()
)

# Check code instead and raise exception if it's the wrong one.
if response.status_code == 200:
Expand Down Expand Up @@ -154,6 +162,60 @@ def jobs_one(self, job_id: str = None, cluster_name: str = None) -> dict[str, an
params[k] = a
return self._request(endpoint, params)

def get_user_props(self, job_id: str, cluster_name: str) -> dict[str, any]:
"""REST call to api/v1/clusters/jobs/user_props/get.

Call for get_user_props().

Args:
job_id (str): ID of job to retrieve props
cluster_name (str): Name of job cluster

Returns:
dict[any,any]: props.
"""
endpoint = "api/v1/clusters/jobs/user_props/get"
params = {"job_id": job_id, "cluster_name": cluster_name}
return self._request(endpoint, params)

def set_user_props(
self, job_id: str, cluster_name: str, updates: dict
) -> dict[str, any]:
"""REST call to api/v1/clusters/jobs/user_props/set.

Call for set_user_props().

Args:
job_id (str): ID of job to retrieve props
cluster_name (str): Name of job cluster
updates (dict): Dict of props to update.

Returns:
dict[any,any]: Returns the updated props.
"""
endpoint = "api/v1/clusters/jobs/user_props/set"
params = {"job_id": job_id, "cluster_name": cluster_name, "updates": updates}
return self._request(endpoint, params, method="PUT")

def delete_user_props(
self, job_id: str, cluster_name: str, keys: str | list
) -> dict[str, any]:
"""REST call to api/v1/clusters/jobs/user_props/delete.

Call for delete_user_props().

Args:
job_id (str): ID of job to retrieve props
cluster_name (str): Name of job cluster
keys (list): List of keys to delete.

Returns:
dict[any,any]: Returns the updated props.
"""
endpoint = "api/v1/clusters/jobs/user_props/delete"
params = {"job_id": job_id, "cluster_name": cluster_name, "keys": keys}
return self._request(endpoint, params, method="PUT")

def jobs_user_dict_update(
self, job_id: str = None, cluster_name: str = None, update_pairs: dict = {}
) -> dict[str, any]:
Expand Down Expand Up @@ -191,7 +253,7 @@ def jobs_user_dict_update(
# Due to current constraints, we have to pass "update_pairs"
# as a string representing a structure in json.
params["update_pairs"] = json.dumps(update_pairs)
return self._request(endpoint, params, method="PUT")
return self._request(endpoint, params, method="PUT", send_json=False)

def nodes_list(self, cluster_name: str = None) -> list[dict[str, any]]:
"""REST call to api/v1/clusters/nodes/list.
Expand Down
97 changes: 97 additions & 0 deletions clockwork_tools_test/test_mt_job_user_props.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import random


def _get_test_user_props(mtclient, fake_data):
# Find an entry that's associated with the user that's currently logged in.
# This becomes the ground truth against which we compare the retrieved user props.
LD_candidates = [
D_job_user_props_entry
for D_job_user_props_entry in fake_data["job_user_props"]
if (
D_job_user_props_entry["mila_email_username"] == mtclient.email
and len(D_job_user_props_entry["props"]) > 0
)
]
assert (
len(LD_candidates) > 0
), "There should be at least one job_user_props entry for the user that's currently logged in."
D_job_user_props_entry = random.choice(LD_candidates)

job_id = D_job_user_props_entry["job_id"]
cluster_name = D_job_user_props_entry["cluster_name"]
original_props = D_job_user_props_entry["props"]
return job_id, cluster_name, original_props


def test_cw_tools_get_user_props(mtclient, fake_data):
job_id, cluster_name, original_props = _get_test_user_props(mtclient, fake_data)
props = mtclient.get_user_props(job_id, cluster_name)
assert original_props == props


def test_cw_tools_set_and_delete_user_props(mtclient, fake_data):
job_id, cluster_name, original_props = _get_test_user_props(mtclient, fake_data)

assert original_props
# Test we can replace existing user prop value.
an_existing_prop_name = next(iter(original_props.keys()))
a_new_value = "a totally new value"
# We check new value is not already associated to existing prop name
assert original_props[an_existing_prop_name] != a_new_value
# Set new value
props = mtclient.set_user_props(
job_id, cluster_name, {an_existing_prop_name: a_new_value}
)
# Check set returns same value as get
assert props == mtclient.get_user_props(job_id, cluster_name)
# Check returned props
assert len(props) == len(original_props)
# Props should be original props, except that `an_existing_prop_name` is now associated to `a_new_value`.
assert props == {
an_existing_prop_name: a_new_value,
**{
key: value
for key, value in original_props.items()
if key != an_existing_prop_name
},
}

# Get back to original props for tests below
props = mtclient.set_user_props(job_id, cluster_name, original_props)
# Check set and get
assert props == mtclient.get_user_props(job_id, cluster_name) == original_props

# Define new props to add
new_props = {"a new name": "a new prop", "aa": "aa", "bb": "bb", "cc": "cc"}
gyom marked this conversation as resolved.
Show resolved Hide resolved
# We check new props are not already in original props
for new_prop_name in new_props.keys():
assert new_prop_name not in original_props

# Test set new props
props = mtclient.set_user_props(job_id, cluster_name, new_props)
assert len(props) == len(original_props) + len(new_props)
assert props == {**original_props, **new_props}

# Test delete a prop using a str
assert mtclient.delete_user_props(job_id, cluster_name, "aa") == ""
props = mtclient.get_user_props(job_id, cluster_name)
assert len(props) == len(original_props) + len(new_props) - 1
assert props == {
**original_props,
"a new name": "a new prop",
"bb": "bb",
"cc": "cc",
}

# Test delete a prop using a list with 1 string
assert mtclient.delete_user_props(job_id, cluster_name, ["a new name"]) == ""
props = mtclient.get_user_props(job_id, cluster_name)
assert len(props) == len(original_props) + len(new_props) - 2
assert props == {**original_props, "bb": "bb", "cc": "cc"}

# Test delete multiple props using a list with many strings
assert mtclient.delete_user_props(job_id, cluster_name, ["bb", "cc"]) == ""
props = mtclient.get_user_props(job_id, cluster_name)
assert props == original_props

# As props are not original_props, no need to clean-up code here!
Loading
Loading