esgpull 0.6.3__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 (80) hide show
  1. esgpull/__init__.py +12 -0
  2. esgpull/auth.py +181 -0
  3. esgpull/cli/__init__.py +73 -0
  4. esgpull/cli/add.py +103 -0
  5. esgpull/cli/autoremove.py +38 -0
  6. esgpull/cli/config.py +116 -0
  7. esgpull/cli/convert.py +285 -0
  8. esgpull/cli/decorators.py +342 -0
  9. esgpull/cli/download.py +74 -0
  10. esgpull/cli/facet.py +23 -0
  11. esgpull/cli/get.py +28 -0
  12. esgpull/cli/install.py +85 -0
  13. esgpull/cli/link.py +105 -0
  14. esgpull/cli/login.py +56 -0
  15. esgpull/cli/remove.py +73 -0
  16. esgpull/cli/retry.py +43 -0
  17. esgpull/cli/search.py +201 -0
  18. esgpull/cli/self.py +238 -0
  19. esgpull/cli/show.py +66 -0
  20. esgpull/cli/status.py +67 -0
  21. esgpull/cli/track.py +87 -0
  22. esgpull/cli/update.py +184 -0
  23. esgpull/cli/utils.py +247 -0
  24. esgpull/config.py +410 -0
  25. esgpull/constants.py +56 -0
  26. esgpull/context.py +724 -0
  27. esgpull/database.py +161 -0
  28. esgpull/download.py +162 -0
  29. esgpull/esgpull.py +447 -0
  30. esgpull/exceptions.py +167 -0
  31. esgpull/fs.py +253 -0
  32. esgpull/graph.py +460 -0
  33. esgpull/install_config.py +185 -0
  34. esgpull/migrations/README +1 -0
  35. esgpull/migrations/env.py +82 -0
  36. esgpull/migrations/script.py.mako +24 -0
  37. esgpull/migrations/versions/0.3.0_update_tables.py +170 -0
  38. esgpull/migrations/versions/0.3.1_update_tables.py +25 -0
  39. esgpull/migrations/versions/0.3.2_update_tables.py +26 -0
  40. esgpull/migrations/versions/0.3.3_update_tables.py +25 -0
  41. esgpull/migrations/versions/0.3.4_update_tables.py +25 -0
  42. esgpull/migrations/versions/0.3.5_update_tables.py +25 -0
  43. esgpull/migrations/versions/0.3.6_update_tables.py +26 -0
  44. esgpull/migrations/versions/0.3.7_update_tables.py +26 -0
  45. esgpull/migrations/versions/0.3.8_update_tables.py +26 -0
  46. esgpull/migrations/versions/0.4.0_update_tables.py +25 -0
  47. esgpull/migrations/versions/0.5.0_update_tables.py +26 -0
  48. esgpull/migrations/versions/0.5.1_update_tables.py +26 -0
  49. esgpull/migrations/versions/0.5.2_update_tables.py +25 -0
  50. esgpull/migrations/versions/0.5.3_update_tables.py +26 -0
  51. esgpull/migrations/versions/0.5.4_update_tables.py +25 -0
  52. esgpull/migrations/versions/0.5.5_update_tables.py +25 -0
  53. esgpull/migrations/versions/0.6.0_update_tables.py +25 -0
  54. esgpull/migrations/versions/0.6.1_update_tables.py +25 -0
  55. esgpull/migrations/versions/0.6.2_update_tables.py +25 -0
  56. esgpull/migrations/versions/0.6.3_update_tables.py +25 -0
  57. esgpull/models/__init__.py +31 -0
  58. esgpull/models/base.py +50 -0
  59. esgpull/models/dataset.py +34 -0
  60. esgpull/models/facet.py +18 -0
  61. esgpull/models/file.py +65 -0
  62. esgpull/models/options.py +164 -0
  63. esgpull/models/query.py +481 -0
  64. esgpull/models/selection.py +201 -0
  65. esgpull/models/sql.py +258 -0
  66. esgpull/models/synda_file.py +85 -0
  67. esgpull/models/tag.py +19 -0
  68. esgpull/models/utils.py +54 -0
  69. esgpull/presets.py +13 -0
  70. esgpull/processor.py +172 -0
  71. esgpull/py.typed +0 -0
  72. esgpull/result.py +53 -0
  73. esgpull/tui.py +346 -0
  74. esgpull/utils.py +54 -0
  75. esgpull/version.py +1 -0
  76. esgpull-0.6.3.dist-info/METADATA +110 -0
  77. esgpull-0.6.3.dist-info/RECORD +80 -0
  78. esgpull-0.6.3.dist-info/WHEEL +4 -0
  79. esgpull-0.6.3.dist-info/entry_points.txt +3 -0
  80. esgpull-0.6.3.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,25 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.4.0
