fair-cli 0.9.6__tar.gz → 0.9.7__tar.gz

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 (37) hide show
  1. {fair_cli-0.9.6 → fair_cli-0.9.7}/CITATION.cff +2 -2
  2. {fair_cli-0.9.6 → fair_cli-0.9.7}/PKG-INFO +5 -5
  3. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/cli.py +39 -48
  4. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/common.py +21 -13
  5. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/configuration/__init__.py +90 -78
  6. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/history.py +16 -33
  7. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/identifiers.py +30 -19
  8. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/logging.py +7 -6
  9. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/register.py +21 -24
  10. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/registry/file_types.py +1 -1
  11. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/registry/requests.py +98 -88
  12. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/registry/server.py +20 -21
  13. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/registry/storage.py +43 -43
  14. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/registry/sync.py +370 -245
  15. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/registry/versioning.py +2 -6
  16. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/run.py +2 -2
  17. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/session.py +232 -210
  18. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/staging.py +35 -57
  19. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/templates/__init__.py +2 -2
  20. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/testing.py +3 -3
  21. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/user_config/__init__.py +45 -112
  22. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/user_config/globbing.py +1 -3
  23. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/user_config/validation.py +1 -1
  24. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/utilities.py +4 -6
  25. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/virtualenv.py +0 -1
  26. {fair_cli-0.9.6 → fair_cli-0.9.7}/pyproject.toml +5 -4
  27. {fair_cli-0.9.6 → fair_cli-0.9.7}/CHANGELOG.md +0 -0
  28. {fair_cli-0.9.6 → fair_cli-0.9.7}/LICENSE +0 -0
  29. {fair_cli-0.9.6 → fair_cli-0.9.7}/README.md +0 -0
  30. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/__init__.py +0 -0
  31. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/configuration/validation.py +0 -0
  32. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/exceptions.py +0 -0
  33. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/files.txt +0 -0
  34. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/registry/__init__.py +0 -0
  35. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/registry/file_formats.json +0 -0
  36. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/templates/config.jinja +0 -0
  37. {fair_cli-0.9.6 → fair_cli-0.9.7}/fair/templates/hist.jinja +0 -0
@@ -26,7 +26,7 @@ authors:
26
26
  affiliation: University of Glasgow
27
27
  orcid: https://orcid.org/0000-0002-4424-9890
28
28
 
29
- date-released: 2024-08-04
29
+ date-released: 2025-04-08
30
30
  keywords:
31
31
  - FAIR Data Pipeline
32
32
  - FAIR
@@ -36,4 +36,4 @@ license: BSD-2-Clause
36
36
  message: If you use this software, please cite it using these metadata.
37
37
  repository-code: https://github.com/FAIRDataPipeline/FAIR-CLI/
38
38
  title: "The FAIR Data Pipeline command line tool"
39
- version: 0.9.6
39
+ version: 0.9.7
@@ -1,13 +1,12 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: fair-cli
3
- Version: 0.9.6
3
+ Version: 0.9.7
4
4
  Summary: Synchronization interface for the SCRC FAIR Data Pipeline registry
5
- Home-page: https://www.fairdatapipeline.org/
6
5
  License: BSD-2-Clause
7
6
  Keywords: FAIR Data Pipeline,FAIR,Data Management,Provenance
8
7
  Author: Richard Reeve
9
8
  Author-email: richard.reeve@glasgow.ac.uk
10
- Requires-Python: >=3.8.0,<4.0.0
9
+ Requires-Python: >=3.9.0,<4.0.0
11
10
  Classifier: Development Status :: 4 - Beta
12
11
  Classifier: Environment :: Console
13
12
  Classifier: Intended Audience :: Science/Research
@@ -16,11 +15,11 @@ Classifier: Operating System :: MacOS
16
15
  Classifier: Operating System :: OS Independent
17
16
  Classifier: Operating System :: POSIX :: Linux
