vdi-client 0.1.0__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.
vdi_client/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ # SPDX-License-Identifier: GPL-2.0-only
2
+ # Copyright (C) 2025-2026 VDI contributors
3
+ """
4
+ VDI client - client tools for the virtual data infrastructure.
5
+
6
+ This module provides initialization logic for the package.
7
+ """
8
+
9
+ try:
10
+ from importlib.metadata import version
11
+
12
+ __version__ = version("vdi-client")
13
+ except Exception: # pragma: no cover
14
+ __version__ = "unknown"
vdi_client/cli.py ADDED
@@ -0,0 +1,389 @@
1
+ # SPDX-License-Identifier: GPL-2.0-only
2
+ # Copyright (C) 2025-2026 VDI contributors
3
+ """
4
+ VDI client - command-line interface.
5
+
6
+ The CLI provides three subcommands:
7
+
8
+ - ``run`` : run a user program with VDI extensions (``LD_PRELOAD``)
9
+ - ``view`` : create, list, delete, inspect, upload to and remove files
10
+ from views on a VDI server
11
+ - ``log`` : list, inspect and clean up VDI log files
12
+
13
+ Common arguments (accepted before the subcommand-specific arguments):
14
+
15
+ --base-url - base URL for VDI server to be accessed
16
+ --config - full path to config file [default: ``${HOME}/.vdi/config``]
17
+ -h - print usage for command
18
+ -v - verbose output
19
+ --dry-run - only print what the command would do without performing actions
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import argparse
25
+ import sys
26
+ from pathlib import Path
27
+ from typing import Any
28
+
29
+ from vdi_client import __version__
30
+ from vdi_client.config import DEFAULT_CONFIG_PATH, load_config, resolve_base_url
31
+ from vdi_client.log import clean_logs, list_logs, show_log
32
+ from vdi_client.run import run as run_command
33
+ from vdi_client.view import (
34
+ create_view,
35
+ delete_view,
36
+ get_url,
37
+ list_files,
38
+ list_views,
39
+ remove_file,
40
+ upload_file,
41
+ )
42
+
43
+
44
+ def _add_common_args(parser: argparse.ArgumentParser, *, suppress_defaults: bool = False) -> None:
45
+ """Add common arguments shared by all subcommands.
46
+
47
+ When *suppress_defaults* is ``True`` the arguments use
48
+ :data:`argparse.SUPPRESS` as their default so that they do not
49
+ overwrite values already set by a parent parser.
50
+ """
51
+ base_url_default: Any = argparse.SUPPRESS if suppress_defaults else None
52
+ config_default: Any = argparse.SUPPRESS if suppress_defaults else None
53
+ verbose_default: Any = argparse.SUPPRESS if suppress_defaults else False
54
+ dry_run_default: Any = argparse.SUPPRESS if suppress_defaults else False
55
+
56
+ parser.add_argument(
57
+ "--base-url",
58
+ default=base_url_default,
59
+ help="base url for VDI server to be accessed",
60
+ )
61
+ parser.add_argument(
62
+ "--config",
63
+ type=Path,
64
+ default=config_default,
65
+ help="full path to config file [default: ${HOME}/.vdi/config]",
66
+ )
67
+ parser.add_argument(
68
+ "-v",
69
+ dest="verbose",
70
+ action="store_true",
71
+ default=verbose_default,
72
+ help="verbose output",
73
+ )
74
+ parser.add_argument(
75
+ "--dry-run",
76
+ dest="dry_run",
77
+ action="store_true",
78
+ default=dry_run_default,
79
+ help="only print what command would do without actually performing the actions",
80
+ )
81
+
82
+
83
+ def create_parser() -> argparse.ArgumentParser:
84
+ """Create and return the top-level argument parser for the VDI CLI.
85
+
86
+ Returns:
87
+ The configured :class:`argparse.ArgumentParser` instance.
88
+ """
89
+ parser = argparse.ArgumentParser(
90
+ prog="vdi",
91
+ description="Client tools for the virtual data infrastructure (VDI).",
92
+ )
93
+ parser.add_argument(
94
+ "--version",
95
+ action="version",
96
+ version=f"vdi-client {__version__}",
97
+ )
98
+ subparsers = parser.add_subparsers(dest="command")
99
+
100
+ # --- run subcommand ---
101
+ run_parser = subparsers.add_parser(
102
+ "run",
103
+ help="run the user program with the given user arguments",
104
+ description="Run the user program with the given user arguments.",
105
+ )
106
+ _add_common_args(run_parser)
107
+ run_parser.add_argument(
108
+ "--lib",
109
+ dest="lib",
110
+ default=None,
111
+ help="path to libvdi.so (overrides VDI_LIB_PATH env var and default location)",
112
+ )
113
+ run_parser.add_argument(
114
+ "program",
115
+ nargs="?",
116
+ help="path to program to be run",
117
+ )
118
+ run_parser.add_argument(
119
+ "program_args",
120
+ nargs=argparse.REMAINDER,
121
+ help="any arguments to the program to be run",
122
+ )
123
+
124
+ # --- view subcommand ---
125
+ view_parser = subparsers.add_parser(
126
+ "view",
127
+ help="create, list and delete views",
128
+ description="Create, list and delete views on a VDI server.",
129
+ )
130
+ _add_common_args(view_parser)
131
+ view_sub = view_parser.add_subparsers(dest="subcommand")
132
+
133
+ create_p = view_sub.add_parser("create", help="create a new view")
134
+ _add_common_args(create_p, suppress_defaults=True)
135
+ create_p.add_argument("view_name", help="name of the view to be created")
136
+
137
+ delete_p = view_sub.add_parser("delete", help="delete a view")
138
+ _add_common_args(delete_p, suppress_defaults=True)
139
+ delete_p.add_argument("view_name", help="name of the view to be deleted")
140
+
141
+ files_p = view_sub.add_parser("files", help="list files in a view")
142
+ _add_common_args(files_p, suppress_defaults=True)
143
+ files_p.add_argument("view_name", help="name of the view to show the files it contains")
144
+
145
+ geturl_p = view_sub.add_parser("geturl", help="print the download URL for a file in a view")
146
+ _add_common_args(geturl_p, suppress_defaults=True)
147
+ geturl_p.add_argument("view_name", help="name of the view that contains the file")
148
+ geturl_p.add_argument("file_name", help="name of the file for which the URL should be shown")
149
+
150
+ list_p = view_sub.add_parser("list", help="list all views")
151
+ _add_common_args(list_p, suppress_defaults=True)
152
+
153
+ remove_p = view_sub.add_parser("remove", help="remove a file from a view")
154
+ _add_common_args(remove_p, suppress_defaults=True)
155
+ remove_p.add_argument("view_name", help="name of the view that contains the file")
156
+ remove_p.add_argument("file_name", help="name of the file to be removed from the view")
157
+
158
+ upload_p = view_sub.add_parser("upload", help="upload a file to a view")
159
+ _add_common_args(upload_p, suppress_defaults=True)
160
+ upload_p.add_argument("view_name", help="name of the view")
161
+ upload_p.add_argument("file_name", help="path to the file that should be uploaded to the view")
162
+
163
+ # --- log subcommand ---
164
+ log_parser = subparsers.add_parser(
165
+ "log",
166
+ help="list, show and clean up VDI log files",
167
+ description="List, show and clean up VDI log files.",
168
+ )
169
+ _add_common_args(log_parser)
170
+ log_parser.add_argument(
171
+ "--log-dir",
172
+ dest="log_dir",
173
+ default=None,
174
+ help="directory containing log files [default: $VDI_LOG_DIR or ~/.vdi/logs/]",
175
+ )
176
+ log_parser.add_argument(
177
+ "--log-prefix",
178
+ dest="log_prefix",
179
+ default=None,
180
+ help="prefix for log file names [default: $VDI_LOG_FILE_PREFIX or 'vdi_log.']",
181
+ )
182
+ log_sub = log_parser.add_subparsers(dest="subcommand")
183
+
184
+ log_list_p = log_sub.add_parser("list", help="list log files")
185
+ _add_common_args(log_list_p, suppress_defaults=True)
186
+ log_list_p.add_argument(
187
+ "--log-dir",
188
+ dest="log_dir",
189
+ default=argparse.SUPPRESS,
190
+ help="directory containing log files [default: $VDI_LOG_DIR or ~/.vdi/logs/]",
191
+ )
192
+ log_list_p.add_argument(
193
+ "--log-prefix",
194
+ dest="log_prefix",
195
+ default=argparse.SUPPRESS,
196
+ help="prefix for log file names [default: $VDI_LOG_FILE_PREFIX or 'vdi_log.']",
197
+ )
198
+ log_list_p.add_argument(
199
+ "--sort",
200
+ dest="sort_key",
201
+ choices=["pid", "date", "size", "program"],
202
+ default="pid",
203
+ help="sort log files by the given key [default: pid]",
204
+ )
205
+ log_list_p.add_argument(
206
+ "-r",
207
+ "--reverse",
208
+ dest="reverse",
209
+ action="store_true",
210
+ default=False,
211
+ help="reverse sort order",
212
+ )
213
+ log_list_p.add_argument(
214
+ "--long",
215
+ dest="long_format",
216
+ action="store_true",
217
+ default=False,
218
+ help="show size, modification time and program name",
219
+ )
220
+
221
+ log_show_p = log_sub.add_parser("show", help="print the contents of a log file")
222
+ _add_common_args(log_show_p, suppress_defaults=True)
223
+ log_show_p.add_argument(
224
+ "--log-dir",
225
+ dest="log_dir",
226
+ default=argparse.SUPPRESS,
227
+ help="directory containing log files [default: $VDI_LOG_DIR or ~/.vdi/logs/]",
228
+ )
229
+ log_show_p.add_argument(
230
+ "--log-prefix",
231
+ dest="log_prefix",
232
+ default=argparse.SUPPRESS,
233
+ help="prefix for log file names [default: $VDI_LOG_FILE_PREFIX or 'vdi_log.']",
234
+ )
235
+ log_show_p.add_argument("pid", type=int, help="process ID of the log file to show")
236
+
237
+ log_clean_p = log_sub.add_parser("clean", help="remove log files")
238
+ _add_common_args(log_clean_p, suppress_defaults=True)
239
+ log_clean_p.add_argument(
240
+ "--log-dir",
241
+ dest="log_dir",
242
+ default=argparse.SUPPRESS,
243
+ help="directory containing log files [default: $VDI_LOG_DIR or ~/.vdi/logs/]",
244
+ )
245
+ log_clean_p.add_argument(
246
+ "--log-prefix",
247
+ dest="log_prefix",
248
+ default=argparse.SUPPRESS,
249
+ help="prefix for log file names [default: $VDI_LOG_FILE_PREFIX or 'vdi_log.']",
250
+ )
251
+ log_clean_p.add_argument(
252
+ "--pid",
253
+ type=int,
254
+ default=None,
255
+ help="remove only the log file for this PID (default: remove all)",
256
+ )
257
+
258
+ return parser
259
+
260
+
261
+ def _handle_run(args: argparse.Namespace) -> int:
262
+ """Dispatch the ``run`` subcommand."""
263
+ return run_command(
264
+ program=args.program,
265
+ program_args=args.program_args,
266
+ lib_override=args.lib,
267
+ dry_run=args.dry_run,
268
+ verbose=args.verbose,
269
+ )
270
+
271
+
272
+ def _handle_view(args: argparse.Namespace) -> int:
273
+ """Dispatch the ``view`` subcommand and its sub-subcommands."""
274
+ config = load_config(args.config if args.config is not None else DEFAULT_CONFIG_PATH)
275
+ base_url = resolve_base_url(args.base_url, config)
276
+
277
+ if args.subcommand is None:
278
+ print("Error: no view subcommand specified.", file=sys.stderr)
279
+ print(
280
+ "Subcommands: create, delete, files, geturl, list, remove, upload",
281
+ file=sys.stderr,
282
+ )
283
+ return 1
284
+
285
+ # geturl does not require a server connection
286
+ if args.subcommand == "geturl":
287
+ if base_url is None:
288
+ print(
289
+ "Error: BASE_URL is not set. Provide it via --base-url or config file.",
290
+ file=sys.stderr,
291
+ )
292
+ return 1
293
+ return get_url(base_url, args.view_name, args.file_name)
294
+
295
+ if base_url is None:
296
+ print(
297
+ "Error: BASE_URL is not set. Provide it via --base-url or config file.",
298
+ file=sys.stderr,
299
+ )
300
+ return 1
301
+
302
+ if args.subcommand == "create":
303
+ return create_view(base_url, args.view_name, dry_run=args.dry_run, verbose=args.verbose)
304
+ elif args.subcommand == "delete":
305
+ return delete_view(base_url, args.view_name, dry_run=args.dry_run, verbose=args.verbose)
306
+ elif args.subcommand == "files":
307
+ return list_files(base_url, args.view_name, dry_run=args.dry_run, verbose=args.verbose)
308
+ elif args.subcommand == "list":
309
+ return list_views(base_url, dry_run=args.dry_run, verbose=args.verbose)
310
+ elif args.subcommand == "remove":
311
+ return remove_file(base_url, args.view_name, args.file_name, dry_run=args.dry_run, verbose=args.verbose)
312
+ elif args.subcommand == "upload":
313
+ return upload_file(base_url, args.view_name, args.file_name, dry_run=args.dry_run, verbose=args.verbose)
314
+
315
+ print(f"Error: unknown view subcommand '{args.subcommand}'.", file=sys.stderr)
316
+ return 1
317
+
318
+
319
+ def _handle_log(args: argparse.Namespace) -> int:
320
+ """Dispatch the ``log`` subcommand and its sub-subcommands."""
321
+ log_dir = getattr(args, "log_dir", None)
322
+ log_prefix = getattr(args, "log_prefix", None)
323
+
324
+ if args.subcommand is None:
325
+ print("Error: no log subcommand specified.", file=sys.stderr)
326
+ print("Subcommands: list, show, clean", file=sys.stderr)
327
+ return 1
328
+
329
+ if args.subcommand == "list":
330
+ return list_logs(
331
+ log_dir_override=log_dir,
332
+ log_prefix_override=log_prefix,
333
+ sort_key=args.sort_key,
334
+ reverse=args.reverse,
335
+ long_format=args.long_format,
336
+ dry_run=args.dry_run,
337
+ verbose=args.verbose,
338
+ )
339
+ elif args.subcommand == "show":
340
+ return show_log(
341
+ args.pid,
342
+ log_dir_override=log_dir,
343
+ log_prefix_override=log_prefix,
344
+ dry_run=args.dry_run,
345
+ verbose=args.verbose,
346
+ )
347
+ elif args.subcommand == "clean":
348
+ return clean_logs(
349
+ pid=args.pid,
350
+ log_dir_override=log_dir,
351
+ log_prefix_override=log_prefix,
352
+ dry_run=args.dry_run,
353
+ verbose=args.verbose,
354
+ )
355
+
356
+ print(f"Error: unknown log subcommand '{args.subcommand}'.", file=sys.stderr)
357
+ return 1
358
+
359
+
360
+ def main(argv: list[str] | None = None) -> int:
361
+ """Entry point for the VDI CLI.
362
+
363
+ Args:
364
+ argv: Optional list of command-line arguments. If ``None``,
365
+ :data:`sys.argv` is used (via :mod:`argparse`).
366
+
367
+ Returns:
368
+ Process exit code (0 on success, non-zero on error).
369
+ """
370
+ parser = create_parser()
371
+ args = parser.parse_args(argv)
372
+
373
+ if args.command is None:
374
+ parser.print_help()
375
+ return 0
376
+
377
+ if args.command == "run":
378
+ return _handle_run(args)
379
+ elif args.command == "view":
380
+ return _handle_view(args)
381
+ elif args.command == "log":
382
+ return _handle_log(args)
383
+
384
+ parser.print_help()
385
+ return 1
386
+
387
+
388
+ if __name__ == "__main__":
389
+ sys.exit(main())
vdi_client/config.py ADDED
@@ -0,0 +1,129 @@
1
+ # SPDX-License-Identifier: GPL-2.0-only
2
+ # Copyright (C) 2025-2026 VDI contributors
3
+ """
4
+ VDI client - configuration file handling.
5
+
6
+ Loads optional shell-style configuration files that may define ``BASE_URL``
7
+ and other environment variables used by the VDI client.
8
+
9
+ This module also defines constants and resolution helpers for log file
10
+ location and naming (used by the ``log`` subcommand).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+ from pathlib import Path
17
+
18
+ DEFAULT_CONFIG_PATH = Path.home() / ".vdi" / "config"
19
+
20
+ # --- Log file configuration ---
21
+
22
+ # Environment variables that configure log file location and naming.
23
+ VDI_LOG_DIR_ENV = "VDI_LOG_DIR"
24
+ VDI_LOG_PREFIX_ENV = "VDI_LOG_FILE_PREFIX"
25
+
26
+ # Defaults matching the wrapper library behaviour.
27
+ DEFAULT_LOG_DIR = Path.home() / ".vdi" / "logs"
28
+ DEFAULT_LOG_PREFIX = "vdi_log."
29
+
30
+
31
+ def resolve_log_dir(log_dir_override: str | None = None) -> Path:
32
+ """Resolve the directory where VDI log files are stored.
33
+
34
+ The lookup order is:
35
+
36
+ 1. Explicit ``log_dir_override`` (from ``--log-dir`` flag).
37
+ 2. ``VDI_LOG_DIR`` environment variable.
38
+ 3. ``${HOME}/.vdi/logs/`` (default).
39
+
40
+ Args:
41
+ log_dir_override: Optional explicit path passed on the command line.
42
+
43
+ Returns:
44
+ The resolved :class:`~pathlib.Path` to the log directory.
45
+ """
46
+ if log_dir_override:
47
+ return Path(log_dir_override)
48
+ env_dir = os.environ.get(VDI_LOG_DIR_ENV)
49
+ if env_dir:
50
+ return Path(env_dir)
51
+ return DEFAULT_LOG_DIR
52
+
53
+
54
+ def resolve_log_prefix(log_prefix_override: str | None = None) -> str:
55
+ """Resolve the prefix used for VDI log file names.
56
+
57
+ The lookup order is:
58
+
59
+ 1. Explicit ``log_prefix_override`` (from ``--log-prefix`` flag).
60
+ 2. ``VDI_LOG_FILE_PREFIX`` environment variable.
61
+ 3. ``vdi_log.`` (default).
62
+
63
+ Args:
64
+ log_prefix_override: Optional explicit prefix passed on the command line.
65
+
66
+ Returns:
67
+ The resolved prefix string.
68
+ """
69
+ if log_prefix_override is not None:
70
+ return log_prefix_override
71
+ return os.environ.get(VDI_LOG_PREFIX_ENV, DEFAULT_LOG_PREFIX)
72
+
73
+
74
+ def load_config(config_path: Path | None = None) -> dict[str, str]:
75
+ """Load a VDI configuration file into a dictionary.
76
+
77
+ The config file is sourced shell-style: each line should be a
78
+ ``KEY=VALUE`` assignment. Lines starting with ``#`` are ignored.
79
+
80
+ Args:
81
+ config_path: Path to the config file. If ``None``, the default
82
+ path (``${HOME}/.vdi/config``) is used.
83
+
84
+ Returns:
85
+ A dictionary of configuration key-value pairs. Returns an empty
86
+ dict if the file does not exist.
87
+ """
88
+ if config_path is None:
89
+ config_path = DEFAULT_CONFIG_PATH
90
+
91
+ config: dict[str, str] = {}
92
+
93
+ if not config_path.is_file():
94
+ return config
95
+
96
+ with config_path.open("r", encoding="utf-8") as file:
97
+ for line in file:
98
+ line = line.strip()
99
+ if not line or line.startswith("#"):
100
+ continue
101
+ if "=" not in line:
102
+ continue
103
+ key, _, value = line.partition("=")
104
+ key = key.strip()
105
+ value = value.strip()
106
+ # Strip surrounding quotes if present
107
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
108
+ value = value[1:-1]
109
+ config[key] = value
110
+
111
+ return config
112
+
113
+
114
+ def resolve_base_url(
115
+ cli_base_url: str | None,
116
+ config: dict[str, str],
117
+ ) -> str | None:
118
+ """Resolve the VDI server base URL from CLI args or config.
119
+
120
+ Args:
121
+ cli_base_url: Base URL provided via ``--base-url`` on the command line.
122
+ config: Configuration dictionary loaded from the config file.
123
+
124
+ Returns:
125
+ The resolved base URL, or ``None`` if not set.
126
+ """
127
+ if cli_base_url:
128
+ return cli_base_url
129
+ return config.get("BASE_URL") or os.environ.get("BASE_URL")
Binary file
Binary file
Binary file