ichec-platform-core 0.0.1__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.
Files changed (45) hide show
  1. ichec_platform_core/__init__.py +0 -0
  2. ichec_platform_core/filesystem/__init__.py +1 -0
  3. ichec_platform_core/filesystem/cli.py +52 -0
  4. ichec_platform_core/filesystem/filesystem.py +268 -0
  5. ichec_platform_core/logging_utils.py +26 -0
  6. ichec_platform_core/main_cli.py +39 -0
  7. ichec_platform_core/network/__init__.py +1 -0
  8. ichec_platform_core/network/cli.py +44 -0
  9. ichec_platform_core/network/http_client.py +125 -0
  10. ichec_platform_core/project/__init__.py +2 -0
  11. ichec_platform_core/project/milestone.py +34 -0
  12. ichec_platform_core/project/person.py +17 -0
  13. ichec_platform_core/project/version.py +68 -0
  14. ichec_platform_core/py.typed +0 -0
  15. ichec_platform_core/runtime.py +61 -0
  16. ichec_platform_core/serialization/__init__.py +2 -0
  17. ichec_platform_core/serialization/csv_utils.py +15 -0
  18. ichec_platform_core/serialization/json_utils.py +73 -0
  19. ichec_platform_core/serialization/key_value.py +50 -0
  20. ichec_platform_core/serialization/yaml_utils.py +49 -0
  21. ichec_platform_core/system/__init__.py +4 -0
  22. ichec_platform_core/system/cluster/__init__.py +0 -0
  23. ichec_platform_core/system/cluster/cluster_allocation.py +8 -0
  24. ichec_platform_core/system/cluster/node.py +11 -0
  25. ichec_platform_core/system/cpu.py +50 -0
  26. ichec_platform_core/system/environment.py +44 -0
  27. ichec_platform_core/system/gpu.py +38 -0
  28. ichec_platform_core/system/network/__init__.py +0 -0
  29. ichec_platform_core/system/network/host.py +10 -0
  30. ichec_platform_core/system/network/info.py +7 -0
  31. ichec_platform_core/system/process.py +191 -0
  32. ichec_platform_core/system/system_event.py +12 -0
  33. ichec_platform_core/test_utils.py +24 -0
  34. ichec_platform_core/url_utils.py +27 -0
  35. ichec_platform_core/version_control/__init__.py +3 -0
  36. ichec_platform_core/version_control/cli.py +231 -0
  37. ichec_platform_core/version_control/git.py +246 -0
  38. ichec_platform_core/version_control/gitlab.py +106 -0
  39. ichec_platform_core/version_control/gitlab_client.py +339 -0
  40. ichec_platform_core-0.0.1.dist-info/METADATA +100 -0
  41. ichec_platform_core-0.0.1.dist-info/RECORD +45 -0
  42. ichec_platform_core-0.0.1.dist-info/WHEEL +5 -0
  43. ichec_platform_core-0.0.1.dist-info/entry_points.txt +2 -0
  44. ichec_platform_core-0.0.1.dist-info/licenses/LICENSE.txt +241 -0
  45. ichec_platform_core-0.0.1.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1 @@