4
+ Revises: 0.3.8
5
+ Create Date: 2023-05-02 11:52:41.283723
6
+
7
+ """
8
+
9
+ # revision identifiers, used by Alembic.
10
+ revision = "0.4.0"
11
+ down_revision = "0.3.8"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ # ### commands auto generated by Alembic - please adjust! ###
18
+ pass
19
+ # ### end Alembic commands ###
20
+
21
+
22
+ def downgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ pass
25
+ # ### end Alembic commands ###
@@ -0,0 +1,26 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.5.0
4
+ Revises: 0.4.0
5
+ Create Date: 2023-05-05 17:10:57.525968
6
+
7
+ """
8
+
9
+
10
+ # revision identifiers, used by Alembic.
11
+ revision = "0.5.0"
12
+ down_revision = "0.4.0"
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ # ### commands auto generated by Alembic - please adjust! ###
19
+ pass
20
+ # ### end Alembic commands ###
21
+
22
+
23
+ def downgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ pass
26
+ # ### end Alembic commands ###
@@ -0,0 +1,26 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.5.1
4
+ Revises: 0.5.0
5
+ Create Date: 2023-05-17 18:09:56.258545
6
+
7
+ """
8
+
9
+
10
+ # revision identifiers, used by Alembic.
11
+ revision = "0.5.1"
12
+ down_revision = "0.5.0"
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ # ### commands auto generated by Alembic - please adjust! ###
19
+ pass
20
+ # ### end Alembic commands ###
21
+
22
+
23
+ def downgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ pass
26
+ # ### end Alembic commands ###
@@ -0,0 +1,25 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.5.2
4
+ Revises: 0.5.1
5
+ Create Date: 2023-06-06 14:13:19.695120
6
+
7
+ """
8
+
9
+ # revision identifiers, used by Alembic.
10
+ revision = "0.5.2"
11
+ down_revision = "0.5.1"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ # ### commands auto generated by Alembic - please adjust! ###
18
+ pass
19
+ # ### end Alembic commands ###
20
+
21
+
22
+ def downgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ pass
25
+ # ### end Alembic commands ###
@@ -0,0 +1,26 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.5.3
4
+ Revises: 0.5.2
5
+ Create Date: 2023-06-13 10:27:02.691266
6
+
7
+ """
8
+
9
+
10
+ # revision identifiers, used by Alembic.
11
+ revision = "0.5.3"
12
+ down_revision = "0.5.2"
13
+ branch_labels = None
14
+ depends_on = None
15
+
16
+
17
+ def upgrade() -> None:
18
+ # ### commands auto generated by Alembic - please adjust! ###
19
+ pass
20
+ # ### end Alembic commands ###
21
+
22
+
23
+ def downgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ pass
26
+ # ### end Alembic commands ###
@@ -0,0 +1,25 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.5.4
4
+ Revises: 0.5.3
5
+ Create Date: 2023-06-13 11:43:15.867306
6
+
7
+ """
8
+
9
+ # revision identifiers, used by Alembic.
10
+ revision = "0.5.4"
11
+ down_revision = "0.5.3"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ # ### commands auto generated by Alembic - please adjust! ###
18
+ pass
19
+ # ### end Alembic commands ###
20
+
21
+
22
+ def downgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ pass
25
+ # ### end Alembic commands ###
@@ -0,0 +1,25 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.5.5
4
+ Revises: 0.5.4
5
+ Create Date: 2023-06-23 15:21:08.251368
6
+
7
+ """
8
+
9
+ # revision identifiers, used by Alembic.
10
+ revision = "0.5.5"
11
+ down_revision = "0.5.4"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ # ### commands auto generated by Alembic - please adjust! ###
18
+ pass
19
+ # ### end Alembic commands ###
20
+
21
+
22
+ def downgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ pass
25
+ # ### end Alembic commands ###
@@ -0,0 +1,25 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.6.0
4
+ Revises: 0.5.5
5
+ Create Date: 2023-07-28 15:01:40.702583
6
+
7
+ """
8
+
9
+ # revision identifiers, used by Alembic.
10
+ revision = "0.6.0"
11
+ down_revision = "0.5.5"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ # ### commands auto generated by Alembic - please adjust! ###
18
+ pass
19
+ # ### end Alembic commands ###
20
+
21
+
22
+ def downgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ pass
25
+ # ### end Alembic commands ###
@@ -0,0 +1,25 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.6.1
4
+ Revises: 0.6.0
5
+ Create Date: 2023-07-28 18:40:20.911726
6
+
7
+ """
8
+
9
+ # revision identifiers, used by Alembic.
10
+ revision = "0.6.1"
11
+ down_revision = "0.6.0"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ # ### commands auto generated by Alembic - please adjust! ###
18
+ pass
19
+ # ### end Alembic commands ###
20
+
21
+
22
+ def downgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ pass
25
+ # ### end Alembic commands ###
@@ -0,0 +1,25 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.6.2
4
+ Revises: 0.6.1
5
+ Create Date: 2023-08-18 17:11:15.605131
6
+
7
+ """
8
+
9
+ # revision identifiers, used by Alembic.
10
+ revision = "0.6.2"
11
+ down_revision = "0.6.1"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ # ### commands auto generated by Alembic - please adjust! ###
18
+ pass
19
+ # ### end Alembic commands ###
20
+
21
+
22
+ def downgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ pass
25
+ # ### end Alembic commands ###
@@ -0,0 +1,25 @@
1
+ """update tables
2
+
3
+ Revision ID: 0.6.3
4
+ Revises: 0.6.2
5
+ Create Date: 2024-06-13 11:48:48.097943
6
+
7
+ """
8
+
9
+ # revision identifiers, used by Alembic.
10
+ revision = "0.6.3"
11
+ down_revision = "0.6.2"
12
+ branch_labels = None
13
+ depends_on = None
14
+
15
+
16
+ def upgrade() -> None:
17
+ # ### commands auto generated by Alembic - please adjust! ###
18
+ pass
19
+ # ### end Alembic commands ###
20
+
21
+
22
+ def downgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ pass
25
+ # ### end Alembic commands ###
@@ -0,0 +1,31 @@
1
+ from typing import TypeVar
2
+
3
+ from esgpull.models.base import Base
4
+ from esgpull.models.dataset import Dataset
5
+ from esgpull.models.facet import Facet
6
+ from esgpull.models.file import FastFile, FileStatus
7
+ from esgpull.models.options import Option, Options
8
+ from esgpull.models.query import File, LegacyQuery, Query, QueryDict
9
+ from esgpull.models.selection import Selection
10
+ from esgpull.models.synda_file import SyndaFile
11
+ from esgpull.models.tag import Tag
12
+
13
+ Table = TypeVar("Table", bound=Base)
14
+
15
+ __all__ = [
16
+ "Base",
17
+ "Dataset",
18
+ "Facet",
19
+ "FastFile",
20
+ "File",
21
+ "FileStatus",
22
+ "LegacyQuery",
23
+ "Option",
24
+ "Options",
25
+ "Query",
26
+ "QueryDict",
27
+ "Selection",
28
+ "SyndaFile",
29
+ "Table",
30
+ "Tag",
31
+ ]
esgpull/models/base.py ADDED
@@ -0,0 +1,50 @@
1
+ from collections.abc import Mapping
2
+ from dataclasses import Field
3
+ from hashlib import sha1
4
+ from typing import Any, ClassVar, TypeVar, cast
5
+
6
+ import sqlalchemy as sa
7
+ from sqlalchemy.orm import (
8
+ DeclarativeBase,
9
+ InstanceState,
10
+ Mapped,
11
+ MappedAsDataclass,
12
+ mapped_column,
13
+ )
14
+
15
+ T = TypeVar("T")
16
+ Sha = sa.String(40)
17
+
18
+
19
+ class Base(MappedAsDataclass, DeclarativeBase):
20
+ __dataclass_fields__: ClassVar[dict[str, Field]]
21
+ __sql_attrs__ = ("id", "sha", "_sa_instance_state", "__dataclass_fields__")
22
+
23
+ sha: Mapped[str] = mapped_column(
24
+ Sha,
25
+ init=False,
26
+ repr=False,
27
+ primary_key=True,
28
+ )
29
+
30
+ @property
31
+ def _names(self) -> tuple[str, ...]:
32
+ result: tuple[str, ...] = ()
33
+ for name in self.__dataclass_fields__:
34
+ if name in self.__sql_attrs__:
35
+ continue
36
+ result += (name,)
37
+ return result
38
+
39
+ def _as_bytes(self) -> bytes:
40
+ raise NotImplementedError
41
+
42
+ def compute_sha(self) -> None:
43
+ self.sha = sha1(self._as_bytes()).hexdigest()
44
+
45
+ @property
46
+ def state(self) -> InstanceState:
47
+ return cast(InstanceState, sa.inspect(self))
48
+
49
+ def asdict(self) -> Mapping[str, Any]:
50
+ raise NotImplementedError
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+
5
+ from esgpull.models.utils import find_int, find_str
6
+
7
+
8
+ @dataclass
9
+ class Dataset:
10
+ dataset_id: str
11
+ master_id: str
12
+ version: str
13
+ data_node: str
14
+ size: int
15
+ number_of_files: int
16
+
17
+ @classmethod
18
+ def serialize(cls, source: dict) -> Dataset:
19
+ dataset_id = find_str(source["instance_id"]).partition("|")[0]
20
+ master_id, version = dataset_id.rsplit(".", 1)
21
+ data_node = find_str(source["data_node"])
22
+ size = find_int(source["size"])
23
+ number_of_files = find_int(source["number_of_files"])
24
+ return cls(
25
+ dataset_id=dataset_id,
26
+ master_id=master_id,
27
+ version=version,
28
+ data_node=data_node,
29
+ size=size,
30
+ number_of_files=number_of_files,
31
+ )
32
+
33
+ def asdict(self) -> dict:
34
+ return asdict(self)
@@ -0,0 +1,18 @@
1
+ import sqlalchemy as sa
2
+ from sqlalchemy.orm import Mapped, mapped_column
3
+
4
+ from esgpull.models.base import Base
5
+
6
+
7
+ class Facet(Base):
8
+ __tablename__ = "facet"
9
+
10
+ name: Mapped[str] = mapped_column(sa.String(64))
11
+ value: Mapped[str] = mapped_column(sa.String(255))
12
+
13
+ def _as_bytes(self) -> bytes:
14
+ self_tuple = (self.name, self.value)
15
+ return str(self_tuple).encode()
16
+
17
+ def __hash__(self) -> int:
18
+ return hash(self._as_bytes())
esgpull/models/file.py ADDED
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ from typing_extensions import NotRequired, TypedDict
7
+
8
+ from esgpull.models.base import Base
9
+ from esgpull.models.utils import find_str
10
+
11
+
12
+ class FileStatus(Enum):
13
+ New = "new"
14
+ Queued = "queued"
15
+ Starting = "starting"
16
+ Started = "started"
17
+ Pausing = "pausing"
18
+ Paused = "paused"
19
+ Error = "error"
20
+ Cancelled = "cancelled"
21
+ Done = "done"
22
+
23
+ @classmethod
24
+ def retryable(cls) -> list[FileStatus]:
25
+ return [cls.Error, cls.Cancelled]
26
+
27
+ @classmethod
28
+ def contains(cls, s: str) -> bool:
29
+ return s in [v.value for v in cls]
30
+
31
+
32
+ class FileDict(TypedDict):
33
+ file_id: str
34
+ dataset_id: str
35
+ master_id: str
36
+ url: str
37
+ version: str
38
+ filename: str
39
+ local_path: str
40
+ data_node: str
41
+ checksum: str
42
+ checksum_type: str
43
+ size: int
44
+ status: NotRequired[str]
45
+
46
+
47
+ @dataclass(init=False)
48
+ class FastFile:
49
+ sha: str
50
+ file_id: str
51
+ checksum: str
52
+
53
+ def _as_bytes(self) -> bytes:
54
+ self_tuple = (self.file_id, self.checksum)
55
+ return str(self_tuple).encode()
56
+
57
+ @classmethod
58
+ def serialize(cls, source: dict) -> FastFile:
59
+ result = cls()
60
+ dataset_id = find_str(source["dataset_id"]).partition("|")[0]
61
+ filename = find_str(source["title"])
62
+ result.file_id = ".".join([dataset_id, filename])
63
+ result.checksum = find_str(source["checksum"])
64
+ Base.compute_sha(result) # type: ignore
65
+ return result
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator, MutableMapping
4
+ from enum import Enum
5
+
6
+ import sqlalchemy as sa
7
+ from sqlalchemy.orm import Mapped, mapped_column
8
+
9
+ from esgpull.models.base import Base
10
+
11
+
12
+ class Option(Enum):
13
+ false = 0, False
14
+ true = 1, True
15
+ none = 2, None
16
+ notset = 3, None
17
+
18
+ @classmethod
19
+ def _missing_(cls, value):
20
+ if isinstance(value, str) and value in cls._member_map_:
21
+ return cls[value]
22
+ elif isinstance(value, bool):
23
+ if value:
24
+ return cls.true
25
+ else:
26
+ return cls.false
27
+ elif value is None:
28
+ return cls.none
29
+ else:
30
+ raise ValueError(value)
31
+
32
+ def is_set(self) -> bool:
33
+ return self in [
34
+ Option.true,
35
+ Option.false,
36
+ Option.none,
37
+ ]
38
+
39
+ def is_bool(self) -> bool:
40
+ return self in [Option.true, Option.false]
41
+
42
+ def __bool__(self) -> bool:
43
+ if not self.is_bool():
44
+ raise ValueError(self)
45
+ return self == Option.true
46
+
47
+
48
+ class Options(Base):
49
+ __tablename__ = "options"
50
+
51
+ distrib: Mapped[Option] = mapped_column(sa.Enum(Option))
52
+ latest: Mapped[Option] = mapped_column(sa.Enum(Option))
53
+ replica: Mapped[Option] = mapped_column(sa.Enum(Option))
54
+ retracted: Mapped[Option] = mapped_column(sa.Enum(Option))
55
+
56
+ _distrib_ = Option(False)
57
+ _latest_ = Option(True)
58
+ _replica_ = Option(None)
59
+ _retracted_ = Option(False)
60
+
61
+ @classmethod
62
+ def default(cls) -> Options:
63
+ return cls(
64
+ cls._distrib_,
65
+ cls._latest_,
66
+ cls._replica_,
67
+ cls._retracted_,
68
+ )
69
+
70
+ @classmethod
71
+ def _set_defaults(
72
+ cls,
73
+ distrib: str | bool | None,
74
+ latest: str | bool | None,
75
+ replica: str | bool | None,
76
+ retracted: str | bool | None,
77
+ ) -> None:
78
+ cls._distrib_ = Option(distrib)
79
+ cls._latest_ = Option(latest)
80
+ cls._replica_ = Option(replica)
81
+ cls._retracted_ = Option(retracted)
82
+
83
+ def __init__(
84
+ self,
85
+ distrib: Option | str | bool | None = Option.notset,
86
+ latest: Option | str | bool | None = Option.notset,
87
+ replica: Option | str | bool | None = Option.notset,
88
+ retracted: Option | str | bool | None = Option.notset,
89
+ ):
90
+ setattr(self, "distrib", distrib)
91
+ setattr(self, "latest", latest)
92
+ setattr(self, "replica", replica)
93
+ setattr(self, "retracted", retracted)
94
+
95
+ def __setattr__(
96
+ self,
97
+ name: str,
98
+ value: Option | str | bool | None,
99
+ ) -> None:
100
+ if name in self.__sql_attrs__:
101
+ super().__setattr__(name, value)
102
+ elif name in self._names:
103
+ super().__setattr__(name, Option(value))
104
+ else:
105
+ raise AttributeError(name)
106
+
107
+ def __getitem__(self, name: str) -> Option:
108
+ if name in self._names:
109
+ return getattr(self, name)
110
+ else:
111
+ raise KeyError(name)
112
+
113
+ def __setitem__(
114
+ self,
115
+ name: str,
116
+ value: Option | str | bool | None,
117
+ ) -> None:
118
+ setattr(self, name, value)
119
+
120
+ def _as_bytes(self) -> bytes:
121
+ self_tuple = (self.distrib, self.latest, self.replica, self.retracted)
122
+ return str(self_tuple).encode()
123
+
124
+ def items(
125
+ self,
126
+ use_default: bool = False,
127
+ keep_notset: bool = False,
128
+ ) -> Iterator[tuple[str, Option]]:
129
+ default = self.default()
130
+ for name in self._names:
131
+ option = getattr(self, name, Option.notset)
132
+ if option.is_set():
133
+ yield name, option
134
+ elif use_default:
135
+ yield name, getattr(default, name)
136
+ elif keep_notset:
137
+ yield name, option
138
+
139
+ def asdict(self) -> MutableMapping[str, bool | None]:
140
+ return {name: option.value[1] for name, option in self.items()}
141
+
142
+ def __bool__(self) -> bool:
143
+ return next(self.items(), None) is not None
144
+
145
+ def __rich_repr__(self) -> Iterator:
146
+ for name, option in self.items():
147
+ yield name, option.value
148
+
149
+ def __repr__(self) -> str:
150
+ cls_name = self.__class__.__name__
151
+ items = [f"{k}={v}" for k, v in self.__rich_repr__()]
152
+ return f"{cls_name}(" + ", ".join(items) + ")"
153
+
154
+ def trackable(self) -> bool:
155
+ return all(opt.is_set() for (_, opt) in self.items(keep_notset=True))
156
+
157
+ def apply_defaults(self, parent: Options):
158
+ for name, opt in self.items(keep_notset=True):
159
+ if opt.is_set():
160
+ continue
161
+ elif parent[name].is_set():
162
+ self[name] = parent[name]
163
+ else:
164
+ self[name] = self.default()[name]