scale-nucleus 0.1.22__py3-none-any.whl → 0.6.4__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 (73) hide show
  1. cli/client.py +14 -0
  2. cli/datasets.py +77 -0
  3. cli/helpers/__init__.py +0 -0
  4. cli/helpers/nucleus_url.py +10 -0
  5. cli/helpers/web_helper.py +40 -0
  6. cli/install_completion.py +33 -0
  7. cli/jobs.py +42 -0
  8. cli/models.py +35 -0
  9. cli/nu.py +42 -0
  10. cli/reference.py +8 -0
  11. cli/slices.py +62 -0
  12. cli/tests.py +121 -0
  13. nucleus/__init__.py +453 -699
  14. nucleus/annotation.py +435 -80
  15. nucleus/autocurate.py +9 -0
  16. nucleus/connection.py +87 -0
  17. nucleus/constants.py +12 -2
  18. nucleus/data_transfer_object/__init__.py +0 -0
  19. nucleus/data_transfer_object/dataset_details.py +9 -0
  20. nucleus/data_transfer_object/dataset_info.py +26 -0
  21. nucleus/data_transfer_object/dataset_size.py +5 -0
  22. nucleus/data_transfer_object/scenes_list.py +18 -0
  23. nucleus/dataset.py +1139 -215
  24. nucleus/dataset_item.py +130 -26
  25. nucleus/dataset_item_uploader.py +297 -0
  26. nucleus/deprecation_warning.py +32 -0
  27. nucleus/errors.py +21 -1
  28. nucleus/job.py +71 -3
  29. nucleus/logger.py +9 -0
  30. nucleus/metadata_manager.py +45 -0
  31. nucleus/metrics/__init__.py +10 -0
  32. nucleus/metrics/base.py +117 -0
  33. nucleus/metrics/categorization_metrics.py +197 -0
  34. nucleus/metrics/errors.py +7 -0
  35. nucleus/metrics/filters.py +40 -0
  36. nucleus/metrics/geometry.py +198 -0
  37. nucleus/metrics/metric_utils.py +28 -0
  38. nucleus/metrics/polygon_metrics.py +480 -0
  39. nucleus/metrics/polygon_utils.py +299 -0
  40. nucleus/model.py +121 -15
  41. nucleus/model_run.py +34 -57
  42. nucleus/payload_constructor.py +30 -18
  43. nucleus/prediction.py +259 -17
  44. nucleus/pydantic_base.py +26 -0
  45. nucleus/retry_strategy.py +4 -0
  46. nucleus/scene.py +204 -19
  47. nucleus/slice.py +230 -67
  48. nucleus/upload_response.py +20 -9
  49. nucleus/url_utils.py +4 -0
  50. nucleus/utils.py +139 -35
  51. nucleus/validate/__init__.py +24 -0
  52. nucleus/validate/client.py +168 -0
  53. nucleus/validate/constants.py +20 -0
  54. nucleus/validate/data_transfer_objects/__init__.py +0 -0
  55. nucleus/validate/data_transfer_objects/eval_function.py +81 -0
  56. nucleus/validate/data_transfer_objects/scenario_test.py +19 -0
  57. nucleus/validate/data_transfer_objects/scenario_test_evaluations.py +11 -0
  58. nucleus/validate/data_transfer_objects/scenario_test_metric.py +12 -0
  59. nucleus/validate/errors.py +6 -0
  60. nucleus/validate/eval_functions/__init__.py +0 -0
  61. nucleus/validate/eval_functions/available_eval_functions.py +212 -0
  62. nucleus/validate/eval_functions/base_eval_function.py +60 -0
  63. nucleus/validate/scenario_test.py +143 -0
  64. nucleus/validate/scenario_test_evaluation.py +114 -0
  65. nucleus/validate/scenario_test_metric.py +14 -0
  66. nucleus/validate/utils.py +8 -0
  67. {scale_nucleus-0.1.22.dist-info → scale_nucleus-0.6.4.dist-info}/LICENSE +0 -0
  68. scale_nucleus-0.6.4.dist-info/METADATA +213 -0
  69. scale_nucleus-0.6.4.dist-info/RECORD +71 -0
  70. {scale_nucleus-0.1.22.dist-info → scale_nucleus-0.6.4.dist-info}/WHEEL +1 -1
  71. scale_nucleus-0.6.4.dist-info/entry_points.txt +3 -0
  72. scale_nucleus-0.1.22.dist-info/METADATA +0 -85
  73. scale_nucleus-0.1.22.dist-info/RECORD +0 -21