1
+ from .filesystem import * # NOQA
@@ -0,0 +1,52 @@
1
+ from pathlib import Path
2
+ import logging
3
+
4
+ from ichec_platform_core.cli_utils import launch_common
5
+
6
+ from .filesystem import read_file, replace_in_files
7
+
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def cli_replace_in_files(args):
13
+
14
+ launch_common(args)
15
+
16
+ search_term = read_file(args.search.resolve()).strip()
17
+ replace_term = read_file(args.insert.resolve()).strip()
18
+
19
+ target_dir = args.target.resolve()
20
+ logger.info(
21
+ "Replacing '%s' with '%s' in files in %s", search_term, replace_term, target_dir
22
+ )
23
+ count = replace_in_files(target_dir, search_term, replace_term, args.extension)
24
+ logger.info("Replaced in %d files", count)
25
+
26
+
27
+ def register(subparsers):
28
+
29
+ parser = subparsers.add_parser("filesystem")
30
+ subparsers = parser.add_subparsers(required=True)
31
+
32
+ replace_files_parser = subparsers.add_parser("replace_in_files")
33
+
34
+ replace_files_parser.add_argument(
35
+ "--target",
36
+ type=Path,
37
+ help="Path to the directory containing the files",
38
+ )
39
+ replace_files_parser.add_argument(
40
+ "--search", type=Path, help="File containing the term to be replaced"
41
+ )
42
+ replace_files_parser.add_argument(
43
+ "--insert", type=Path, help="File containing the term to be inserted"
44
+ )
45
+ replace_files_parser.add_argument(
46
+ "--extension",
47
+ type=str,
48
+ default="",
49
+ help="Only process files with this extension, if set",
50
+ )
51
+
52
+ replace_files_parser.set_defaults(func=cli_replace_in_files)
@@ -0,0 +1,268 @@
1
+ """
2
+ This module has wrappers over Python built-ins for filsystem operations
3
+ """
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+ import os
8
+
9
+ from ichec_platform_core.runtime import ctx
10
+
11
+
12
+ def copy(src: Path, dst: Path) -> None:
13
+ """
14
+ Copy the file or directory from `src` to `dst`
15
+ :param src: The location to copy from
16
+ :param dst: The location to copy to
17
+ """
18
+
19
+ if ctx.can_modify():
20
+ shutil.copy(src, dst)
21
+ else:
22
+ ctx.add_cmd(f"copy {src} {dst}")
23
+
24
+
25
+ def make_archive(archive: Path, fmt: str, src: Path, rename: bool = False) -> None:
26
+ """
27
+ Create an archive with name `archive_name` and chosen format, e.g. zip
28
+ from the content in `src`
29
+ :param Path archive_name: The path to the created archive
30
+ :param str fmt: The desired archive format, e.g. zip
31
+ :param Path src: The src of the content to be archived
32
+ :param bool rename: If true rename source directory to have the archive name
33
+ """
34
+
35
+ if ctx.can_modify():
36
+ if rename:
37
+ shutil.copytree(src, archive)
38
+ src = archive
39
+ shutil.make_archive(str(archive), fmt, src)
40
+ if rename:
41
+ shutil.rmtree(archive)
42
+ else:
43
+ ctx.add_cmd(f"make_archive {archive} {fmt} {src}")
44
+
45
+
46
+ def unpack_archive(src: Path, dst: Path) -> None:
47
+ """
48
+ Extract the archive at `src` to `dst`
49
+ """
50
+
51
+ if ctx.can_modify():
52
+ shutil.unpack_archive(src, dst)
53
+ else:
54
+ ctx.add_cmd(f"unpack_archive {src} {dst}")
55
+
56
+
57
+ def copy_file(src: Path, dst: Path):
58
+ """
59
+ Copy a file in src to the target directory in dst
60
+
61
+ Make the dst directory if needed
62
+ """
63
+ os.makedirs(dst, exist_ok=True)
64
+ shutil.copy(src, dst / src.name)
65
+
66
+
67
+ def copy_files(src: Path, dst: Path):
68
+ """
69
+ Copy all files from the `src` directory to the `dst` directory
70
+
71
+ Create the dst directory first if needed.
72
+
73
+ :param Path src: The directory to copy from
74
+ :param Path dst: The directory to copy to
75
+ """
76
+ os.makedirs(dst, exist_ok=True)
77
+ for direntry in src.iterdir():
78
+ if direntry.is_file():
79
+ shutil.copy(direntry, dst)
80
+
81
+
82
+ def copy_files_relative(paths: list[Path], src: Path, dst: Path):
83
+ """
84
+ For each relative path in `paths` copy the file at `src/path` to
85
+ `dst/path`.
86
+
87
+ Tries to create the dst directory first.
88
+
89
+ :param list[Path] paths: The list of paths to copy
90
+ :param Path src: The source path to copy relative to
91
+ :param Path dst: The destination path to copy into.
92
+ """
93
+
94
+ for path in paths:
95
+ os.makedirs(dst / path.parent, exist_ok=True)
96
+ shutil.copy(src / path, dst / path)
97
+
98
+
99
+ def clear_dir(src: Path):
100
+ """
101
+ If there is a directory at `src` delete it and make a new one
102
+ """
103
+
104
+ if src.exists():
105
+ shutil.rmtree(src)
106
+ os.makedirs(src)
107
+
108
+
109
+ def read_file(src: Path):
110
+ """
111
+ Read a file at the provided path
112
+ """
113
+ with open(src, "r", encoding="utf-8") as f:
114
+ return f.read()
115
+
116
+
117
+ def read_file_lines(src: Path):
118
+ """
119
+ Read a file as lines
120
+ """
121
+
122
+ with open(src, "r", encoding="utf-8") as f:
123
+ return f.readlines()
124
+
125
+
126
+ def write_file_lines(
127
+ src: Path, lines: list[str], make_parents: bool = True, make_exe: bool = False
128
+ ):
129
+ """
130
+ Write a file with lines
131
+ """
132
+
133
+ if make_parents:
134
+ os.makedirs(src.parent, exist_ok=True)
135
+
136
+ with open(src, "w", encoding="utf-8") as f:
137
+ for line in lines:
138
+ f.write(line)
139
+
140
+ if make_exe:
141
+ os.chmod(src, 0o0755)
142
+
143
+
144
+ def write_file(src: Path, content: str, make_parents: bool = True):
145
+ """
146
+ Write a file
147
+ """
148
+ if make_parents:
149
+ os.makedirs(src.parent, exist_ok=True)
150
+
151
+ with open(src, "w", encoding="utf-8") as f:
152
+ f.write(content)
153
+
154
+
155
+ def replace_in_file(src: Path, search_term: str, replace_term: str) -> bool:
156
+ """
157
+ Replace the search term in a file
158
+ """
159
+
160
+ content = read_file_lines(src)
161
+ has_update = False
162
+ for idx, line in enumerate(content):
163
+ replaced_line = line.replace(search_term, replace_term)
164
+ if replaced_line != line:
165
+ content[idx] = replaced_line
166
+ has_update = True
167
+ if has_update:
168
+ write_file_lines(src, content)
169
+ return True
170
+ return False
171
+
172
+
173
+ def replace_in_files(
174
+ src: Path, search_term: str, replace_term: str, extension: str
175
+ ) -> int:
176
+ """
177
+ Replace a search term in multiple files
178
+ """
179
+
180
+ count = 0
181
+ for direntry in src.iterdir():
182
+ if direntry.is_file():
183
+ if not extension or direntry.suffix == f".{extension}":
184
+ replaced = replace_in_file(direntry, search_term, replace_term)
185
+ if replaced:
186
+ count += 1
187
+ return count
188
+
189
+
190
+ def file_contains_string(src: Path, search_term: str) -> bool:
191
+ """
192
+ True if a file contains the search term
193
+ """
194
+ content = read_file(src)
195
+ return search_term in content
196
+
197
+
198
+ def get_dirs(path: Path, prefix: str = "") -> list[Path]:
199
+ """
200
+ Return all directories in a path, optionally filter directory
201
+ name on a prefix
202
+ """
203
+ if prefix:
204
+ return [
205
+ entry
206
+ for entry in path.iterdir()
207
+ if entry.is_dir() and entry.name.startswith(prefix)
208
+ ]
209
+ return [entry for entry in path.iterdir() if entry.is_dir()]
210
+
211
+
212
+ def is_file_with_ext(f, extension: str) -> bool:
213
+ """
214
+ True if the path item is a file with given extension
215
+ """
216
+ return f.is_file() and str(f).endswith(f".{extension}")
217
+
218
+
219
+ def get_files(path: Path, extension: str) -> list:
220
+ """
221
+ Return all files in this dir with the provided extension
222
+ """
223
+ return [f for f in path.glob("*") if is_file_with_ext(f, extension)]
224
+
225
+
226
+ def is_file_with_extensions(
227
+ f, extensions: tuple[str], excludes: list[str] | None = None
228
+ ):
229
+ """
230
+ True if the path item is a file and has one of the provided extensions
231
+ """
232
+
233
+ if not f.is_file():
234
+ return False
235
+ for ext in extensions:
236
+ check_str = str(f).lower()
237
+ if check_str.endswith(f".{ext.lower()}"):
238
+ if excludes:
239
+ for exclude in excludes:
240
+ if check_str.endswith(f"{exclude.lower()}.{ext.lower()}"):
241
+ return False
242
+ return True
243
+ return False
244
+
245
+
246
+ def get_files_recursive(
247
+ path: Path, extensions: tuple[str], excludes: list[str] | None = None
248
+ ):
249
+ """
250
+ Get all provided files recursively, filter on extensions and excludes
251
+ """
252
+ return [
253
+ f for f in path.rglob("*") if is_file_with_extensions(f, extensions, excludes)
254
+ ]
255
+
256
+
257
+ def get_json_files(path: Path):
258
+ """
259
+ Get all json files in this dir
260
+ """
261
+ return get_files(path, extension="json")
262
+
263
+
264
+ def get_csv_files(path: Path):
265
+ """
266
+ Get all csv files in this dir
267
+ """
268
+ return get_files(path, extension="csv")
@@ -0,0 +1,26 @@
1
+ import logging
2
+
3
+
4
+ def setup_default_logger(log_filename: str | None = None):
5
+ fmt = "%(asctime)s%(msecs)03d | %(filename)s:%(lineno)s:%(funcName)s | %(message)s"
6
+ logging.basicConfig(
7
+ filename=log_filename,
8
+ filemode="w",
9
+ format=fmt,
10
+ datefmt="%Y%m%dT%H:%M:%S:",
11
+ level=logging.INFO,
12
+ )
13
+
14
+
15
+ class LogLine:
16
+ """
17
+ Helper class to represent a log line
18
+ """
19
+
20
+ def __init__(self, timestamp: float, thread_id: int, message: str):
21
+ self.timestamp = timestamp
22
+ self.message = message
23
+ self.thread_id = thread_id
24
+
25
+ def __str__(self):
26
+ return f"{self.timestamp} | {self.thread_id} | {self.message}"
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ This is the main entrypoint for the ichec_platform_core utility
5
+ """
6
+
7
+ import argparse
8
+ import logging
9
+
10
+ from ichec_platform_core.cli_utils import launch_common
11
+ import ichec_platform_core.network.cli
12
+ import ichec_platform_core.filesystem.cli
13
+ import ichec_platform_core.version_control.cli
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ def main_cli():
20
+ parser = argparse.ArgumentParser()
21
+ parser.add_argument(
22
+ "--dry_run",
23
+ type=int,
24
+ default=0,
25
+ help="Dry run script - 0 can modify, 1 can read, 2 no modify - no read",
26
+ )
27
+ subparsers = parser.add_subparsers(required=True)
28
+
29
+ ichec_platform_core.network.cli.register(subparsers)
30
+ ichec_platform_core.filesystem.cli.register(subparsers)
31
+ ichec_platform_core.version_control.cli.register(subparsers)
32
+
33
+ args = parser.parse_args()
34
+ launch_common(args)
35
+ args.func(args)
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main_cli()
@@ -0,0 +1 @@
1
+ from .http_client import * # NOQA
@@ -0,0 +1,44 @@
1
+ import os
2
+ from pathlib import Path
3
+ import logging
4
+
5
+ from ichec_platform_core.cli_utils import launch_common
6
+
7
+ from .http_client import HttpClient
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def download(args):
13
+
14
+ launch_common(args)
15
+ logger.info("Attempting to download from %s", args.url)
16
+
17
+ headers = {args.token_header: args.token}
18
+
19
+ http_client = HttpClient()
20
+ http_client.download_file(args.url, args.download_dir, headers)
21
+
22
+
23
+ def register(subparsers):
24
+
25
+ parser = subparsers.add_parser("network")
26
+
27
+ subparsers = parser.add_subparsers(required=True)
28
+
29
+ download_parser = subparsers.add_parser("download")
30
+
31
+ download_parser.add_argument("url", type=str, help="Url to download")
32
+ download_parser.add_argument(
33
+ "--token", type=str, help="Optional auth token", default=""
34
+ )
35
+ download_parser.add_argument(
36
+ "--token_header", type=str, help="Optional auth token header key", default=""
37
+ )
38
+ download_parser.add_argument(
39
+ "--download_dir",
40
+ type=Path,
41
+ help="Directory to download to",
42
+ default=Path(os.getcwd()),
43
+ )
44
+ download_parser.set_defaults(func=download)
@@ -0,0 +1,125 @@
1
+ """
2
+ A basic low-dependency http client
3
+ """
4
+
5
+ import urllib.request
6
+ import urllib.parse
7
+ from pathlib import Path
8
+ import os
9
+ import json
10
+
11
+ from ichec_platform_core.runtime import ctx
12
+
13
+
14
+ class HttpClient:
15
+ """
16
+ A basic http client with low dependencies (urllib)
17
+ """
18
+
19
+ def __init__(self):
20
+ self.default_encode_charset = "utf-8"
21
+ self.default_decode_charset = "utf-8"
22
+
23
+ def post_json(self, url: str, payload, headers: dict[str, str] | None):
24
+ if not headers:
25
+ headers = {}
26
+
27
+ headers["content-type"] = "application/json"
28
+ return self.make_post_request(url, headers, json.dumps(payload))
29
+
30
+ def make_get_request(self, url: str, headers: dict[str, str] | None = None):
31
+ if not headers:
32
+ headers = {}
33
+ return self._make_request(url, "GET", headers)
34
+
35
+ def make_put_request(
36
+ self, url: str, headers: dict[str, str] | None = None, payload=None
37
+ ):
38
+ if not headers:
39
+ headers = {}
40
+ return self._make_request(url, "PUT", headers, payload)
41
+
42
+ def make_post_request(
43
+ self, url: str, headers: dict[str, str] | None = None, payload=None
44
+ ):
45
+ if not headers:
46
+ headers = {}
47
+ return self._make_request(url, "POST", headers, payload)
48
+
49
+ def download_file(
50
+ self, url: str, path: Path, headers: dict[str, str] | None = None
51
+ ):
52
+ if not headers:
53
+ headers = {}
54
+
55
+ if not ctx.can_read():
56
+ ctx.add_cmd(f"download file {url} {path} {headers}")
57
+
58
+ with open(path, "wb") as f:
59
+ req = urllib.request.Request(url, headers=headers)
60
+ with urllib.request.urlopen(req) as response:
61
+ self._check_http_status(response)
62
+ f.write(response.read())
63
+
64
+ def upload_file(self, url: str, path: Path, headers: dict[str, str] | None = None):
65
+
66
+ if not headers:
67
+ headers = {}
68
+
69
+ if not ctx.can_modify():
70
+ ctx.add_cmd(f"upload file {url} {path} {headers}")
71
+ return ""
72
+
73
+ if not path.exists():
74
+ raise RuntimeError(f"Attempted to upload file without valid path: {path}")
75
+
76
+ headers["Content-Length"] = str(os.stat(path).st_size)
77
+ headers["Content-Type"] = "application/octet-stream"
78
+
79
+ with open(path, "rb") as f:
80
+ req = urllib.request.Request(url, headers=headers, data=f, method="PUT")
81
+ with urllib.request.urlopen(req) as response:
82
+ self._check_http_status(response)
83
+ response_str = response.read().decode(self._get_charset(response))
84
+ return response_str
85
+
86
+ def _make_request(
87
+ self, url: str, method: str, headers: dict[str, str] | None = None, payload=None
88
+ ):
89
+
90
+ if not headers:
91
+ headers = {}
92
+
93
+ if method == "GET":
94
+ if not ctx.can_read():
95
+ ctx.add_cmd(f"make_request GET {url} {headers}")
96
+ return ""
97
+ elif method in ["PUT", "POST"]:
98
+ if not ctx.can_modify():
99
+ ctx.add_cmd(f"make_request {method} {url} {headers} {payload}")
100
+ return ""
101
+
102
+ req = urllib.request.Request(
103
+ url, headers=headers, data=self._endcode_payload(payload), method=method
104
+ )
105
+ with urllib.request.urlopen(req) as response:
106
+ self._check_http_status(response)
107
+ response_str = response.read().decode(self._get_charset(response))
108
+ return response_str
109
+
110
+ def _check_http_status(self, response):
111
+ if response.status >= 300:
112
+ body = response.read().decode(self._get_charset(response))
113
+ msg = f"code {response.status}, reason {response.reason}, and body {body}"
114
+ raise RuntimeError(f"Http Request failed with: {msg}")
115
+
116
+ def _endcode_payload(self, payload):
117
+ if not payload:
118
+ return payload
119
+ return payload.encode("utf-8")
120
+
121
+ def _get_charset(self, response):
122
+ charset = response.headers.get_content_charset()
123
+ if not charset:
124
+ charset = "utf-8"
125
+ return charset
@@ -0,0 +1,2 @@
1
+ from .milestone import * # NOQA
2
+ from .version import Version # NOQA
@@ -0,0 +1,34 @@
1
+ """
2
+ This module has milestone related project elements
3
+ """
4
+
5
+ from datetime import datetime
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from .person import Person
10
+
11
+
12
+ class Issue(BaseModel, frozen=True):
13
+ """
14
+ An issue is a user story or defect to work on
15
+ """
16
+
17
+ title: str = ""
18
+ description: str = ""
19
+ start_date: datetime | None = None
20
+ due_date: datetime | None = None
21
+ assignee: Person | None = None
22
+
23
+
24
+ class Milestone(BaseModel, frozen=True):
25
+ """
26
+ A milestone is a collection of work items with start and
27
+ planned end dates
28
+ """
29
+
30
+ title: str = ""
31
+ description: str = ""
32
+ start_date: datetime | None = None
33
+ due_date: datetime | None = None
34
+ issues: list[Issue] = []
@@ -0,0 +1,17 @@
1
+ """
2
+ Class representing a person, eg a user on a platform
3
+ """
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class Person(BaseModel, frozen=True):
9
+ """
10
+ A person class - used for referencing project
11
+ contributors.
12
+ """
13
+
14
+ name: str = ""
15
+ email: str = ""
16
+ orcid: str = ""
17
+ username: str
@@ -0,0 +1,68 @@
1
+ """
2
+ This module has functionality for representing and
3
+ manipulating project version numbers.
4
+ """
5
+
6
+ from datetime import date
7
+
8
+ from pydantic import BaseModel
9
+
10
+
11
+ class Version(BaseModel, frozen=True):
12
+ """
13
+ Representation of a package version number.
14
+
15
+ This resembles a semver scheme but doesn't necessarily
16
+ follow one.
17
+
18
+ Args:
19
+ content (str): Version in string form x.y.zzzz
20
+ scheme (str): The versioning scheme to use, 'semver' or 'date'
21
+
22
+ Attributes:
23
+ major (int): Major version number
24
+ minor (int): Minor version number
25
+ patch (int): Path version number
26
+ scheme (str): Versioning scheme to use
27
+ """
28
+
29
+ major: int
30
+ minor: int
31
+ patch: int
32
+ scheme: str = "semver"
33
+
34
+ def as_string(self) -> str:
35
+ return f"{self.major}.{self.minor}.{self.patch}"
36
+
37
+
38
+ def increment(version: Version, field="patch", today=date.today()) -> Version:
39
+ major = version.major
40
+ minor = version.minor
41
+ patch = version.patch
42
+
43
+ if version.scheme == "semver":
44
+ if field == "patch":
45
+ patch += 1
46
+ elif field == "minor":
47
+ patch = 0
48
+ minor += 1
49
+ elif field == "major":
50
+ major += 1
51
+ minor = 0
52
+ patch = 0
53
+ elif version.scheme == "date":
54
+ major = today.year - 2000
55
+ minor = today.month
56
+ if minor != version.minor:
57
+ patch = 1
58
+ else:
59
+ patch = version.patch + 1
60
+ return version.model_copy(update={"major": major, "minor": minor, "patch": patch})
61
+
62
+
63
+ def parse(content: str, scheme: str = "semver") -> Version:
64
+ """
65
+ Parse the version from the input content, assumes 'major.minor.patch' format.
66
+ """
67
+ major, minor, patch = content.split(".")
68
+ return Version(major=int(major), minor=int(minor), patch=int(patch), scheme=scheme)
File without changes