18
17
  Classifier: Programming Language :: Python :: 3
19
- Classifier: Programming Language :: Python :: 3.8
20
18
  Classifier: Programming Language :: Python :: 3.9
21
19
  Classifier: Programming Language :: Python :: 3.10
22
20
  Classifier: Programming Language :: Python :: 3.11
23
21
  Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
24
23
  Classifier: Topic :: Database :: Front-Ends
25
24
  Classifier: Topic :: Scientific/Engineering
26
25
  Provides-Extra: all
@@ -39,6 +38,7 @@ Requires-Dist: simplejson (>=3.17.5,<4.0.0)
39
38
  Requires-Dist: toml (>=0.10.2,<0.11.0)
40
39
  Requires-Dist: validators (>=0.18.2,<0.19.0)
41
40
  Project-URL: Documentation, https://www.fairdatapipeline.org/docs/interface/fdp/
41
+ Project-URL: Homepage, https://www.fairdatapipeline.org/
42
42
  Project-URL: Issue Tracker, https://github.com/FAIRDataPipeline/FAIR-CLI/issues
43
43
  Project-URL: Repository, https://github.com/FAIRDataPipeline/FAIR-CLI
44
44
  Description-Content-Type: text/markdown
@@ -60,7 +60,7 @@ def complete_data_products(ctx, param, incomplete) -> typing.List[str]:
60
60
  _staging_file = fdp_com.staging_cache(os.getcwd())
61
61
  if not os.path.exists(_staging_file):
62
62
  return []
63
- _staging_data = yaml.safe_load(open(_staging_file, encoding='utf-8'))
63
+ _staging_data = yaml.safe_load(open(_staging_file, encoding="utf-8"))
64
64
  _candidates = list(_staging_data["data_product"].keys())