cli/client.py ADDED
@@ -0,0 +1,14 @@
1
+ import functools
2
+ import os
3
+
4
+ import nucleus
5
+
6
+
7
+ @functools.lru_cache()
8
+ def init_client():
9
+ api_key = os.environ.get("NUCLEUS_API_KEY", None)
10
+ if api_key:
11
+ client = nucleus.NucleusClient(api_key)
12
+ else:
13
+ raise RuntimeError("No NUCLEUS_API_KEY set")
14
+ return client
cli/datasets.py ADDED
@@ -0,0 +1,77 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.table import Column, Table
4
+
5
+ from cli.client import init_client
6
+ from cli.helpers.nucleus_url import nucleus_url
7
+ from cli.helpers.web_helper import launch_web_or_invoke
8
+
9
+
10
+ @click.group("datasets", invoke_without_command=True)
11
+ @click.option("--web", is_flag=True, help="Launch browser")
12
+ @click.pass_context
13
+ def datasets(ctx, web):
14
+ """Datasets are the base collections of items in Nucleus
15
+
16
+ https://dashboard.scale.com/nucleus/datasets
17
+ """
18
+ launch_web_or_invoke(
19
+ sub_url="datasets", ctx=ctx, launch_browser=web, command=list_datasets
20
+ )
21
+
22
+
23
+ @datasets.command("list")
24
+ @click.option(
25
+ "-m", "--machine-readable", is_flag=True, help="Removes pretty printing"
26
+ )
27
+ def list_datasets(machine_readable):
28
+ """List all available Datasets"""
29
+ console = Console()
30
+ with console.status("Finding your Datasets!", spinner="dots4"):
31
+ client = init_client()
32
+ all_datasets = client.datasets
33
+ if machine_readable:
34
+ table_params = {"box": None, "pad_edge": False}
35
+ else:
36
+ table_params = {
37
+ "title": ":fire: Datasets",
38
+ "title_justify": "left",
39
+ }
40
+
41
+ table = Table(
42
+ "id", "Name", Column("url", overflow="fold"), **table_params
43
+ )
44
+ for ds in all_datasets:
45
+ table.add_row(ds.id, ds.name, nucleus_url(ds.id))
46
+ console.print(table)
47
+
48
+
49
+ @datasets.command("delete")
50
+ @click.option("--id", prompt=True)
51
+ @click.option(
52
+ "--no-confirm-deletion",
53
+ is_flag=True,
54
+ help="WARNING: No confirmation for deletion",
55
+ )
56
+ @click.pass_context
57
+ def delete_dataset(ctx, id, no_confirm_deletion):
58
+ """Delete a Dataset"""
59
+ console = Console()
60
+ client = init_client()
61
+ id = id.strip()
62
+ dataset = client.get_dataset(id)
63
+ delete_string = ""
64
+ if not no_confirm_deletion:
65
+ delete_string = click.prompt(
66
+ click.style(
67
+ f"Type 'DELETE' to delete dataset: {dataset}", fg="red"
68
+ )
69
+ )
70
+ if no_confirm_deletion or delete_string == "DELETE":
71
+ client.delete_dataset(dataset.id)
72
+ console.print(f":fire: :anguished: Deleted {id}")
73
+ else:
74
+ console.print(
75
+ f":rotating_light: Refusing to delete {id}. Received '{delete_string}' instead of 'DELETE'"
76
+ )
77
+ ctx.abort()
File without changes
@@ -0,0 +1,10 @@
1
+ import os
2
+ from urllib.parse import urljoin
3
+
4
+
5
+ def nucleus_url(sub_path: str):
6
+ nucleus_base = os.environ.get(
7
+ "NUCLEUS_DASHBOARD", "https://dashboard.scale.com/nucleus/"
8
+ )
9
+ extra_params = os.environ.get("NUCLEUS_DASH_PARAMS", "")
10
+ return urljoin(nucleus_base, sub_path.lstrip("/") + extra_params)
@@ -0,0 +1,40 @@
1
+ import click
2
+
3
+ from cli.helpers.nucleus_url import nucleus_url
4
+
5
+
6
+ def launch_web_or_show_help(
7
+ sub_url: str, ctx: click.Context, launch_browser: bool
8
+ ):
9
+ """ Launches the sub_url (composed with nuclues_url(sub_url)) in the browser if requested"""
10
+ if not ctx.invoked_subcommand:
11
+ if launch_browser:
12
+ url = nucleus_url(sub_url)
13
+ click.launch(url)
14
+ else:
15
+ click.echo(ctx.get_help())
16
+ else:
17
+ if launch_browser:
18
+ click.echo(click.style("--web does not work with sub-commands"))
19
+ ctx.abort()
20
+
21
+
22
+ def launch_web_or_invoke(
23
+ sub_url: str,
24
+ ctx: click.Context,
25
+ launch_browser: bool,
26
+ command: click.BaseCommand,
27
+ ):
28
+ """Launches the sub_url (composed with nuclues_url(sub_url)) in the browser if requested, otherwise invokes
29
+ the passed command
30
+ """
31
+ if not ctx.invoked_subcommand:
32
+ if launch_browser:
33
+ url = nucleus_url(sub_url)
34
+ click.launch(url)
35
+ else:
36
+ ctx.invoke(command)
37
+ else:
38
+ if launch_browser:
39
+ click.echo(click.style("--web does not work with sub-commands"))
40
+ ctx.abort()
@@ -0,0 +1,33 @@
1
+ import os
2
+ import shutil
3
+
4
+ import click
5
+ from shellingham import detect_shell
6
+
7
+
8
+ @click.command("install-completion")
9
+ def install_completion():
10
+ """Install shell completion script to your rc file"""
11
+ shell, _ = detect_shell()
12
+ if shell == "zsh":
13
+ rc_path = "~/.zshrc"
14
+ append_to_file = 'eval "$(_NU_COMPLETE=zsh_source nu)"'
15
+ elif shell == "bash":
16
+ rc_path = "~/.bashrc"
17
+ append_to_file = 'eval "$(_NU_COMPLETE=bash_source nu)"'
18
+ elif shell == "fish":
19
+ rc_path = "~/.config/fish/completions/foo-bar.fish"
20
+ append_to_file = "eval (env _NU_COMPLETE=fish_source nu)"
21
+ else:
22
+ raise RuntimeError(f"Unsupported shell {shell} for completions")
23
+
24
+ rc_path_expanded = os.path.expanduser(rc_path)
25
+ rc_bak = f"{rc_path_expanded}.bak"
26
+ shutil.copy(rc_path_expanded, rc_bak)
27
+ click.echo(f"Backed up {rc_path} to {rc_bak}")
28
+ with open(rc_path_expanded, mode="a") as rc_file:
29
+ rc_file.write("\n")
30
+ rc_file.write("# Shell completion for nu\n")
31
+ rc_file.write(append_to_file)
32
+ click.echo(f"Completion script added to {rc_path}")
33
+ click.echo(f"Don't forget to `source {rc_path}")
cli/jobs.py ADDED
@@ -0,0 +1,42 @@
1
+ import click
2
+ from rich.live import Live
3
+ from rich.spinner import Spinner
4
+ from rich.table import Column, Table
5
+
6
+ from cli.client import init_client
7
+ from cli.helpers.web_helper import launch_web_or_invoke
8
+
9
+
10
+ @click.group("jobs", invoke_without_command=True)
11
+ @click.option("--web", is_flag=True, help="Launch browser")
12
+ @click.pass_context
13
+ def jobs(ctx, web):
14
+ """Jobs are a wrapper around various long-running tasks withing Nucleus
15
+
16
+ https://dashboard.scale.com/nucleus/jobs
17
+ """
18
+ launch_web_or_invoke("jobs", ctx, web, list_jobs)
19
+
20
+
21
+ @jobs.command("list")
22
+ def list_jobs():
23
+ """List all of your Jobs"""
24
+ client = init_client()
25
+ table = Table(
26
+ Column("id", overflow="fold", min_width=24),
27
+ "status",
28
+ "type",
29
+ "created at",
30
+ title=":satellite: Jobs",
31
+ title_justify="left",
32
+ )
33
+ with Live(Spinner("dots4", text="Finding your Jobs!")) as live:
34
+ all_jobs = client.jobs
35
+ for job in all_jobs:
36
+ table.add_row(
37
+ job.job_id,
38
+ job.job_last_known_status,
39
+ job.job_type,
40
+ job.job_creation_time,
41
+ )
42
+ live.update(table)
cli/models.py ADDED
@@ -0,0 +1,35 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.table import Column, Table
4
+
5
+ from cli.client import init_client
6
+ from cli.helpers.nucleus_url import nucleus_url
7
+ from cli.helpers.web_helper import launch_web_or_invoke
8
+
9
+
10
+ @click.group("models", invoke_without_command=True)
11
+ @click.option("--web", is_flag=True, help="Launch browser")
12
+ @click.pass_context
13
+ def models(ctx, web):
14
+ """Models help you store and access your ML model data
15
+
16
+ https://dashboard.scale.com/nucleus/models
17
+ """
18
+ launch_web_or_invoke("models", ctx, web, list_models)
19
+
20
+
21
+ @models.command("list")
22
+ def list_models():
23
+ """List your Models"""
24
+ console = Console()
25
+ with console.status("Finding your Models!", spinner="dots4"):
26
+ client = init_client()
27
+ table = Table(
28
+ Column("id", overflow="fold", min_width=24),
29
+ "name",
30
+ Column("url", overflow="fold"),
31
+ )
32
+ models = client.models
33
+ for m in models:
34
+ table.add_row(m.id, m.name, nucleus_url(m.id))
35
+ console.print(table)
cli/nu.py ADDED
@@ -0,0 +1,42 @@
1
+ import click
2
+
3
+ from cli.datasets import datasets
4
+ from cli.helpers.web_helper import launch_web_or_show_help
5
+ from cli.install_completion import install_completion
6
+ from cli.jobs import jobs
7
+ from cli.models import models
8
+ from cli.reference import reference
9
+ from cli.slices import slices
10
+ from cli.tests import tests
11
+
12
+
13
+ @click.group("cli", invoke_without_command=True)
14
+ @click.option("--web", is_flag=True, help="Launch browser")
15
+ @click.pass_context
16
+ def nu(ctx, web):
17
+ """Nucleus CLI (nu)
18
+
19
+ \b
20
+ ███╗ ██╗██╗ ██╗ ██████╗██╗ ███████╗██╗ ██╗███████╗
21
+ ████╗ ██║██║ ██║██╔════╝██║ ██╔════╝██║ ██║██╔════╝
22
+ ██╔██╗ ██║██║ ██║██║ ██║ █████╗ ██║ ██║███████╗
23
+ ██║╚██╗██║██║ ██║██║ ██║ ██╔══╝ ██║ ██║╚════██║
24
+ ██║ ╚████║╚██████╔╝╚██████╗███████╗███████╗╚██████╔╝███████║
25
+ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝╚══════╝╚══════╝ ╚═════╝ ╚══════╝
26
+
27
+ `nu` is a command line interface to interact with Scale Nucleus (https://dashboard.scale.com/nucleus)
28
+ """
29
+ launch_web_or_show_help(sub_url="", ctx=ctx, launch_browser=web)
30
+
31
+
32
+ nu.add_command(datasets) # type: ignore
33
+ nu.add_command(install_completion) # type: ignore
34
+ nu.add_command(jobs) # type: ignore
35
+ nu.add_command(models) # type: ignore
36
+ nu.add_command(reference) # type: ignore
37
+ nu.add_command(slices) # type: ignore
38
+ nu.add_command(tests) # type: ignore
39
+
40
+ if __name__ == "__main__":
41
+ """To debug, run this script followed by request command tree e.g. `cli/nu.py datasets list`"""
42
+ nu()
cli/reference.py ADDED
@@ -0,0 +1,8 @@
1
+ import click
2
+
3
+
4
+ # NOTE: Named reference instead of docs to not clash with dataset autocomplete
5
+ @click.command("reference")
6
+ def reference():
7
+ """ View the Nucleus reference documentation in the browser"""
8
+ click.launch("https://nucleus.scale.com/docs")
cli/slices.py ADDED
@@ -0,0 +1,62 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.live import Live
4
+ from rich.spinner import Spinner
5
+ from rich.table import Column, Table
6
+ from rich.tree import Tree
7
+
8
+ from cli.client import init_client
9
+ from cli.helpers.nucleus_url import nucleus_url
10
+ from cli.helpers.web_helper import launch_web_or_invoke
11
+ from nucleus import NucleusAPIError
12
+
13
+
14
+ @click.group("slices", invoke_without_command=True)
15
+ @click.option("--web", is_flag=True, help="Launch browser")
16
+ @click.pass_context
17
+ def slices(ctx, web):
18
+ """Slices are named subsets of Datasets"""
19
+ # TODO(gunnar): We don't have a natural landing for slices overview, until then we land on "/"
20
+ launch_web_or_invoke("", ctx, web, list_slices)
21
+
22
+
23
+ @slices.command("list")
24
+ def list_slices():
25
+ """List all available Slices"""
26
+ with Live(
27
+ Spinner("dots4", text="Finding your Slices!"),
28
+ vertical_overflow="visible",
29
+ ) as live:
30
+ client = init_client()
31
+ datasets = client.datasets
32
+ table = Table(
33
+ Column("id", overflow="fold", min_width=24),
34
+ "name",
35
+ "dataset_name",
36
+ Column("url", overflow="fold"),
37
+ title=":cake: Slices",
38
+ title_justify="left",
39
+ )
40
+ errors = {}
41
+ for ds in datasets:
42
+ try:
43
+ ds_slices = ds.slices
44
+ if ds_slices:
45
+ for slc_id in ds_slices:
46
+ slice_url = nucleus_url(f"{ds.id}/{slc_id}")
47
+ slice_info = client.get_slice(slc_id).info()
48
+ table.add_row(
49
+ slc_id, slice_info["name"], ds.name, slice_url
50
+ )
51
+ live.update(table)
52
+ except NucleusAPIError as e:
53
+ errors[ds.id] = e
54
+
55
+ error_tree = Tree(
56
+ ":x: Encountered the following errors while fetching information"
57
+ )
58
+ for ds_id, error in errors.items():
59
+ dataset_branch = error_tree.add(f"Dataset: {ds_id}")
60
+ dataset_branch.add(f"Error: {error}")
61
+
62
+ Console().print(error_tree)
cli/tests.py ADDED
@@ -0,0 +1,121 @@
1
+ import click
2
+ from rich.console import Console
3
+ from rich.live import Live
4
+ from rich.table import Column, Table
5
+ from rich.tree import Tree
6
+
7
+ from cli.client import init_client
8
+ from cli.helpers.nucleus_url import nucleus_url
9
+ from cli.helpers.web_helper import launch_web_or_invoke
10
+ from nucleus import NucleusAPIError
11
+ from nucleus.validate import (
12
+ AvailableEvalFunctions,
13
+ ScenarioTestMetric,
14
+ ThresholdComparison,
15
+ )
16
+
17
+
18
+ @click.group("tests", invoke_without_command=True)
19
+ @click.option("--web", is_flag=True, help="Launch browser")
20
+ @click.pass_context
21
+ def tests(ctx, web):
22
+ """Scenario Tests allow you to test your Models
23
+
24
+ https://dashboard.scale.com/nucleus/scenario-tests
25
+ """
26
+ launch_web_or_invoke("scenario-tests", ctx, web, list_tests)
27
+
28
+
29
+ @tests.command("list")
30
+ def list_tests():
31
+ """List all your Scenario Tests"""
32
+ console = Console()
33
+ with console.status("Finding your Scenario Tests", spinner="dots4"):
34
+ client = init_client()
35
+ scenario_tests = client.validate.list_scenario_tests()
36
+ table = Table(
37
+ Column("id", overflow="fold", min_width=24),
38
+ "Name",
39
+ "slice_id",
40
+ Column("url", overflow="fold"),
41
+ title=":chart_with_upwards_trend: Scenario Tests",
42
+ title_justify="left",
43
+ )
44
+ for ut in scenario_tests:
45
+ table.add_row(ut.id, ut.name, ut.slice_id, nucleus_url(ut.id))
46
+ console.print(table)
47
+
48
+
49
+ def format_criterion(
50
+ criterion: ScenarioTestMetric, eval_functions: AvailableEvalFunctions
51
+ ):
52
+ op_map = {
53
+ ThresholdComparison.GREATER_THAN: ">",
54
+ ThresholdComparison.GREATER_THAN_EQUAL_TO: ">=",
55
+ ThresholdComparison.LESS_THAN: "<",
56
+ ThresholdComparison.LESS_THAN_EQUAL_TO: "<=",
57
+ }
58
+ eval_function_name = eval_functions.from_id(
59
+ criterion.eval_function_id
60
+ ).name
61
+ return f"{eval_function_name} {op_map[criterion.threshold_comparison]} {criterion.threshold}"
62
+ pass
63
+
64
+
65
+ @tests.command("describe")
66
+ @click.argument("scenario-test-id", default=None, required=False)
67
+ @click.option(
68
+ "--all", "-a", is_flag=True, help="View details about all Scenario Tests"
69
+ )
70
+ def describe_test(scenario_test_id, all):
71
+ """View detailed information about a test or all tests"""
72
+ console = Console()
73
+ # scenario_test = client.validate.get_scenario_test(scenario_test_id)
74
+ assert scenario_test_id or all, "Must pass a scenario_test_id or --all"
75
+ client = init_client()
76
+ scenario_tests = client.validate.list_scenario_tests()
77
+ if all:
78
+ tree = Tree(":chart_with_upwards_trend: All Scenario Tests")
79
+ with Live(
80
+ "Fetching description of all Scenario Tests",
81
+ vertical_overflow="visible",
82
+ ) as live:
83
+ for idx, ut in enumerate(scenario_tests):
84
+ test_branch = tree.add(f"{idx}: Scenario Test")
85
+ build_scenario_test_info_tree(client, ut, test_branch)
86
+ live.update(tree)
87
+ else:
88
+ with console.status("Fetching Scenario Test information"):
89
+ scenario_test = [
90
+ ut for ut in scenario_tests if ut.id == scenario_test_id
91
+ ][0]
92
+ tree = Tree(":chart_with_upwards_trend: Scenario Test")
93
+ build_scenario_test_info_tree(client, scenario_test, tree)
94
+ console.print(tree)
95
+
96
+
97
+ def build_scenario_test_info_tree(client, scenario_test, tree):
98
+ try:
99
+ slc = client.get_slice(scenario_test.slice_id)
100
+ info_branch = tree.add(":mag: Details")
101
+ info_branch.add(f"id: '{scenario_test.id}'")
102
+ info_branch.add(f"name: '{scenario_test.name}'")
103
+ scenario_test_url = nucleus_url(scenario_test.id)
104
+ info_branch.add(f"url: {scenario_test_url}")
105
+ slice_url = nucleus_url(f"{slc.dataset_id}/{slc.slice_id}")
106
+ slice_branch = tree.add(":cake: Slice")
107
+ slice_branch.add(f"id: '{slc.id}'")
108
+ slice_info = slc.info()
109
+ slice_branch.add(f"name: '{slice_info['name']}'")
110
+ slice_branch.add(f"len: {len(slc.items)}")
111
+ slice_branch.add(f"url: {slice_url}")
112
+ criteria = scenario_test.get_criteria()
113
+ criteria_branch = tree.add(":crossed_flags: Criteria")
114
+ for criterion in criteria:
115
+ pretty_criterion = format_criterion(
116
+ criterion, client.validate.eval_functions
117
+ )
118
+ criteria_branch.add(pretty_criterion)
119
+ except NucleusAPIError as e:
120
+ error_branch = tree.add(":x: Error")
121
+ error_branch.add(f"detail: {str(e)}")