smart-tests-cli 2.0.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.
Files changed (96) hide show
  1. smart_tests/__init__.py +0 -0
  2. smart_tests/__main__.py +60 -0
  3. smart_tests/app.py +67 -0
  4. smart_tests/args4p/README.md +102 -0
  5. smart_tests/args4p/__init__.py +13 -0
  6. smart_tests/args4p/argument.py +45 -0
  7. smart_tests/args4p/command.py +593 -0
  8. smart_tests/args4p/converters/__init__.py +75 -0
  9. smart_tests/args4p/decorators.py +98 -0
  10. smart_tests/args4p/exceptions.py +12 -0
  11. smart_tests/args4p/option.py +85 -0
  12. smart_tests/args4p/parameter.py +84 -0
  13. smart_tests/args4p/typer/__init__.py +42 -0
  14. smart_tests/commands/__init__.py +0 -0
  15. smart_tests/commands/compare/__init__.py +11 -0
  16. smart_tests/commands/compare/subsets.py +58 -0
  17. smart_tests/commands/detect_flakes.py +105 -0
  18. smart_tests/commands/inspect/__init__.py +13 -0
  19. smart_tests/commands/inspect/model.py +52 -0
  20. smart_tests/commands/inspect/subset.py +138 -0
  21. smart_tests/commands/record/__init__.py +19 -0
  22. smart_tests/commands/record/attachment.py +38 -0
  23. smart_tests/commands/record/build.py +356 -0
  24. smart_tests/commands/record/case_event.py +190 -0
  25. smart_tests/commands/record/commit.py +157 -0
  26. smart_tests/commands/record/session.py +120 -0
  27. smart_tests/commands/record/tests.py +498 -0
  28. smart_tests/commands/stats/__init__.py +11 -0
  29. smart_tests/commands/stats/test_sessions.py +45 -0
  30. smart_tests/commands/subset.py +567 -0
  31. smart_tests/commands/test_path_writer.py +51 -0
  32. smart_tests/commands/verify.py +153 -0
  33. smart_tests/jar/exe_deploy.jar +0 -0
  34. smart_tests/plugins/__init__.py +0 -0
  35. smart_tests/test_runners/__init__.py +0 -0
  36. smart_tests/test_runners/adb.py +24 -0
  37. smart_tests/test_runners/ant.py +35 -0
  38. smart_tests/test_runners/bazel.py +103 -0
  39. smart_tests/test_runners/behave.py +62 -0
  40. smart_tests/test_runners/codeceptjs.py +33 -0
  41. smart_tests/test_runners/ctest.py +164 -0
  42. smart_tests/test_runners/cts.py +189 -0
  43. smart_tests/test_runners/cucumber.py +451 -0
  44. smart_tests/test_runners/cypress.py +46 -0
  45. smart_tests/test_runners/dotnet.py +106 -0
  46. smart_tests/test_runners/file.py +20 -0
  47. smart_tests/test_runners/flutter.py +251 -0
  48. smart_tests/test_runners/go_test.py +99 -0
  49. smart_tests/test_runners/googletest.py +34 -0
  50. smart_tests/test_runners/gradle.py +96 -0
  51. smart_tests/test_runners/jest.py +52 -0
  52. smart_tests/test_runners/maven.py +149 -0
  53. smart_tests/test_runners/minitest.py +40 -0
  54. smart_tests/test_runners/nunit.py +190 -0
  55. smart_tests/test_runners/playwright.py +252 -0
  56. smart_tests/test_runners/prove.py +74 -0
  57. smart_tests/test_runners/pytest.py +358 -0
  58. smart_tests/test_runners/raw.py +238 -0
  59. smart_tests/test_runners/robot.py +125 -0
  60. smart_tests/test_runners/rspec.py +5 -0
  61. smart_tests/test_runners/smart_tests.py +235 -0
  62. smart_tests/test_runners/vitest.py +49 -0
  63. smart_tests/test_runners/xctest.py +79 -0
  64. smart_tests/testpath.py +154 -0
  65. smart_tests/utils/__init__.py +0 -0
  66. smart_tests/utils/authentication.py +78 -0
  67. smart_tests/utils/ci_provider.py +7 -0
  68. smart_tests/utils/commands.py +14 -0
  69. smart_tests/utils/commit_ingester.py +59 -0
  70. smart_tests/utils/common_tz.py +12 -0
  71. smart_tests/utils/edit_distance.py +11 -0
  72. smart_tests/utils/env_keys.py +19 -0
  73. smart_tests/utils/exceptions.py +34 -0
  74. smart_tests/utils/fail_fast_mode.py +99 -0
  75. smart_tests/utils/file_name_pattern.py +4 -0
  76. smart_tests/utils/git_log_parser.py +53 -0
  77. smart_tests/utils/glob.py +44 -0
  78. smart_tests/utils/gzipgen.py +46 -0
  79. smart_tests/utils/http_client.py +169 -0
  80. smart_tests/utils/java.py +61 -0
  81. smart_tests/utils/link.py +149 -0
  82. smart_tests/utils/logger.py +53 -0
  83. smart_tests/utils/no_build.py +2 -0
  84. smart_tests/utils/sax.py +119 -0
  85. smart_tests/utils/session.py +73 -0
  86. smart_tests/utils/smart_tests_client.py +134 -0
  87. smart_tests/utils/subprocess.py +12 -0
  88. smart_tests/utils/tracking.py +95 -0
  89. smart_tests/utils/typer_types.py +241 -0
  90. smart_tests/version.py +7 -0
  91. smart_tests_cli-2.0.0.dist-info/METADATA +168 -0
  92. smart_tests_cli-2.0.0.dist-info/RECORD +96 -0
  93. smart_tests_cli-2.0.0.dist-info/WHEEL +5 -0
  94. smart_tests_cli-2.0.0.dist-info/entry_points.txt +2 -0
  95. smart_tests_cli-2.0.0.dist-info/licenses/LICENSE.txt +202 -0
  96. smart_tests_cli-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Optional, Type
