dpdispatcher 0.6.9__py3-none-any.whl → 0.6.11__py3-none-any.whl
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.
Potentially problematic release.
This version of dpdispatcher might be problematic. Click here for more details.
- dpdispatcher/_version.py +16 -3
- dpdispatcher/contexts/openapi_context.py +68 -11
- dpdispatcher/contexts/ssh_context.py +0 -2
- dpdispatcher/machines/openapi.py +38 -8
- dpdispatcher/machines/slurm.py +1 -1
- {dpdispatcher-0.6.9.dist-info → dpdispatcher-0.6.11.dist-info}/METADATA +1 -1
- {dpdispatcher-0.6.9.dist-info → dpdispatcher-0.6.11.dist-info}/RECORD +11 -11
- {dpdispatcher-0.6.9.dist-info → dpdispatcher-0.6.11.dist-info}/WHEEL +0 -0
- {dpdispatcher-0.6.9.dist-info → dpdispatcher-0.6.11.dist-info}/entry_points.txt +0 -0
- {dpdispatcher-0.6.9.dist-info → dpdispatcher-0.6.11.dist-info}/licenses/LICENSE +0 -0
- {dpdispatcher-0.6.9.dist-info → dpdispatcher-0.6.11.dist-info}/top_level.txt +0 -0
dpdispatcher/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.6.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 6,
|
|
31
|
+
__version__ = version = '0.6.11'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 6, 11)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -1,18 +1,20 @@
|
|
|
1
|
+
import glob
|
|
1
2
|
import os
|
|
2
3
|
import shutil
|
|
3
4
|
import uuid
|
|
5
|
+
from zipfile import ZipFile
|
|
4
6
|
|
|
5
7
|
import tqdm
|
|
6
8
|
|
|
7
9
|
try:
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
|
|
11
|
-
from bohriumsdk.util import Util
|
|
12
|
-
except ModuleNotFoundError:
|
|
10
|
+
from bohrium import Bohrium
|
|
11
|
+
from bohrium.resources import Job, Tiefblue
|
|
12
|
+
except ModuleNotFoundError as e:
|
|
13
13
|
found_bohriumsdk = False
|
|
14
|
+
import_bohrium_error = e
|
|
14
15
|
else:
|
|
15
16
|
found_bohriumsdk = True
|
|
17
|
+
import_bohrium_error = None
|
|
16
18
|
|
|
17
19
|
from dpdispatcher.base_context import BaseContext
|
|
18
20
|
from dpdispatcher.dlog import dlog
|
|
@@ -23,6 +25,36 @@ DP_CLOUD_SERVER_HOME_DIR = os.path.join(
|
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
|
|
28
|
+
def unzip_file(zip_file, out_dir="./"):
|
|
29
|
+
obj = ZipFile(zip_file, "r")
|
|
30
|
+
for item in obj.namelist():
|
|
31
|
+
obj.extract(item, out_dir)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def zip_file_list(root_path, zip_filename, file_list=[]):
|
|
35
|
+
out_zip_file = os.path.join(root_path, zip_filename)
|
|
36
|
+
# print('debug: file_list', file_list)
|
|
37
|
+
zip_obj = ZipFile(out_zip_file, "w")
|
|
38
|
+
for f in file_list:
|
|
39
|
+
matched_files = os.path.join(root_path, f)
|
|
40
|
+
for ii in glob.glob(matched_files):
|
|
41
|
+
# print('debug: matched_files:ii', ii)
|
|
42
|
+
if os.path.isdir(ii):
|
|
43
|
+
arcname = os.path.relpath(ii, start=root_path)
|
|
44
|
+
zip_obj.write(ii, arcname)
|
|
45
|
+
for root, dirs, files in os.walk(ii):
|
|
46
|
+
for file in files:
|
|
47
|
+
filename = os.path.join(root, file)
|
|
48
|
+
arcname = os.path.relpath(filename, start=root_path)
|
|
49
|
+
# print('debug: filename:arcname:root_path', filename, arcname, root_path)
|
|
50
|
+
zip_obj.write(filename, arcname)
|
|
51
|
+
else:
|
|
52
|
+
arcname = os.path.relpath(ii, start=root_path)
|
|
53
|
+
zip_obj.write(ii, arcname)
|
|
54
|
+
zip_obj.close()
|
|
55
|
+
return out_zip_file
|
|
56
|
+
|
|
57
|
+
|
|
26
58
|
class OpenAPIContext(BaseContext):
|
|
27
59
|
def __init__(
|
|
28
60
|
self,
|
|
@@ -35,16 +67,41 @@ class OpenAPIContext(BaseContext):
|
|
|
35
67
|
if not found_bohriumsdk:
|
|
36
68
|
raise ModuleNotFoundError(
|
|
37
69
|
"bohriumsdk not installed. Install dpdispatcher with `pip install dpdispatcher[bohrium]`"
|
|
38
|
-
)
|
|
70
|
+
) from import_bohrium_error
|
|
39
71
|
self.init_local_root = local_root
|
|
40
72
|
self.init_remote_root = remote_root
|
|
41
73
|
self.temp_local_root = os.path.abspath(local_root)
|
|
42
74
|
self.remote_profile = remote_profile
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
access_key = (
|
|
76
|
+
remote_profile.get("access_key", None)
|
|
77
|
+
or os.getenv("BOHRIUM_ACCESS_KEY", None)
|
|
78
|
+
or os.getenv("ACCESS_KEY", None)
|
|
79
|
+
)
|
|
80
|
+
project_id = (
|
|
81
|
+
remote_profile.get("project_id", None)
|
|
82
|
+
or os.getenv("BOHRIUM_PROJECT_ID", None)
|
|
83
|
+
or os.getenv("PROJECT_ID", None)
|
|
84
|
+
)
|
|
85
|
+
app_key = (
|
|
86
|
+
remote_profile.get("app_key", None)
|
|
87
|
+
or os.getenv("BOHRIUM_APP_KEY", None)
|
|
88
|
+
or os.getenv("APP_KEY", None)
|
|
89
|
+
)
|
|
90
|
+
if access_key is None:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
"remote_profile must contain 'access_key' or set environment variable 'BOHRIUM_ACCESS_KEY'"
|
|
93
|
+
)
|
|
94
|
+
if project_id is None:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"remote_profile must contain 'project_id' or set environment variable 'BOHRIUM_PROJECT_ID'"
|
|
97
|
+
)
|
|
98
|
+
self.client = Bohrium(
|
|
99
|
+
access_key=access_key, project_id=project_id, app_key=app_key
|
|
100
|
+
)
|
|
101
|
+
self.storage = Tiefblue()
|
|
45
102
|
self.job = Job(client=self.client)
|
|
46
|
-
self.util = Util()
|
|
47
103
|
self.jgid = None
|
|
104
|
+
os.makedirs(DP_CLOUD_SERVER_HOME_DIR, exist_ok=True)
|
|
48
105
|
|
|
49
106
|
@classmethod
|
|
50
107
|
def load_from_dict(cls, context_dict):
|
|
@@ -97,7 +154,7 @@ class OpenAPIContext(BaseContext):
|
|
|
97
154
|
for file in task.forward_files:
|
|
98
155
|
upload_file_list.append(os.path.join(task.task_work_path, file))
|
|
99
156
|
|
|
100
|
-
upload_zip =
|
|
157
|
+
upload_zip = zip_file_list(
|
|
101
158
|
self.local_root, zip_task_file, file_list=upload_file_list
|
|
102
159
|
)
|
|
103
160
|
project_id = self.remote_profile.get("project_id", 0)
|
|
@@ -189,7 +246,7 @@ class OpenAPIContext(BaseContext):
|
|
|
189
246
|
):
|
|
190
247
|
continue
|
|
191
248
|
self.storage.download_from_url(info["resultUrl"], target_result_zip)
|
|
192
|
-
|
|
249
|
+
unzip_file(target_result_zip, out_dir=self.local_root)
|
|
193
250
|
self._backup(self.local_root, target_result_zip)
|
|
194
251
|
self._clean_backup(
|
|
195
252
|
self.local_root, keep_backup=self.remote_profile.get("keep_backup", True)
|
|
@@ -163,7 +163,6 @@ class SSHSession:
|
|
|
163
163
|
if os.path.exists(key_path):
|
|
164
164
|
for pkey_class in (
|
|
165
165
|
paramiko.RSAKey,
|
|
166
|
-
paramiko.DSSKey,
|
|
167
166
|
paramiko.ECDSAKey,
|
|
168
167
|
paramiko.Ed25519Key,
|
|
169
168
|
):
|
|
@@ -181,7 +180,6 @@ class SSHSession:
|
|
|
181
180
|
elif self.look_for_keys:
|
|
182
181
|
for keytype, name in [
|
|
183
182
|
(paramiko.RSAKey, "rsa"),
|
|
184
|
-
(paramiko.DSSKey, "dsa"),
|
|
185
183
|
(paramiko.ECDSAKey, "ecdsa"),
|
|
186
184
|
(paramiko.Ed25519Key, "ed25519"),
|
|
187
185
|
]:
|
dpdispatcher/machines/openapi.py
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shutil
|
|
3
3
|
import time
|
|
4
|
+
from zipfile import ZipFile
|
|
4
5
|
|
|
5
6
|
from dpdispatcher.utils.utils import customized_script_header_template
|
|
6
7
|
|
|
7
8
|
try:
|
|
8
|
-
from
|
|
9
|
-
from
|
|
10
|
-
from bohriumsdk.storage import Storage
|
|
11
|
-
from bohriumsdk.util import Util
|
|
9
|
+
from bohrium import Bohrium
|
|
10
|
+
from bohrium.resources import Job, Tiefblue
|
|
12
11
|
except ModuleNotFoundError:
|
|
13
12
|
found_bohriumsdk = False
|
|
14
13
|
else:
|
|
@@ -23,6 +22,12 @@ shell_script_header_template = """
|
|
|
23
22
|
"""
|
|
24
23
|
|
|
25
24
|
|
|
25
|
+
def unzip_file(zip_file, out_dir="./"):
|
|
26
|
+
obj = ZipFile(zip_file, "r")
|
|
27
|
+
for item in obj.namelist():
|
|
28
|
+
obj.extract(item, out_dir)
|
|
29
|
+
|
|
30
|
+
|
|
26
31
|
class OpenAPI(Machine):
|
|
27
32
|
def __init__(self, context):
|
|
28
33
|
if not found_bohriumsdk:
|
|
@@ -35,9 +40,35 @@ class OpenAPI(Machine):
|
|
|
35
40
|
self.grouped = self.remote_profile.get("grouped", True)
|
|
36
41
|
self.retry_count = self.remote_profile.get("retry_count", 3)
|
|
37
42
|
self.ignore_exit_code = context.remote_profile.get("ignore_exit_code", True)
|
|
38
|
-
|
|
43
|
+
|
|
44
|
+
access_key = (
|
|
45
|
+
self.remote_profile.get("access_key", None)
|
|
46
|
+
or os.getenv("BOHRIUM_ACCESS_KEY", None)
|
|
47
|
+
or os.getenv("ACCESS_KEY", None)
|
|
48
|
+
)
|
|
49
|
+
project_id = (
|
|
50
|
+
self.remote_profile.get("project_id", None)
|
|
51
|
+
or os.getenv("BOHRIUM_PROJECT_ID", None)
|
|
52
|
+
or os.getenv("PROJECT_ID", None)
|
|
53
|
+
)
|
|
54
|
+
app_key = (
|
|
55
|
+
self.remote_profile.get("app_key", None)
|
|
56
|
+
or os.getenv("BOHRIUM_APP_KEY", None)
|
|
57
|
+
or os.getenv("APP_KEY", None)
|
|
58
|
+
)
|
|
59
|
+
if access_key is None:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
"remote_profile must contain 'access_key' or set environment variable 'BOHRIUM_ACCESS_KEY'"
|
|
62
|
+
)
|
|
63
|
+
if project_id is None:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"remote_profile must contain 'project_id' or set environment variable 'BOHRIUM_PROJECT_ID'"
|
|
66
|
+
)
|
|
67
|
+
self.client = Bohrium(
|
|
68
|
+
access_key=access_key, project_id=project_id, app_key=app_key
|
|
69
|
+
)
|
|
70
|
+
self.storage = Tiefblue()
|
|
39
71
|
self.job = Job(client=self.client)
|
|
40
|
-
self.storage = Storage(client=self.client)
|
|
41
72
|
self.group_id = None
|
|
42
73
|
|
|
43
74
|
def gen_script(self, job):
|
|
@@ -102,7 +133,6 @@ class OpenAPI(Machine):
|
|
|
102
133
|
}
|
|
103
134
|
if job.job_state == JobStatus.unsubmitted:
|
|
104
135
|
openapi_params["job_id"] = job.job_id
|
|
105
|
-
|
|
106
136
|
data = self.job.insert(**openapi_params)
|
|
107
137
|
|
|
108
138
|
job.job_id = data.get("jobId", 0) # type: ignore
|
|
@@ -170,7 +200,7 @@ class OpenAPI(Machine):
|
|
|
170
200
|
result_filename = job_hash + "_back.zip"
|
|
171
201
|
target_result_zip = os.path.join(self.context.local_root, result_filename)
|
|
172
202
|
self.storage.download_from_url(job_url, target_result_zip)
|
|
173
|
-
|
|
203
|
+
unzip_file(target_result_zip, out_dir=self.context.local_root)
|
|
174
204
|
try:
|
|
175
205
|
os.makedirs(os.path.join(self.context.local_root, "backup"), exist_ok=True)
|
|
176
206
|
shutil.move(
|
dpdispatcher/machines/slurm.py
CHANGED
|
@@ -85,7 +85,7 @@ class Slurm(Machine):
|
|
|
85
85
|
# self.context.write_file(fname=os.path.join(self.context.submission.work_base, script_file_name), write_str=script_str)
|
|
86
86
|
command = "cd {} && {} {}".format(
|
|
87
87
|
shlex.quote(self.context.remote_root),
|
|
88
|
-
"sbatch",
|
|
88
|
+
"sbatch --parsable",
|
|
89
89
|
shlex.quote(script_file_name),
|
|
90
90
|
)
|
|
91
91
|
ret, stdin, stdout, stderr = self.context.block_call(command)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dpdispatcher
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.11
|
|
4
4
|
Summary: Generate HPC scheduler systems jobs input scripts, submit these scripts to HPC systems, and poke until they finish
|
|
5
5
|
Author: DeepModeling
|
|
6
6
|
License: GNU LESSER GENERAL PUBLIC LICENSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
dpdispatcher/__init__.py,sha256=CLZP_N5CTp14ujWCykEHuJjoIfKR6CwrclXhjWUgNoE,517
|
|
2
2
|
dpdispatcher/__main__.py,sha256=BFhG-mSBzVZUEezQJqXWZnt2WsnhAHT_zpT8Y6gpOz0,116
|
|
3
|
-
dpdispatcher/_version.py,sha256=
|
|
3
|
+
dpdispatcher/_version.py,sha256=sw6_B_LEWeMBiN2AFkGTSamIubZxwWvM45JyuClFrPY,706
|
|
4
4
|
dpdispatcher/arginfo.py,sha256=pNaxYIE6ahBidpR7OCKZdw8iGt003uTXGSlVzwiuvRg,188
|
|
5
5
|
dpdispatcher/base_context.py,sha256=W4eWDWVzYeL6EuEkivmJp-_h_B2mV9PtRWc09l1_Qzc,5242
|
|
6
6
|
dpdispatcher/dlog.py,sha256=QJKAwB6gV3Zb6zQUL9dZ_uIoTIEy9Z7ecmVQ-8WNmD8,1081
|
|
@@ -13,8 +13,8 @@ dpdispatcher/contexts/dp_cloud_server_context.py,sha256=PGRMef3q2hfK-o5dNIWWvzPc
|
|
|
13
13
|
dpdispatcher/contexts/hdfs_context.py,sha256=mYQzXMZ4A9EjjWBAH3Ba6HOErUhMMwCsKxOjpd5R57Y,9105
|
|
14
14
|
dpdispatcher/contexts/lazy_local_context.py,sha256=FAClbLD2F4LizUqFzMOg3t0Z6NLeTDLJy7NkRcDELFs,5070
|
|
15
15
|
dpdispatcher/contexts/local_context.py,sha256=VbaSXGAc_EDMT0K5WV_flBF0bX87ntrwO_hq_Bkcb04,14590
|
|
16
|
-
dpdispatcher/contexts/openapi_context.py,sha256=
|
|
17
|
-
dpdispatcher/contexts/ssh_context.py,sha256=
|
|
16
|
+
dpdispatcher/contexts/openapi_context.py,sha256=bWfg-y-QHAT1iSva6PvuLgMs1uJDLom4QC06IGMeUfs,11928
|
|
17
|
+
dpdispatcher/contexts/ssh_context.py,sha256=VOpPRQX1WS6WbWxCYhuT62q203v1Qx_hygLsV41p3Fg,37585
|
|
18
18
|
dpdispatcher/dpcloudserver/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
19
|
dpdispatcher/dpcloudserver/client.py,sha256=k1niKjG6zFnMtHn_UuCjYoOcMju3o3PV-GdyVLr5-KM,165
|
|
20
20
|
dpdispatcher/entrypoints/__init__.py,sha256=exKSFT3j2oCerGwtI8WbHQK-D0K-CyifocRji1xntT4,20
|
|
@@ -27,10 +27,10 @@ dpdispatcher/machines/distributed_shell.py,sha256=c0-lGeGz_M-PY2gPciT-uYZLQht5XT
|
|
|
27
27
|
dpdispatcher/machines/dp_cloud_server.py,sha256=SR69gsFb2BvOQCW1QnWfP3cQvu_qHLJNsycp5wzosJU,11706
|
|
28
28
|
dpdispatcher/machines/fugaku.py,sha256=oY2hD2ldL2dztwtJ9WNisdsfPnaX-5yTRXewIT9r60I,4314
|
|
29
29
|
dpdispatcher/machines/lsf.py,sha256=xGDq8OLAk83E9EjK_3-QtEOyahvBGspWbxT__7mnSTw,7896
|
|
30
|
-
dpdispatcher/machines/openapi.py,sha256=
|
|
30
|
+
dpdispatcher/machines/openapi.py,sha256=8unjG9HTCBwbkZTW_t-QKOSaBHfqhzsMARTVVCDGGHw,9929
|
|
31
31
|
dpdispatcher/machines/pbs.py,sha256=gUoj3OGQbZRBK4P-WXlhrxlQqTeUi9X8JGLOkAB__wE,12669
|
|
32
32
|
dpdispatcher/machines/shell.py,sha256=EeYnRCowXdzO3Nh25Yh_t5xeM6frq4uChk4GVx7OjH8,4797
|
|
33
|
-
dpdispatcher/machines/slurm.py,sha256=
|
|
33
|
+
dpdispatcher/machines/slurm.py,sha256=rN51Qh_u9ZVmjkIClu4Tfc-Qesr19DxPaaeh0FmRx14,15413
|
|
34
34
|
dpdispatcher/utils/__init__.py,sha256=fwvwkMf7DFNQkNBiIce8Y8gRA6FhICwKjkKiXu_BEJg,13
|
|
35
35
|
dpdispatcher/utils/hdfs_cli.py,sha256=a1a9PJAzt3wsTcdaSw_oD1vcNw59pMooxpAHjYOaaGA,5209
|
|
36
36
|
dpdispatcher/utils/job_status.py,sha256=Eszs4TPLfszCuf6zLaFonf25feXDUguF28spYOjJpQE,233
|
|
@@ -41,9 +41,9 @@ dpdispatcher/utils/dpcloudserver/client.py,sha256=fp1e14MTgsMgasZSWowq-NqfCoi21P
|
|
|
41
41
|
dpdispatcher/utils/dpcloudserver/config.py,sha256=NteQzf1OeEkz2UbkXHHQ0B72cUu23zLVzpM9Yh4v1Cc,559
|
|
42
42
|
dpdispatcher/utils/dpcloudserver/retcode.py,sha256=1qAF8gFZx55u2sO8KbtYSIIrjcO-IGufEUlwbkSfC1g,721
|
|
43
43
|
dpdispatcher/utils/dpcloudserver/zip_file.py,sha256=f9WrlktwHW0YipaWg5Y0kxjMZlhD1cJYa6EUpvu4Cro,2611
|
|
44
|
-
dpdispatcher-0.6.
|
|
45
|
-
dpdispatcher-0.6.
|
|
46
|
-
dpdispatcher-0.6.
|
|
47
|
-
dpdispatcher-0.6.
|
|
48
|
-
dpdispatcher-0.6.
|
|
49
|
-
dpdispatcher-0.6.
|
|
44
|
+
dpdispatcher-0.6.11.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
|
|
45
|
+
dpdispatcher-0.6.11.dist-info/METADATA,sha256=z3SBMyU0Mq8f1Sv_4vbYAUYcjfh0Fjjbjdv9wNBf_EM,12834
|
|
46
|
+
dpdispatcher-0.6.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
47
|
+
dpdispatcher-0.6.11.dist-info/entry_points.txt,sha256=NRHUV0IU_u7_XtcmmEDnVzAcUmurhiEAGwENckrajo4,233
|
|
48
|
+
dpdispatcher-0.6.11.dist-info/top_level.txt,sha256=35jAQoXY-b-e9fJ1_mxhZUiaCoJNt1ZI7mpFRf07Qjs,13
|
|
49
|
+
dpdispatcher-0.6.11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|