Skip to content

Commit 68ec1c4

Browse files
committed
Merge branch 'dev' into 224-prevent-use-of-run-update-methods-when-the-run-instance-has-not-been-initialised
2 parents 112ecc6 + e37a58b commit 68ec1c4

31 files changed

+1128
-626
lines changed

.github/workflows/test_client_macos.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ jobs:
1515
runs-on: macos-latest
1616
steps:
1717
- uses: actions/checkout@v3
18-
- name: Set up Python 3.11
18+
- name: Set up Python 3.12
1919
uses: actions/setup-python@v3
2020
with:
21-
python-version: "3.11"
21+
python-version: "3.12"
2222
- name: Install dependencies
2323
run: |
2424
rm poetry.lock

.github/workflows/test_client_ubuntu.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ jobs:
1919
timeout-minutes: 20
2020
steps:
2121
- uses: actions/checkout@v3
22-
- name: Set up Python 3.11
22+
- name: Set up Python 3.12
2323
uses: actions/setup-python@v3
2424
with:
25-
python-version: "3.11"
25+
python-version: "3.12"
2626
- name: Install dependencies
2727
run: python -m pip install poetry
2828
- name: Test with pytest

.github/workflows/test_client_windows.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ jobs:
1515
timeout-minutes: 20
1616
steps:
1717
- uses: actions/checkout@v3
18-
- name: Set up Python 3.11
18+
- name: Set up Python 3.12
1919
uses: actions/setup-python@v3
2020
with:
21-
python-version: "3.11"
21+
python-version: "3.12"
2222
- name: Install dependencies
2323
run: |
2424
rm poetry.lock
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# This workflow will install Python dependencies, run tests and lint with a single version of Python
2+
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
3+
4+
name: Simvue Client (Python Versions)
5+
6+
on:
7+
push:
8+
branches: [ "main", "dev", "hotfix/update-ci" ]
9+
pull_request:
10+
branches: [ "main", "dev", "hotfix/update-ci" ]
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
build:
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 20
19+
strategy:
20+
matrix:
21+
python-version: ['3.9', '3.10', '3.11']
22+
steps:
23+
- uses: actions/checkout@v3
24+
- name: Set up Python ${{ matrix.python-version }}
25+
uses: actions/setup-python@v3
26+
with:
27+
python-version: "${{ matrix.python-version }}"
28+
- name: Install dependencies
29+
run: python -m pip install poetry
30+
- name: Test with pytest
31+
run: |
32+
export SIMVUE_URL=${{ secrets.SIMVUE_URL }}
33+
export SIMVUE_TOKEN=${{ secrets.SIMVUE_TOKEN }}
34+
poetry install --all-extras
35+
poetry run pytest --cov --cov-report=xml tests/unit/ tests/refactor/

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ repos:
2323
args: [--branch, main, --branch, dev]
2424
- id: check-added-large-files
2525
- repo: https://github.com/astral-sh/ruff-pre-commit
26-
rev: v0.4.3
26+
rev: v0.4.4
2727
hooks:
2828
- id: ruff
2929
args: [ --fix, --exit-non-zero-on-fix, "--ignore=C901" ]

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Collect metadata, metrics and artifacts from simulations, processing and AI/ML t
1212

1313
<div align="center">
1414
<a href="https://github.com/simvue-io/client/blob/main/LICENSE" target="_blank"><img src="https://img.shields.io/github/license/simvue-io/client"/></a>
15+
<img src="https://img.shields.io/badge/python-3.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue">
1516
<a href="https://pypi.org/project/simvue/" target="_blank"><img src="https://img.shields.io/pypi/v/simvue.svg"/></a>
1617
<a href="https://pepy.tech/project/simvue"><img src="https://static.pepy.tech/badge/simvue"/></a>
1718
<p></p>

poetry.lock

Lines changed: 191 additions & 150 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,11 @@ pandas = {version = "^2.2.0", optional = true}
4343
plotly = {version = "^5.18.0", optional = true}
4444
numpy = {version = "^1.26.3", optional = true}
4545
matplotlib = {version = "^3.8.2", optional = true}
46+
typing_extensions = { version = "^4.11.0", python = "<3.10" }
4647
toml = "^0.10.2"
4748
click = "^8.1.7"
49+
humanfriendly = "^10.0"
50+
tabulate = "^0.9.0"
4851