4
+
5
+ from . import decorator
6
+ from .argument import Argument
7
+ from .command import Command, Group
8
+ from .exceptions import BadConfigException
9
+ from .option import NO_DEFAULT, Option
10
+ from .parameter import Parameter
11
+
12
+
13
+ def _command(
14
+ name: Optional[str] = None,
15
+ help: Optional[str] = None,
16
+ cls: Type[Command] = Command,
17
+ ):
18
+ def decorator(f: Callable) -> Command:
19
+ return cls(name=name, help=help, callback=f)
20
+
21
+ return decorator
22
+
23
+
24
+ @decorator
25
+ def command(name: Optional[str] = None, help: Optional[str] = None) -> Callable[..., Command]:
26
+ return _command(name, help, Command)
27
+
28
+
29
+ @decorator
30
+ def group(name: Optional[str] = None, help: Optional[str] = None) -> Callable[..., Group]:
31
+ return _command(name, help, Group) # type: ignore
32
+
33
+
34
+ @decorator
35
+ def option(
36
+ *param_decls: str,
37
+ help: str | None = None, type: type | Callable | None = None, default: Any = NO_DEFAULT, required: bool = False,
38
+ metavar: str | None = None, multiple: bool = False, hidden: bool = False
39
+ ) -> Callable:
40
+ '''
41
+ See README.md for usage
42
+ '''
43
+
44
+ def decorator(f: Callable) -> Callable:
45
+ if len(param_decls) == 0:
46
+ raise BadConfigException("Variable name is required")
47
+
48
+ variable_name = param_decls[-1]
49
+ if len(param_decls) == 1:
50
+ option_names = [f"--{variable_name.replace('_', '-')}"]
51
+ else:
52
+ option_names = list(param_decls[:-1])
53
+
54
+ o = Option(
55
+ name=variable_name,
56
+ option_names=option_names,
57
+ help=help,
58
+ type=type,
59
+ default=default,
60
+ required=required,
61
+ metavar=metavar,
62
+ multiple=multiple,
63
+ hidden=hidden)
64
+
65
+ return _attach(f, o)
66
+
67
+ return decorator
68
+
69
+
70
+ @decorator
71
+ def argument(
72
+ name: str,
73
+ type: type | Callable = str,
74
+ multiple: bool = False,
75
+ required: bool = True,
76
+ metavar: str | None = None,
77
+ help: str | None = None,
78
+ default: Any = NO_DEFAULT
79
+ ) -> Callable:
80
+ '''
81
+ See README.md for usage
82
+ '''
83
+ a = Argument(name=name, type=type, multiple=multiple, required=required, metavar=metavar, help=help, default=default)
84
+ return lambda f: _attach(f, a)
85
+
86
+
87
+ def _attach(f: Callable[..., Any], param: Parameter):
88
+ # depending on whether a command annotation comes before/after parameter annotations, 'f' might be
89
+ # a naked user-defined function or a Command instance
90
+ if isinstance(f, Command):
91
+ f.add_param(param, True)
92
+ else:
93
+ if not hasattr(f, "__args4p_params__"):
94
+ f.__args4p_params__ = [] # type: ignore
95
+
96
+ f.__args4p_params__.append(param) # type: ignore
97
+
98
+ return f
@@ -0,0 +1,12 @@
1
+ class BadCmdLineException(Exception):
2
+ '''
3
+ Indicates that arguments given by the user are invalid.
4
+
5
+ The message gets printed, and the CLI exists with non-zero.
6
+ '''
7
+
8
+
9
+ class BadConfigException(Exception):
10
+ '''
11
+ Indicates that the option/command/argument declarations are invalid
12
+ '''
@@ -0,0 +1,85 @@
1
+ from typing import Any, Callable, Optional
2
+
3
+ from .exceptions import BadCmdLineException
4
+ from .parameter import Parameter, normalize_type
5
+
6
+
7
+ class NoDefault:
8
+ '''
9
+ If there's no default value configured for option/argument, we use `NO_DEFAULT`.
10
+ In contrast, `None` is a valid and very typical default value.
11
+ '''
12
+ pass
13
+
14
+
15
+ NO_DEFAULT = NoDefault()
16
+
17
+
18
+ class Option(Parameter):
19
+ clazz = "option"
20
+
21
+ hidden: bool
22
+
23
+ def __init__(
24
+ self, name: Optional[str],
25
+ option_names: list[str],
26
+ help: str | None = None,
27
+ type: type | Callable[..., Any] | None = str,
28
+ default: Any = NO_DEFAULT,
29
+ required: bool = False,
30
+ metavar: str | None = None,
31
+ multiple: bool = False,
32
+ hidden: bool = False):
33
+ self.name = name # type: ignore[assignment] # once properly constructed, name is never None
34
+ self.option_names = option_names
35
+ self.help = help
36
+ self.type = normalize_type(type)
37
+ self.default = default
38
+ self.required = required
39
+ self.metavar = metavar
40
+ self.multiple = multiple
41
+ self.hidden = hidden
42
+
43
+ def append(self, existing: Any, option_name: str, args): # args is ArgList, but typing it creates a circular import
44
+ '''
45
+ Given the current value 'existing' that represents the present value to invoke the user function with,
46
+ this method is called when this option was specified as 'option_name' on the command line.
47
+ 'args' is pointing at the next argument after 'option_name', which may be the value for this option.
48
+ '''
49
+
50
+ if self.type == bool or self.type == Optional[bool]:
51
+ v = True
52
+ else:
53
+ v = args.eat(option_name)
54
+ try:
55
+ v = self.type(v)
56
+ except ValueError as e:
57
+ raise BadCmdLineException(f"Invalid value '{v}' for option '{option_name}': {str(e)}") from e
58
+
59
+ if self.multiple:
60
+ if existing is None:
61
+ existing = []
62
+ existing.append(v)
63
+ return existing
64
+ else:
65
+ return v
66
+
67
+ def attach_to_command(self, command): # typing command makes reference circular
68
+ super().attach_to_command(command)
69
+
70
+ if self.metavar is None:
71
+ def type_name(t):
72
+ if hasattr(t, "__name__"):
73
+ return t.__name__
74
+ else:
75
+ return str(t)
76
+ self.metavar = type_name(self.type).upper()
77
+ if self.type == bool and self.default is NO_DEFAULT:
78
+ # if the flag is absent, bind the value to False, or else the function signature requires a defalut value,
79
+ # which is silly
80
+ self.default = False
81
+
82
+ def __repr__(self):
83
+ return (f"Option(name={self.name!r}, option_names={self.option_names!r}, help={self.help!r}, "
84
+ f"type={self.type.__name__!r}, default={self.default!r}, required={self.required!r}, "
85
+ f"metavar={self.metavar!r}, many={self.multiple!r})")
@@ -0,0 +1,84 @@
1
+ import inspect
2
+ import types
3
+ from typing import Annotated, Any, Callable, Optional, Union, get_args, get_origin
4
+
5
+ from smart_tests.args4p.exceptions import BadConfigException
6
+
7
+
8
+ def to_type(p: inspect.Parameter) -> Optional[type]:
9
+ '''
10
+ Given output from inspect.signature, extract the type annotation.
11
+ '''
12
+ annotation = p.annotation
13
+ if annotation == inspect.Parameter.empty:
14
+ return None
15
+
16
+ # we expect a List[something] and we want to extract 'something'
17
+
18
+ origin = get_origin(annotation)
19
+ if origin is Annotated:
20
+ return get_args(annotation)[0]
21
+
22
+ return annotation
23
+
24
+
25
+ def normalize_type(t) -> type:
26
+ if isinstance(t, types.UnionType) or get_origin(t) is Union:
27
+ # x|None is a common typing of a parameter that confuses args4p.
28
+ # we want to normalize it to just x
29
+ # Not sure when UnionType is used and when Union is used, but they both seem to appear
30
+ args = get_args(t)
31
+ if len(args) == 2 and args[1] is type(None):
32
+ return args[0]
33
+
34
+ return t
35
+
36
+
37
+ class Parameter:
38
+ '''
39
+ Common parts of Argument and Option
40
+ '''
41
+
42
+ # the name of the argument, used as the variable name in the user function
43
+ # when created from typer.Option or typer.Argument, this is not set until attached to a command
44
+ name: str
45
+
46
+ multiple: bool # True if this argument can appear multiple times
47
+ required: bool # True if this argument is required
48
+ metavar: str | None # the name to use in help messages for the argument value
49
+ help: str | None # the help message for this argument
50
+ default: Any # the default value if the argument/option is not provided
51
+ clazz: str # "argument" or "option"
52
+
53
+ # convert the string argument to a value.
54
+ # For multiple=True, this is the type of each individual value.
55
+ # 'type' object itself, like 'int' is a convenient callable to do just that
56
+ type: type | Callable
57
+
58
+ def attach_to_command(self, command): # typing command makes reference circular
59
+ def error(msg: str):
60
+ raise BadConfigException(
61
+ f"{msg} in function '{command.callback.__name__}': "
62
+ f"{inspect.getsourcefile(command.callback)}:{inspect.getsourcelines(command.callback)[1]}")
63
+
64
+ for name, param in inspect.signature(command.callback).parameters.items():
65
+ if name == self.name:
66
+ # we found the parameter that matches the name
67
+ if self.type is None:
68
+ def infer_type() -> type:
69
+ t = normalize_type(to_type(param))
70
+ if t is None:
71
+ raise error(f"Type annotation is missing on parameter '{name}'")
72
+ if self.multiple:
73
+ # we expect a List[something] and we want to extract 'something'
74
+ if get_origin(t) is list:
75
+ return get_args(t)[0]
76
+ raise error(f"multiple=True requires a List[T] type annotation with parameter '{name}'")
77
+ else:
78
+ return t
79
+
80
+ self.type = infer_type()
81
+
82
+ return
83
+
84
+ raise error(f"No parameter named '{self.name}' found")
@@ -0,0 +1,42 @@
1
+ # This package defines Typer-style annotation based option/command declarations
2
+ from typing import Any, Callable
3
+
4
+ from ..argument import Argument as _Argument
5
+ from ..option import NO_DEFAULT
6
+ from ..option import Option as _Option
7
+
8
+
9
+ def Option(
10
+ *option_names: str,
11
+ help: str | None = None, type: type | Callable | None = None, default: Any = NO_DEFAULT, required: bool = False,
12
+ metavar: str | None = None, multiple: bool = False, hidden: bool = False
13
+ ) -> _Option:
14
+ '''
15
+ See README.md for usage
16
+ '''
17
+
18
+ return _Option(name=None, option_names=list(option_names), help=help, type=type,
19
+ default=default, required=required, metavar=metavar, multiple=multiple, hidden=hidden)
20
+
21
+
22
+ def Argument(
23
+ type: type | Callable = str,
24
+ multiple: bool = False,
25
+ required: bool = True,
26
+ metavar: str | None = None,
27
+ help: str | None = None,
28
+ default: Any = NO_DEFAULT
29
+ ) -> _Argument:
30
+ '''
31
+ See README.md for usage
32
+ '''
33
+ return _Argument(name=None, type=type, multiple=multiple, required=required, metavar=metavar, help=help, default=default)
34
+
35
+
36
+ class Exit(Exception):
37
+ '''
38
+ Raise this exception to exit the CLI with the given exit code
39
+ '''
40
+
41
+ def __init__(self, code: int):
42
+ self.code = code
File without changes
@@ -0,0 +1,11 @@
1
+ from ... import args4p
2
+ from ...app import Application
3
+ from .subsets import subsets
4
+
5
+
6
+ @args4p.group()
7
+ def compare(app: Application):
8
+ return app
9
+
10
+
11
+ compare.add_command(subsets)
@@ -0,0 +1,58 @@
1
+ from pathlib import Path
2
+ from typing import Annotated, List, Tuple, Union
3
+
4
+ import click
5
+ from tabulate import tabulate
6
+
7
+ import smart_tests.args4p.typer as typer
8
+ from smart_tests import args4p
9
+ from smart_tests.app import Application
10
+ from smart_tests.args4p.converters import path
11
+
12
+
13
+ @args4p.command()
14
+ def subsets(
15
+ app: Application,
16
+ file_before: Annotated[Path, typer.Argument(type=path(exists=True), help="First subset file to compare")],
17
+ file_after: Annotated[Path, typer.Argument(type=path(exists=True), help="Second subset file to compare")]
18
+ ):
19
+ """
20
+ Compare two subset files and display changes in test order positions
21
+ """
22
+
23
+ # Read files and map test paths to their indices
24
+ with open(file_before, 'r') as f:
25
+ before_tests = f.read().splitlines()
26
+ before_index_map = {test: idx for idx, test in enumerate(before_tests)}
27
+
28
+ with open(file_after, 'r') as f:
29
+ after_tests = f.read().splitlines()
30
+ after_index_map = {test: idx for idx, test in enumerate(after_tests)}
31
+
32
+ # List of tuples representing test order changes (before, after, diff, test)
33
+ rows: List[Tuple[Union[int, str], Union[int, str], Union[int, str], str]] = []
34
+
35
+ # Calculate order difference and add each test in file_after to changes
36
+ for after_idx, test in enumerate(after_tests):
37
+ if test in before_index_map:
38
+ before_idx = before_index_map[test]
39
+ diff = after_idx - before_idx
40
+ rows.append((before_idx + 1, after_idx + 1, diff, test))
41
+ else:
42
+ rows.append(('-', after_idx + 1, 'NEW', test))
43
+
44
+ # Add all deleted tests to changes
45
+ for before_idx, test in enumerate(before_tests):
46
+ if test not in after_index_map:
47
+ rows.append((before_idx + 1, '-', 'DELETED', test))
48
+
49
+ # Sort changes by the order diff
50
+ rows.sort(key=lambda x: (0 if isinstance(x[2], str) else 1, x[2]))
51
+
52
+ # Display results in a tabular format
53
+ headers = ["Before", "After", "After - Before", "Test"]
54
+ tabular_data = [
55
+ (before, after, f"{diff:+}" if isinstance(diff, int) else diff, test)
56
+ for before, after, diff, test in rows
57
+ ]
58
+ click.echo_via_pager(tabulate(tabular_data, headers=headers, tablefmt="github"))
@@ -0,0 +1,105 @@
1
+ import os
2
+ from enum import Enum
3
+ from typing import Annotated
4
+
5
+ import click
6
+
7
+ import smart_tests.args4p.typer as typer
8
+ from smart_tests.app import Application
9
+ from smart_tests.args4p.command import Group
10
+ from smart_tests.commands.test_path_writer import TestPathWriter
11
+ from smart_tests.testpath import unparse_test_path
12
+ from smart_tests.utils.commands import Command
13
+ from smart_tests.utils.env_keys import REPORT_ERROR_KEY
14
+ from smart_tests.utils.exceptions import print_error_and_die
15
+ from smart_tests.utils.session import get_session
16
+ from smart_tests.utils.smart_tests_client import SmartTestsClient
17
+ from smart_tests.utils.tracking import Tracking, TrackingClient
18
+ from smart_tests.utils.typer_types import ignorable_error
19
+
20
+
21
+ class DetectFlakesRetryThreshold(str, Enum):
22
+ LOW = "LOW"
23
+ MEDIUM = "MEDIUM"
24
+ HIGH = "HIGH"
25
+
26
+ @staticmethod
27
+ def from_str(value: str) -> "DetectFlakesRetryThreshold":
28
+ for member in DetectFlakesRetryThreshold:
29
+ if member.value.lower() == value.lower():
30
+ return member
31
+ raise ValueError(f"Invalid value for DetectFlakesRetryThreshold: {value}")
32
+
33
+
34
+ class DetectFlakes(TestPathWriter):
35
+ def __init__(
36
+ self,
37
+ app: Application,
38
+ session: Annotated[str, typer.Option(
39
+ "--session",
40
+ help="In the format builds/<build-name>/test_sessions/<test-session-id>",
41
+ metavar="SESSION",
42
+ required=True
43
+ )],
44
+ retry_threshold: Annotated[DetectFlakesRetryThreshold, typer.Option(
45
+ "--retry-threshold",
46
+ help="Thoroughness of how \"flake\" is detected",
47
+ type=DetectFlakesRetryThreshold.from_str,
48
+ metavar="low|medium|high"
49
+ )] = DetectFlakesRetryThreshold.MEDIUM,
50
+ test_runner: Annotated[str | None, typer.Argument()] = None,
51
+ ):
52
+ super().__init__(app)
53
+
54
+ app.test_runner = test_runner
55
+ self.tracking_client = TrackingClient(Command.DETECT_FLAKE, app=app)
56
+ self.client = SmartTestsClient(app=app, tracking_client=self.tracking_client)
57
+
58
+ self.session = session
59
+ self.test_session = None
60
+ try:
61
+ self.test_session = get_session(client=self.client, session=session)
62
+ except ValueError as e:
63
+ print_error_and_die(msg=str(e), tracking_client=self.tracking_client, event=Tracking.ErrorEvent.USER_ERROR)
64
+ except Exception as e:
65
+ if os.getenv(REPORT_ERROR_KEY):
66
+ raise e
67
+ else:
68
+ click.echo(ignorable_error(e), err=True)
69
+
70
+ if self.test_session is None:
71
+ raise typer.Exit(0) # bail out
72
+
73
+ self.retry_threshold = retry_threshold
74
+
75
+ def run(self):
76
+ test_paths = []
77
+ try:
78
+ res = self.client.request(
79
+ "get",
80
+ "detect-flake",
81
+ params={
82
+ "confidence": self.retry_threshold.value.upper(),
83
+ "session-id": os.path.basename(self.session),
84
+ "test-runner": self.app.test_runner,
85
+ })
86
+
87
+ res.raise_for_status()
88
+ test_paths = res.json().get("testPaths", [])
89
+ if test_paths:
90
+ self.print(test_paths)
91
+ click.echo("Trying to retry the following tests:", err=True)
92
+ for detail in res.json().get("testDetails", []):
93
+ click.echo(f"{detail.get('reason'): {unparse_test_path(detail.get('fullTestPath'))}}", err=True)
94
+ except Exception as e:
95
+ self.tracking_client.send_error_event(
96
+ event_name=Tracking.ErrorEvent.INTERNAL_CLI_ERROR,
97
+ stack_trace=str(e),
98
+ )
99
+ if os.getenv(REPORT_ERROR_KEY):
100
+ raise e
101
+ else:
102
+ click.echo(ignorable_error(e), err=True)
103
+
104
+
105
+ detect_flakes = Group(name="detect-flakes", callback=DetectFlakes, help="Detect flaky tests")
@@ -0,0 +1,13 @@
1
+ from ... import args4p
2
+ from ...app import Application
3
+ from .model import model
4
+ from .subset import subset
5
+
6
+
7
+ @args4p.group(help="Inspect test and subset data")
8
+ def inspect(app: Application):
9
+ return app
10
+
11
+
12
+ inspect.add_command(model)
13
+ inspect.add_command(subset)
@@ -0,0 +1,52 @@
1
+ import json
2
+ import sys
3
+ from http import HTTPStatus
4
+ from typing import Annotated
5
+
6
+ import click
7
+ from requests import Response
8
+ from tabulate import tabulate
9
+
10
+ from smart_tests import args4p
11
+ from smart_tests.app import Application
12
+ from smart_tests.args4p import typer
13
+ from smart_tests.utils.smart_tests_client import SmartTestsClient
14
+
15
+
16
+ @args4p.command()
17
+ def model(
18
+ app: Application,
19
+ is_json_format: Annotated[bool, typer.Option(
20
+ '--json',
21
+ help="display JSON format"
22
+ )] = False):
23
+ client = SmartTestsClient(app=app)
24
+ try:
25
+ res: Response = client.request("get", "model-metadata")
26
+
27
+ if res.status_code == HTTPStatus.NOT_FOUND:
28
+ click.echo(click.style(
29
+ "Model metadata currently not available for this workspace.", 'yellow'), err=True)
30
+ sys.exit()
31
+
32
+ res.raise_for_status()
33
+
34
+ if is_json_format:
35
+ display_as_json(res)
36
+ else:
37
+ display_as_table(res)
38
+
39
+ except Exception as e:
40
+ client.print_exception_and_recover(e, "Warning: failed to inspect model")
41
+
42
+
43
+ def display_as_json(res: Response):
44
+ res_json = res.json()
45
+ click.echo(json.dumps(res_json, indent=2))
46
+
47
+
48
+ def display_as_table(res: Response):
49
+ headers = ["Metadata", "Value"]
50
+ res_json = res.json()
51
+ rows = [["Training Cutoff Test Session ID", res_json['training_cutoff_test_session_id']]]
52
+ click.echo(tabulate(rows, headers, tablefmt="github"))