65
65
  return [
66
66
  click.shell_completion.CompletionItem(c)
@@ -90,47 +90,49 @@ def cli(ctx):
90
90
  """Welcome to FAIR-CLI, the FAIR data pipeline command-line interface."""
91
91
  pass
92
92
 
93
+
93
94
  @cli.command()
94
95
  @click.argument("file_path")
95
- @click.option("--remote", help="Show Remote Code Runs", default= "origin")
96
+ @click.option("--remote", help="Show Remote Code Runs", default="origin")
96
97
  @click.option("--debug/--no-debug", help="Run in debug mode", default=False)
97
98
  @click.option(
98
99
  "--local/--no-local",
99
100
  help="init without a remote registry - useful for closed systems",
100
101
  default=False,
101
102
  )
102
- def identify(file_path: str, remote:str, debug, local:bool) -> None:
103
+ def identify(file_path: str, remote: str, debug, local: bool) -> None:
103
104
  """
104
105
  list details of a file
105
-
106
+
106
107
  Usage: fair identify /path/to/file [remote]
107
108
  """
108
109
  try:
109
110
  with fdp_session.FAIR(
110
111
  os.getcwd(), debug=debug, server_mode=fdp_svr.SwitchMode.CLI, local=local
111
112
  ) as fair_session:
112
- fair_session.get_details(file_path, remote = "")
113
+ fair_session.get_details(file_path, remote="")
113
114
  if not local:
114
- fair_session.get_details(file_path, remote = remote)
115
+ fair_session.get_details(file_path, remote=remote)
115
116
  except fdp_exc.FAIRCLIException as e:
116
117
  e.err_print()
117
118
  if e.level.lower() == "error":
118
119
  sys.exit(e.exit_code)
119
120
 
121
+
120
122
  @cli.command()
121
123
  @click.argument("data_product")
122
- @click.option("--remote", help="Show Remote Code Runs", default= "origin")
124
+ @click.option("--remote", help="Show Remote Code Runs", default="origin")
123
125
  @click.option("--debug/--no-debug", help="Run in debug mode", default=False)
124
126
  @click.option(
125
127
  "--local/--no-local",
126
128
  help="init without a remote registry - useful for closed systems",
127
129
  default=False,
128
130
  )
129
- def find(data_product: str, remote: str, debug:bool, local:bool) -> None:
131
+ def find(data_product: str, remote: str, debug: bool, local: bool) -> None:
130
132
  """
131
133
  Shows where a data product is located
132
-
133
- Data Products should be formatted:
134
+
135
+ Data Products should be formatted:
134
136
  <namespace>:<data product name>@v<version>
135
137
  eg:
136
138
  PSU:SEIRS_model/parameters@v1.0.0
@@ -149,6 +151,7 @@ def find(data_product: str, remote: str, debug:bool, local:bool) -> None:
149
151
  if e.level.lower() == "error":
150
152
  sys.exit(e.exit_code)
151
153
 
154
+
152
155
  @cli.command()
153
156
  @click.option("--verbose/--not-verbose", help="Display URLs", default=False)
154
157
  @click.option("--debug/--no-debug", help="Run in debug mode", default=False)
@@ -165,60 +168,62 @@ def status(verbose, debug) -> None:
165
168
  if e.level.lower() == "error":
166
169
  sys.exit(e.exit_code)
167
170
 
171
+
168
172
  @cli.group(invoke_without_command=True)
169
173
  @click.option("--debug/--no-debug", help="Run in debug mode", default=False)
170
- @click.option("--remote", help="Show Remote Code Runs", default= "")
174
+ @click.option("--remote", help="Show Remote Code Runs", default="")
171
175
  @click.pass_context
172
176
  def list(ctx, debug, remote) -> None:
173
177
  """Commands to list data_product(s) and code_run(s)"""
174
178
  if ctx.obj is None:
175
179
  ctx.obj = {}
176
- ctx.obj['DEBUG'] = debug
177
- ctx.obj['REMOTE'] = remote
180
+ ctx.obj["DEBUG"] = debug
181
+ ctx.obj["REMOTE"] = remote
178
182
  _current_args = " ".join(sys.argv)
179
183
  if not ("data-products" in _current_args or "code-runs" in _current_args):
180
184
  ctx.invoke(data_products)
181
185
  ctx.invoke(code_runs)
182
186
 
187
+
183
188
  @list.command()
184
189
  @click.pass_context
185
190
  def data_products(ctx) -> None:
186
- debug = ctx.obj['DEBUG']
187
- remote = ctx.obj['REMOTE']
191
+ debug = ctx.obj["DEBUG"]
192
+ remote = ctx.obj["REMOTE"]
188
193
  try:
189
194
  with fdp_session.FAIR(
190
195
  os.getcwd(), debug=debug, server_mode=fdp_svr.SwitchMode.CLI
191
196
  ) as fair_session:
192
- fair_session.show_all_data_products(remote = remote)
197
+ fair_session.show_all_data_products(remote=remote)
193
198
  except fdp_exc.FAIRCLIException as e:
194
199
  e.err_print()
195
200
  if e.level.lower() == "error":
196
201
  sys.exit(e.exit_code)
197
202
 
203
+
198
204
  @list.command()
199
205
  @click.pass_context
200
206
  def code_runs(ctx) -> None:
201
- debug = ctx.obj['DEBUG']
202
- remote = ctx.obj['REMOTE']
207
+ debug = ctx.obj["DEBUG"]
208
+ remote = ctx.obj["REMOTE"]
203
209
  try:
204
210
  with fdp_session.FAIR(
205
211
  os.getcwd(), debug=debug, server_mode=fdp_svr.SwitchMode.CLI
206
212
  ) as fair_session:
207
- fair_session.show_all_code_runs(remote = remote)
213
+ fair_session.show_all_code_runs(remote=remote)
208
214
  except fdp_exc.FAIRCLIException as e:
209
215
  e.err_print()
210
216
  if e.level.lower() == "error":
211
217
  sys.exit(e.exit_code)
212
218
 
219
+
213
220
  @cli.command()
214
221
  @click.option("--debug/--no-debug", help="Run in debug mode", default=False)
215
222
  @click.argument("output", nargs=-1)
216
223
  def create(debug, output: str) -> None:
217
224
  """Generate a new FAIR repository user YAML config file"""
218
225
  output = (
219
- output[0]
220
- if output
221
- else os.path.join(os.getcwd(), fdp_com.USER_CONFIG_FILE)
226
+ output[0] if output else os.path.join(os.getcwd(), fdp_com.USER_CONFIG_FILE)
222
227
  )
223
228
  click.echo(f"Generating new user configuration file '{output}'")
224
229
  with fdp_session.FAIR(os.getcwd(), debug=debug) as fair_session:
@@ -264,9 +269,7 @@ def reset(debug: bool) -> None:
264
269
  default=False,
265
270
  )
266
271
  @click.option("--debug/--no-debug", help="Run in debug mode", default=False)
267
- @click.option(
268
- "--export", help="Export the CLI configuration to a file", default=""
269
- )
272
+ @click.option("--export", help="Export the CLI configuration to a file", default="")
270
273
  @click.option(
271
274
  "--local/--no-local",
272
275
  help="init without a remote registry - useful for closed systems",
@@ -293,7 +296,7 @@ def init(
293
296
  f"Cannot load CLI configuration from file '{using}', "
294
297
  "file does not exist."
295
298
  )
296
- _use_dict = yaml.safe_load(open(using, encoding='utf-8'))
299
+ _use_dict = yaml.safe_load(open(using, encoding="utf-8"))
297
300
 
298
301
  fair_session.initialise(
299
302
  using=_use_dict,
@@ -308,6 +311,7 @@ def init(
308
311
  if e.level.lower() == "error":
309
312
  sys.exit(e.exit_code)
310
313
 
314
+
311
315
  @cli.command()
312
316
  @click.option(
313
317
  "glob",
@@ -343,8 +347,7 @@ def purge(glob: bool, debug: bool, yes: bool, data: bool, all: bool) -> None:
343
347
  )
344
348
  else:
345
349
  _purge = click.confirm(
346
- "Are you sure you want to reset FAIR tracking, "
347
- "this is not reversible?"
350
+ "Are you sure you want to reset FAIR tracking, " "this is not reversible?"
348
351
  )
349
352
  if data:
350
353
  data = click.confirm(
@@ -425,7 +428,7 @@ def start(debug: bool, port: int, address: str) -> None:
425
428
  server_mode=fdp_svr.SwitchMode.USER_START,
426
429
  debug=debug,
427
430
  server_port=port,
428
- server_address=address
431
+ server_address=address,
429
432
  )
430
433
  except fdp_exc.FAIRCLIException as e:
431
434
  e.err_print()
@@ -438,11 +441,7 @@ def start(debug: bool, port: int, address: str) -> None:
438
441
  @click.option("--debug/--no-debug", help="Run in debug mode", default=False)
439
442
  def stop(force: bool, debug: bool) -> None:
440
443
  """Stop the local registry server"""
441
- _mode = (
442
- fdp_svr.SwitchMode.FORCE_STOP
443
- if force
444
- else fdp_svr.SwitchMode.USER_STOP
445
- )
444
+ _mode = fdp_svr.SwitchMode.FORCE_STOP if force else fdp_svr.SwitchMode.USER_STOP
446
445
  try:
447
446
  fdp_session.FAIR(os.getcwd(), server_mode=_mode, debug=debug)
448
447
  except fdp_exc.FAIRCLIException as e:
@@ -519,7 +518,7 @@ def add(identifier: str, debug: bool) -> None:
519
518
  """
520
519
  Add a data product or coderun to staging
521
520
 
522
- Data Products should be formatted:
521
+ Data Products should be formatted:
523
522
  <namespace>:<data product name>@v<version>
524
523
  eg:
525
524
  PSU:SEIRS_model/parameters@v1.0.0
@@ -527,16 +526,14 @@ def add(identifier: str, debug: bool) -> None:
527
526
  Code Runs should be specified by code run uuid
528
527
 
529
528
  Use `fair list` to view Data Products and Code Runs
530
-
529
+
531
530
  """
532
531
  try:
533
532
  with fdp_session.FAIR(
534
533
  os.getcwd(),
535
534
  debug=debug,
536
535
  ) as fair_session:
537
- fair_session.add_to_staging(
538
- identifier
539
- )
536
+ fair_session.add_to_staging(identifier)
540
537
  except fdp_exc.FAIRCLIException as e:
541
538
  e.err_print()
542
539
  if e.level.lower() == "error":
@@ -551,9 +548,7 @@ def add(identifier: str, debug: bool) -> None:
551
548
  default=False,
552
549
  help="remove from tracking but do not delete from file system",
553
550
  )
554
- def rm(
555
- job_ids: typing.List[str], cached: bool = False, debug: bool = False
556
- ) -> None:
551
+ def rm(job_ids: typing.List[str], cached: bool = False, debug: bool = False) -> None:
557
552
  """Removes jobs from system or just tracking"""
558
553
  pass
559
554
 
@@ -581,9 +576,7 @@ def rm(
581
576
  help="init without a remote registry - useful for closed systems",
582
577
  default=False,
583
578
  )
584
- def run(
585
- config: str, script: str, debug: bool, ci: bool, dirty: bool, local: bool
586
- ):
579
+ def run(config: str, script: str, debug: bool, ci: bool, dirty: bool, local: bool):
587
580
  """Initialises a job with the option to specify a bash command"""
588
581
  # Allow no config to be specified, if that is the case use default local
589
582
  click.echo("Running run please wait")
@@ -597,9 +590,7 @@ def run(
597
590
  allow_dirty=dirty,
598
591
  local=local,
599
592
  ) as fair_session:
600
- _hash = fair_session.run(
601
- script, passive=ci, allow_dirty=dirty, local=local
602
- )
593
+ _hash = fair_session.run(script, passive=ci, allow_dirty=dirty, local=local)
603
594
  if ci:
604
595
  click.echo(fdp_run.get_job_dir(_hash))
605
596
  except fdp_exc.FAIRCLIException as e:
@@ -59,9 +59,7 @@ FAIR_REGISTRY_REPO = "https://github.com/FAIRDataPipeline/data-registry.git"
59
59
  DEFAULT_REGISTRY_DOMAIN = "https://data.fairdatapipeline.org/"
60
60
  REGISTRY_INSTALL_URL = "https://data.fairdatapipeline.org/static/localregistry.sh"
61
61
 
62
- DEFAULT_REGISTRY_LOCATION = os.path.join(
63
- pathlib.Path().home(), FAIR_FOLDER, "registry"
64
- )
62
+ DEFAULT_REGISTRY_LOCATION = os.path.join(pathlib.Path().home(), FAIR_FOLDER, "registry")
65
63
 
66
64
  DEFAULT_LOCAL_REGISTRY_URL = "http://127.0.0.1:8000/api/"
67
65
 
@@ -79,7 +77,7 @@ def registry_home() -> str:
79
77
  return os.environ["FAIR_REGISTRY_DIR"]
80
78
  else:
81
79
  return DEFAULT_REGISTRY_LOCATION
82
- _glob_conf = yaml.safe_load(open(global_fdpconfig(), encoding='utf-8'))
80
+ _glob_conf = yaml.safe_load(open(global_fdpconfig(), encoding="utf-8"))
83
81
  if not _glob_conf:
84
82
  return DEFAULT_REGISTRY_LOCATION
85
83
  if "registries" not in _glob_conf:
@@ -149,6 +147,7 @@ def registry_session_port_file(registry_dir: str = None) -> str:
149
147
  registry_dir = registry_home()
150
148
  return os.path.join(registry_dir, "session_port.log")
151
149
 
150
+
152
151
  def registry_session_address_file(registry_dir: str = None) -> str:
153
152
  """Retrieve the location of the registry session port file
154
153
 
@@ -183,7 +182,10 @@ def registry_session_port(registry_dir: str = None) -> int:
183
182
  int
184
183
  current/most recent port used to launch the registry
185
184
  """
186
- return int(open(registry_session_port_file(registry_dir), encoding='utf-8').read().strip())
185
+ return int(
186
+ open(registry_session_port_file(registry_dir), encoding="utf-8").read().strip()
187
+ )
188
+
187
189
 
188
190
  def registry_session_address(registry_dir: str = None) -> str:
189
191
  """Retrieve the registry session address
@@ -202,11 +204,17 @@ def registry_session_address(registry_dir: str = None) -> str:
202
204
  current/most recent address used to launch the registry
203
205
  """
204
206
  if not os.path.exists(registry_session_address_file(registry_dir)):
205
- _logger.warning("Session Address file not found, please make sure your registry is up-to-date")
207
+ _logger.warning(
208
+ "Session Address file not found, please make sure your registry is up-to-date"
209
+ )
206
210
  _logger.info("Using 127.0.0.1")
207
211
  return "127.0.0.1"
208
212
 
209
- _address = open(registry_session_address_file(registry_dir), encoding='utf-8').read().strip()
213
+ _address = (
214
+ open(registry_session_address_file(registry_dir), encoding="utf-8")
215
+ .read()
216
+ .strip()
217
+ )
210
218
  if _address != "0.0.0.0":
211
219
  return _address
212
220
  else:
@@ -226,15 +234,13 @@ def default_data_dir(location: str = "local") -> str:
226
234
  raise fdp_exc.InternalError(
227
235
  f"Failed to read CLI global config file '{global_fdpconfig()}'"
228
236
  )
229
- _glob_conf = yaml.safe_load(open(global_fdpconfig(), encoding='utf-8'))
237
+ _glob_conf = yaml.safe_load(open(global_fdpconfig(), encoding="utf-8"))
230
238
  if "data_store" in _glob_conf["registries"][location]:
231
239
  return _glob_conf["registries"][location]["data_store"]
232
240
  if location == "local":
233
241
  return os.path.join(USER_FAIR_DIR, f"data{os.path.sep}")
234
242
  else:
235
- raise fdp_exc.UserConfigError(
236
- "Cannot guess remote data store location"
237
- )
243
+ raise fdp_exc.UserConfigError("Cannot guess remote data store location")
238
244
 
239
245
 
240
246
  def local_fdpconfig(user_loc: str = os.getcwd()) -> str:
@@ -291,16 +297,18 @@ def find_git_root(start_directory: str = os.getcwd()) -> str:
291
297
 
292
298
  return _repository.git.rev_parse("--show-toplevel").strip()
293
299
 
300
+
294
301
  def set_file_permissions(path: str):
295
302
  for root, dirs, files in os.walk(path, topdown=False):
296
- for dir in [os.path.join(root,d) for d in dirs]:
303
+ for dir in [os.path.join(root, d) for d in dirs]:
297
304
  os.chmod(dir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
298
305
  for file in [os.path.join(root, f) for f in files]:
299
306
  os.chmod(file, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
300
307
 
308
+
301
309
  def remove_readonly(fn, path, excinfo):
302
310
  try:
303
311
  os.chmod(path, stat.S_IWRITE)
304
312
  fn(path)
305
313
  except Exception as exc:
306
- print("Skipped:", path, "because:\n", exc)
314
+ print("Skipped:", path, "because:\n", exc)