4952
[tool.poetry.extras]
5053
dataset = ["pandas", "numpy"]

simvue/client.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -258,10 +258,12 @@ def get_runs(
258258
alerts: bool = False,
259259
metadata: bool = False,
260260
format: typing.Literal["dict", "dataframe"] = "dict",
261+
count: int = 100,
262+
start_index: int = 0,
261263
) -> typing.Union[
262264
"DataFrame", list[dict[str, typing.Union[int, str, float, None]]], None
263265
]:
264-
"""Retrieve all runs matching filters
266+
"""Retrieve all runs matching filters.
265267
266268
Parameters
267269
----------
@@ -280,6 +282,10 @@ def get_runs(
280282
format : str ('dict' | 'dataframe'), optional
281283
the structure of the response, either a dictionary or a dataframe.
282284
Default is 'dict'. Pandas must be installed for 'dataframe'.
285+
count : int, optional
286+
maximum number of entries to return. Default is 100.
287+
start_index : int, optional
288+
the index from which to count entries. Default is 0.
283289
284290
Returns
285291
-------
@@ -301,6 +307,8 @@ def get_runs(
301307
"return_alerts": alerts,
302308
"return_system": system,
303309
"return_metadata": metadata,
310+
"count": count,
311+
"start": start_index,
304312
}
305313

306314
response = requests.get(
@@ -778,14 +786,21 @@ def get_folder(self, folder_id: str) -> typing.Optional[dict[str, typing.Any]]:
778786
return _folders[0]
779787

780788
def get_folders(
781-
self, filters: typing.Optional[list[str]] = None
789+
self,
790+
filters: typing.Optional[list[str]] = None,
791+
count: int = 100,
792+
start_index: int = 0,
782793
) -> list[dict[str, typing.Any]]:
783794
"""Retrieve folders from the server
784795
785796
Parameters
786797
----------
787798
filters : list[str] | None
788799
set of filters to apply to the search
800+
count : int, optional
801+
maximum number of entries to return. Default is 100.
802+
start_index : int, optional
803+
the index from which to count entries. Default is 0.
789804
790805
Returns
791806
-------
@@ -797,7 +812,11 @@ def get_folders(
797812
RuntimeError
798813
if there was a failure retrieving data from the server
799814
"""
800-
params: dict[str, str] = {"filters": json.dumps(filters or [])}
815+
params: dict[str, typing.Union[str, int]] = {
816+
"filters": json.dumps(filters or []),
817+
"count": count,
818+
"start": start_index,
819+
}
801820

802821
response: requests.Response = requests.get(
803822
f"{self._url}/api/folders", headers=self._headers, params=params
@@ -873,7 +892,7 @@ def _get_run_metrics_from_server(
873892
"xaxis": xaxis,
874893
"max_points": max_points,
875894
}
876-
print(params)
895+
877896
metrics_response: requests.Response = requests.get(
878897
f"{self._url}/api/metrics", headers=self._headers, params=params
879898
)

simvue/executor.py

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
__date__ = "2023-11-15"
1212

1313
import logging
14+
import multiprocessing.synchronize
15+
import sys
1416
import multiprocessing
1517
import os
1618
import subprocess
@@ -23,6 +25,48 @@
2325
logger = logging.getLogger(__name__)
2426

2527

28+
def _execute_process(
29+
proc_id: str,
30+
command: typing.List[str],
31+
runner_name: str,
32+
exit_status_dict: typing.Dict[str, int],
33+
std_err: typing.Dict[str, str],
34+
std_out: typing.Dict[str, str],
35+
run_on_exit: typing.Optional[typing.Callable[[int, int, str], None]],
36+
trigger: typing.Optional[multiprocessing.synchronize.Event],
37+
environment: typing.Optional[typing.Dict[str, str]],
38+
) -> None:
39+
with open(f"{runner_name}_{proc_id}.err", "w") as err:
40+
with open(f"{runner_name}_{proc_id}.out", "w") as out:
41+
_result = subprocess.Popen(
42+
command,
43+
stdout=out,
44+
stderr=err,
45+
universal_newlines=True,
46+
env=environment,
47+
)
48+
49+
_status_code = _result.wait()
50+
51+
with open(f"{runner_name}_{proc_id}.err") as err:
52+
std_err[proc_id] = err.read()
53+
54+
with open(f"{runner_name}_{proc_id}.out") as out:
55+
std_out[proc_id] = out.read()
56+
57+
exit_status_dict[proc_id] = _status_code
58+
59+
if run_on_exit:
60+
run_on_exit(
61+
status_code=exit_status_dict[proc_id],
62+
std_out=std_out[proc_id],
63+
std_err=std_err[proc_id],
64+
)
65+
66+
if trigger:
67+
trigger.set()
68+
69+
2670
class Executor:
2771
"""Command Line command executor
2872
@@ -62,6 +106,7 @@ def add_process(
62106
completion_callback: typing.Optional[
63107
typing.Callable[[int, str, str], None]
64108
] = None,
109+
completion_trigger: multiprocessing.synchronize.Event,
65110
**kwargs,
66111
) -> None:
67112
"""Add a process to be executed to the executor.
@@ -91,6 +136,8 @@ def callback_function(status_code: int, std_out: str, std_err: str) -> None:
91136
...
92137
```
93138
139+
Note `completion_callback` is not supported on Windows operating systems.
140+
94141
Parameters
95142
----------
96143
identifier : str
@@ -107,55 +154,24 @@ def callback_function(status_code: int, std_out: str, std_err: str) -> None:
107154
env : typing.Dict[str, str], optional
108155
environment variables for process
109156
completion_callback : typing.Callable | None, optional
110-
callback to run when process terminates
157+
callback to run when process terminates (not supported on Windows)
158+
completion_trigger : multiprocessing.Event | None, optional
159+
this trigger event is set when the processes completes
111160
"""
112161
_pos_args = list(args)
113162

163+
if sys.platform == "win32" and completion_callback:
164+
logger.warning(
165+
"Completion callback for 'add_process' may fail on Windows due to "
166+
"due to function pickling restrictions"
167+
)
168+
114169
if script:
115170
self._runner.save(filename=script, category="code")
116171

117172
if input_file:
118173
self._runner.save(filename=input_file, category="input")
119174

120-
def _exec_process(
121-
proc_id: str,
122-
command: typing.List[str],
123-
runner: "simvue.Run",
124-
exit_status_dict: typing.Dict[str, int],
125-
std_err: typing.Dict[str, str],
126-
std_out: typing.Dict[str, str],
127-
run_on_exit: typing.Optional[
128-
typing.Callable[[int, int, str], None]
129-
] = completion_callback,
130-
environment: typing.Optional[typing.Dict[str, str]] = env,
131-
) -> None:
132-
with open(f"{runner.name}_{proc_id}.err", "w") as err:
133-
with open(f"{runner.name}_{proc_id}.out", "w") as out:
134-
_result = subprocess.Popen(
135-
command,
136-
stdout=out,
137-
stderr=err,
138-
universal_newlines=True,
139-
env=environment,
140-
)
141-
142-
_status_code = _result.wait()
143-
144-
with open(f"{runner.name}_{proc_id}.err") as err:
145-
std_err[proc_id] = err.read()
146-
147-
with open(f"{runner.name}_{proc_id}.out") as out:
148-
std_out[proc_id] = out.read()
149-
150-
exit_status_dict[proc_id] = _status_code
151-
152-
if run_on_exit:
153-
run_on_exit(
154-
status_code=exit_status_dict[proc_id],
155-
std_out=std_out[proc_id],
156-
std_err=std_err[proc_id],
157-
)
158-
159175
_command: typing.List[str] = []
160176

161177
if executable:
@@ -189,14 +205,17 @@ def _exec_process(
189205
self._command_str[identifier] = " ".join(_command)
190206

191207
self._processes[identifier] = multiprocessing.Process(
192-
target=_exec_process,
208+
target=_execute_process,
193209
args=(
194210
identifier,
195211
_command,
196-
self._runner,
212+
self._runner.name,
197213
self._exit_codes,
198214
self._std_err,
199215
self._std_out,
216+
completion_callback,
217+
completion_trigger,
218+
env,
200219
),
201220
)
202221
logger.debug(f"Executing process: {' '.join(_command)}")
@@ -297,7 +316,8 @@ def _clear_cache_files(self) -> None:
297316
def wait_for_completion(self) -> None:
298317
"""Wait for all processes to finish then perform tidy up and upload"""
299318
for process in self._processes.values():
300-
process.join()
319+
if process.is_alive():
320+
process.join()
301321
self._log_events()
302322
self._save_output()
303323

0 commit comments

Comments
 (0)