vcti-deck 1.0.1__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.
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2018-2026 Visual Collaboration Technologies Inc.
2
+ All Rights Reserved.
3
+
4
+ This software is proprietary and confidential. Unauthorized copying,
5
+ distribution, or use of this software, via any medium, is strictly
6
+ prohibited. Access is granted only to authorized VCollab developers
7
+ and individuals explicitly authorized by Visual Collaboration
8
+ Technologies Inc.
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: vcti-deck
3
+ Version: 1.0.1
4
+ Summary: Generic ordered collection with named items and policy-based compatibility
5
+ Author: Visual Collaboration Technologies Inc.
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: test
10
+ Requires-Dist: pytest; extra == "test"
11
+ Requires-Dist: pytest-cov; extra == "test"
12
+ Provides-Extra: lint
13
+ Requires-Dist: ruff; extra == "lint"
14
+ Provides-Extra: typecheck
15
+ Requires-Dist: pyright; extra == "typecheck"
16
+ Dynamic: license-file
17
+
18
+ # Deck
19
+
20
+ Generic ordered collection with named items and policy-based compatibility
21
+ for Python.
22
+
23
+ ## Purpose
24
+
25
+ VCollab applications need ordered collections where items are referenced
26
+ by name, support custom ordering, and optionally enforce compatibility
27
+ rules. `Deck` provides this as a lightweight, zero-dependency building
28
+ block.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install vcti-deck
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Basic Deck
39
+
40
+ ```python
41
+ from vcti.deck import Deck
42
+
43
+ deck = Deck[str]()
44
+ deck.add("header", "Welcome")
45
+ deck.add("body", "Content")
46
+ deck.add("footer", "Copyright")
47
+
48
+ deck.get("header") # "Welcome"
49
+ deck.has("body") # True
50
+ "footer" in deck # True
51
+ len(deck) # 3
52
+ list(deck) # ["Welcome", "Content", "Copyright"]
53
+ ```
54
+
55
+ ### Custom Ordering
56
+
57
+ ```python
58
+ deck.set_order(["footer", "header"])
59
+ list(deck) # ["Copyright", "Welcome"] — body skipped
60
+ deck.ids() # ["footer", "header"]
61
+
62
+ deck.clear_order()
63
+ list(deck) # ["Welcome", "Content", "Copyright"] — insertion order
64
+ ```
65
+
66
+ ### PolicyBoundDeck
67
+
68
+ Use `PolicyBoundDeck` when items must satisfy compatibility rules:
69
+
70
+ ```python
71
+ from enum import StrEnum, auto
72
+ from vcti.deck import PolicyBoundDeck, CompatibilityPolicy
73
+
74
+ class LayerKind(StrEnum):
75
+ IMAGE = auto()
76
+ VECTOR = auto()
77
+
78
+ class Layer:
79
+ def __init__(self, kind: LayerKind):
80
+ self.kind = kind
81
+
82
+ class SameKindPolicy(CompatibilityPolicy[LayerKind, Layer]):
83
+ def is_compatible(self, item: Layer, deck_type: LayerKind) -> bool:
84
+ return item.kind == deck_type
85
+
86
+ image_deck = PolicyBoundDeck(deck_type=LayerKind.IMAGE, policy=SameKindPolicy())
87
+ image_deck.add("photo", Layer(LayerKind.IMAGE)) # OK
88
+ image_deck.add("lines", Layer(LayerKind.VECTOR)) # Raises ValueError
89
+ ```
90
+
91
+ ### Subclass Compatibility
92
+
93
+ For simpler cases, override `_is_compatible` directly:
94
+
95
+ ```python
96
+ from vcti.deck import Deck
97
+
98
+ class PositiveOnlyDeck(Deck[int]):
99
+ def _is_compatible(self, item: int) -> bool:
100
+ return item > 0
101
+
102
+ deck = PositiveOnlyDeck()
103
+ deck.add("a", 5) # OK
104
+ deck.add("b", -1) # Raises ValueError
105
+ ```
106
+
107
+ ## API Summary
108
+
109
+ ### Deck[ItemT]
110
+
111
+ | Method | Description |
112
+ |--------|-------------|
113
+ | `add(id, item, *, replace=False)` | Add item; raise if duplicate unless replace=True |
114
+ | `get(id)` | Return item or None |
115
+ | `has(id)` | Check if ID exists |
116
+ | `remove(id)` | Remove item; raise if missing |
117
+ | `set_order(ids)` | Set custom iteration order |
118
+ | `clear_order()` | Reset to insertion order |
119
+ | `ids()` | IDs in effective order |
120
+ | `items()` | Items in effective order |
121
+
122
+ ### PolicyBoundDeck[DeckTypeT, ItemT]
123
+
124
+ Extends `Deck` with:
125
+
126
+ | Attribute/Method | Description |
127
+ |-----------------|-------------|
128
+ | `deck_type` | The type/category this deck is bound to |
129
+ | `policy` | The `CompatibilityPolicy` instance |
130
+
131
+ ### CompatibilityPolicy[DeckTypeT, ItemT]
132
+
133
+ | Method | Description |
134
+ |--------|-------------|
135
+ | `is_compatible(item, deck_type)` | Return True if item is allowed |
136
+
137
+ ## Dependencies
138
+
139
+ None. Standard library only.
@@ -0,0 +1,122 @@
1
+ # Deck
2
+
3
+ Generic ordered collection with named items and policy-based compatibility
4
+ for Python.
5
+
6
+ ## Purpose
7
+
8
+ VCollab applications need ordered collections where items are referenced
9
+ by name, support custom ordering, and optionally enforce compatibility
10
+ rules. `Deck` provides this as a lightweight, zero-dependency building
11
+ block.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ pip install vcti-deck
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ### Basic Deck
22
+
23
+ ```python
24
+ from vcti.deck import Deck
25
+
26
+ deck = Deck[str]()
27
+ deck.add("header", "Welcome")
28
+ deck.add("body", "Content")
29
+ deck.add("footer", "Copyright")
30
+
31
+ deck.get("header") # "Welcome"
32
+ deck.has("body") # True
33
+ "footer" in deck # True
34
+ len(deck) # 3
35
+ list(deck) # ["Welcome", "Content", "Copyright"]
36
+ ```
37
+
38
+ ### Custom Ordering
39
+
40
+ ```python
41
+ deck.set_order(["footer", "header"])
42
+ list(deck) # ["Copyright", "Welcome"] — body skipped
43
+ deck.ids() # ["footer", "header"]
44
+
45
+ deck.clear_order()
46
+ list(deck) # ["Welcome", "Content", "Copyright"] — insertion order
47
+ ```
48
+
49
+ ### PolicyBoundDeck
50
+
51
+ Use `PolicyBoundDeck` when items must satisfy compatibility rules:
52
+
53
+ ```python
54
+ from enum import StrEnum, auto
55
+ from vcti.deck import PolicyBoundDeck, CompatibilityPolicy
56
+
57
+ class LayerKind(StrEnum):
58
+ IMAGE = auto()
59
+ VECTOR = auto()
60
+
61
+ class Layer:
62
+ def __init__(self, kind: LayerKind):
63
+ self.kind = kind
64
+
65
+ class SameKindPolicy(CompatibilityPolicy[LayerKind, Layer]):
66
+ def is_compatible(self, item: Layer, deck_type: LayerKind) -> bool:
67
+ return item.kind == deck_type
68
+
69
+ image_deck = PolicyBoundDeck(deck_type=LayerKind.IMAGE, policy=SameKindPolicy())
70
+ image_deck.add("photo", Layer(LayerKind.IMAGE)) # OK
71
+ image_deck.add("lines", Layer(LayerKind.VECTOR)) # Raises ValueError
72
+ ```
73
+
74
+ ### Subclass Compatibility
75
+
76
+ For simpler cases, override `_is_compatible` directly:
77
+
78
+ ```python
79
+ from vcti.deck import Deck
80
+
81
+ class PositiveOnlyDeck(Deck[int]):
82
+ def _is_compatible(self, item: int) -> bool:
83
+ return item > 0
84
+
85
+ deck = PositiveOnlyDeck()
86
+ deck.add("a", 5) # OK
87
+ deck.add("b", -1) # Raises ValueError
88
+ ```
89
+
90
+ ## API Summary
91
+
92
+ ### Deck[ItemT]
93
+
94
+ | Method | Description |
95
+ |--------|-------------|
96
+ | `add(id, item, *, replace=False)` | Add item; raise if duplicate unless replace=True |
97
+ | `get(id)` | Return item or None |
98
+ | `has(id)` | Check if ID exists |
99
+ | `remove(id)` | Remove item; raise if missing |
100
+ | `set_order(ids)` | Set custom iteration order |
101
+ | `clear_order()` | Reset to insertion order |
102
+ | `ids()` | IDs in effective order |
103
+ | `items()` | Items in effective order |
104
+
105
+ ### PolicyBoundDeck[DeckTypeT, ItemT]
106
+
107
+ Extends `Deck` with:
108
+
109
+ | Attribute/Method | Description |
110
+ |-----------------|-------------|
111
+ | `deck_type` | The type/category this deck is bound to |
112
+ | `policy` | The `CompatibilityPolicy` instance |
113
+
114
+ ### CompatibilityPolicy[DeckTypeT, ItemT]
115
+
116
+ | Method | Description |
117
+ |--------|-------------|
118
+ | `is_compatible(item, deck_type)` | Return True if item is allowed |
119
+
120
+ ## Dependencies
121
+
122
+ None. Standard library only.
@@ -0,0 +1,44 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vcti-deck"
7
+ version = "1.0.1"
8
+ description = "Generic ordered collection with named items and policy-based compatibility"
9
+ readme = "README.md"
10
+ authors = [
11
+ {name = "Visual Collaboration Technologies Inc."}
12
+ ]
13
+ requires-python = ">=3.12"
14
+ dependencies = []
15
+
16
+ [tool.setuptools.packages.find]
17
+ where = ["src"]
18
+ include = ["vcti.deck", "vcti.deck.*"]
19
+
20
+ [project.optional-dependencies]
21
+ test = ["pytest", "pytest-cov"]
22
+ lint = ["ruff"]
23
+ typecheck = ["pyright"]
24
+
25
+ [tool.setuptools.package-data]
26
+ "vcti.deck" = ["py.typed"]
27
+
28
+ [tool.setuptools]
29
+ zip-safe = true
30
+
31
+ [tool.pyright]
32
+ include = ["src"]
33
+ pythonVersion = "3.12"
34
+ typeCheckingMode = "standard"
35
+
36
+ [tool.pytest.ini_options]
37
+ addopts = "--cov=vcti.deck --cov-report=term-missing --cov-fail-under=95"
38
+
39
+ [tool.ruff]
40
+ target-version = "py312"
41
+ line-length = 99
42
+
43
+ [tool.ruff.lint]
44
+ select = ["E", "F", "W", "I", "UP"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """vcti.deck — generic ordered collection with named items and policy-based compatibility."""
4
+
5
+ from importlib.metadata import version
6
+
7
+ from .deck import Deck
8
+ from .policy_bound_deck import CompatibilityPolicy, PolicyBoundDeck
9
+
10
+ __version__ = version("vcti-deck")
11
+
12
+ __all__ = [
13
+ "__version__",
14
+ "CompatibilityPolicy",
15
+ "Deck",
16
+ "PolicyBoundDeck",
17
+ ]
@@ -0,0 +1,183 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Deck — a generic ordered collection with custom ordering support.
4
+
5
+ A Deck maintains items in order with support for both insertion order and
6
+ user-defined custom ordering. Each item is associated with a unique string
7
+ identifier, making it suitable for collections where items need to be
8
+ referenced by name and reordered flexibly.
9
+
10
+ Example::
11
+
12
+ from vcti.deck import Deck
13
+
14
+ deck = Deck[str]()
15
+ deck.add("first", "Item 1")
16
+ deck.add("second", "Item 2")
17
+ deck.set_order(["second", "first"])
18
+ list(deck) # Custom order: ['Item 2', 'Item 1']
19
+ """
20
+
21
+ from collections import OrderedDict
22
+ from collections.abc import Iterable, Iterator
23
+
24
+ _MISSING: object = object()
25
+
26
+
27
+ class Deck[ItemT]:
28
+ """A generic ordered collection of named items with compatibility and custom ordering.
29
+
30
+ This class provides a container that maintains items in a specific order,
31
+ with support for both insertion order and user-defined custom ordering.
32
+ Each item is associated with a unique string identifier.
33
+
34
+ This class is **not** thread-safe. External synchronisation is required
35
+ when a single ``Deck`` instance is accessed from multiple threads.
36
+
37
+ Items may be any value allowed by *ItemT*, including ``None``. The
38
+ deck places no constraints on item values — only on item IDs (which
39
+ must be non-empty strings) and compatibility (via ``_is_compatible``).
40
+
41
+ Type Parameters:
42
+ ItemT: The type of items stored in the deck.
43
+ """
44
+
45
+ __slots__ = ("_items", "_user_order")
46
+
47
+ def __init__(self) -> None:
48
+ """Initialize an empty deck with no custom ordering."""
49
+ self._items: OrderedDict[str, ItemT] = OrderedDict()
50
+ self._user_order: list[str] | None = None
51
+
52
+ def add(self, item_id: str, item: ItemT, *, replace: bool = False) -> None:
53
+ """Add an item by ID, ensuring compatibility and maintaining insertion order.
54
+
55
+ Args:
56
+ item_id: Unique identifier for the item.
57
+ item: The item to add.
58
+ replace: If True, replace existing item with same ID; otherwise raise.
59
+
60
+ Raises:
61
+ ValueError: If item_id is not a non-empty string.
62
+ ValueError: If the item is incompatible (as determined by _is_compatible).
63
+ KeyError: If an item with the same ID already exists and replace is False.
64
+ """
65
+ if not isinstance(item_id, str) or not item_id:
66
+ raise ValueError(f"item_id must be a non-empty string, got {item_id!r}")
67
+
68
+ if not self._is_compatible(item):
69
+ raise ValueError(f"Incompatible item {item!r} can't be added.")
70
+
71
+ if item_id in self._items and not replace:
72
+ raise KeyError(f"Item with id '{item_id}' already exists.")
73
+
74
+ self._items[item_id] = item
75
+
76
+ if self._user_order is not None and item_id not in self._user_order:
77
+ self._user_order.append(item_id)
78
+
79
+ def get(self, item_id: str, default: ItemT | None = None) -> ItemT | None:
80
+ """Return an item by ID, or *default* if not found.
81
+
82
+ Args:
83
+ item_id: The identifier to look up.
84
+ default: Value to return when *item_id* is absent (default ``None``).
85
+ """
86
+ return self._items.get(item_id, default)
87
+
88
+ def has(self, item_id: str) -> bool:
89
+ """Check whether an item with the given ID exists."""
90
+ return item_id in self._items
91
+
92
+ def remove(self, item_id: str) -> None:
93
+ """Remove an item by ID.
94
+
95
+ Raises:
96
+ KeyError: If no item with the given ID exists.
97
+ """
98
+ try:
99
+ del self._items[item_id]
100
+ except KeyError:
101
+ raise KeyError(f"Item with id '{item_id}' not found.") from None
102
+
103
+ if self._user_order is not None and item_id in self._user_order:
104
+ self._user_order.remove(item_id)
105
+
106
+ def set_order(self, order: Iterable[str]) -> None:
107
+ """Define a custom user-specified order for iteration.
108
+
109
+ Only items listed in the provided order will be included in iteration.
110
+ Items not listed will be skipped.
111
+
112
+ Args:
113
+ order: Iterable of item IDs in the desired order.
114
+
115
+ Raises:
116
+ KeyError: If any ID in the order doesn't exist in the deck.
117
+ ValueError: If any ID appears more than once in the order.
118
+ """
119
+ order_list = list(order)
120
+
121
+ unknown = [oid for oid in order_list if oid not in self._items]
122
+ if unknown:
123
+ raise KeyError(f"Unknown item IDs in custom order: {unknown}")
124
+
125
+ seen: set[str] = set()
126
+ duplicates: list[str] = []
127
+ for oid in order_list:
128
+ if oid in seen:
129
+ duplicates.append(oid)
130
+ seen.add(oid)
131
+ if duplicates:
132
+ raise ValueError(f"Duplicate item IDs in custom order: {duplicates}")
133
+
134
+ self._user_order = order_list
135
+
136
+ def clear_order(self) -> None:
137
+ """Reset to insertion order, removing any custom ordering."""
138
+ self._user_order = None
139
+
140
+ def ids(self) -> list[str]:
141
+ """Return item IDs in the effective order (user-defined or insertion)."""
142
+ if self._user_order is not None:
143
+ return [oid for oid in self._user_order if oid in self._items]
144
+ return list(self._items.keys())
145
+
146
+ def items(self) -> list[ItemT]:
147
+ """Return items in the effective order."""
148
+ return [self._items[oid] for oid in self.ids()]
149
+
150
+ def _is_compatible(self, item: ItemT) -> bool:
151
+ """Hook for subclasses to implement item compatibility checks.
152
+
153
+ Override this method to enforce type-specific compatibility rules
154
+ when adding items to the deck. Returns True by default.
155
+ """
156
+ return True
157
+
158
+ def __iter__(self) -> Iterator[ItemT]:
159
+ """Iterate over items in the effective order.
160
+
161
+ Iteration uses a snapshot of the current order taken when iteration
162
+ begins. Items removed from the deck after the snapshot is taken are
163
+ silently skipped. Items added after the snapshot are not visited.
164
+ """
165
+ for oid in self.ids():
166
+ if oid in self._items:
167
+ yield self._items[oid]
168
+
169
+ def __len__(self) -> int:
170
+ """Return the number of items in the deck."""
171
+ return len(self._items)
172
+
173
+ def __contains__(self, item_id: object) -> bool:
174
+ """Check if an item ID exists in the deck.
175
+
176
+ Accepts any object to conform to the ``__contains__`` protocol.
177
+ Non-string values always return ``False``.
178
+ """
179
+ return item_id in self._items
180
+
181
+ def __repr__(self) -> str:
182
+ """Return a string representation showing item IDs."""
183
+ return f"{self.__class__.__name__}({list(self._items.keys())})"
@@ -0,0 +1,99 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """PolicyBoundDeck — a Deck that enforces item compatibility via a pluggable policy.
4
+
5
+ Example::
6
+
7
+ from vcti.deck import Deck, PolicyBoundDeck, CompatibilityPolicy
8
+
9
+ class PositiveOnly(CompatibilityPolicy[str, int]):
10
+ def is_compatible(self, item: int, deck_type: str) -> bool:
11
+ return item > 0
12
+
13
+ deck = PolicyBoundDeck(deck_type="positive", policy=PositiveOnly())
14
+ deck.add("a", 5) # OK
15
+ deck.add("b", -1) # Raises ValueError
16
+ """
17
+
18
+ from abc import ABC, abstractmethod
19
+
20
+ from .deck import Deck
21
+
22
+
23
+ class CompatibilityPolicy[DeckTypeT, ItemT](ABC):
24
+ """Abstract base for compatibility policies.
25
+
26
+ Implement ``is_compatible`` to define rules governing which items
27
+ can be added to a deck of a given type.
28
+
29
+ Type Parameters:
30
+ DeckTypeT: The type/category of the deck.
31
+ ItemT: The type of items to validate.
32
+ """
33
+
34
+ @abstractmethod
35
+ def is_compatible(self, item: ItemT, deck_type: DeckTypeT) -> bool:
36
+ """Check if *item* is compatible with *deck_type*.
37
+
38
+ Returns:
39
+ True if the item may be added, False otherwise.
40
+ """
41
+ ...
42
+
43
+
44
+ class PolicyBoundDeck[DeckTypeT, ItemT](Deck[ItemT]):
45
+ """A Deck bound to a compatibility policy.
46
+
47
+ Extends :class:`Deck` by delegating ``_is_compatible`` to an external
48
+ :class:`CompatibilityPolicy` instance. The policy is set at construction
49
+ time and cannot be changed.
50
+
51
+ Type Parameters:
52
+ DeckTypeT: The type/category of this deck.
53
+ ItemT: The type of items stored in this deck.
54
+
55
+ Example::
56
+
57
+ from enum import StrEnum, auto
58
+ from vcti.deck import PolicyBoundDeck, CompatibilityPolicy
59
+
60
+ class LayerKind(StrEnum):
61
+ IMAGE = auto()
62
+ VECTOR = auto()
63
+
64
+ class Layer:
65
+ def __init__(self, kind: LayerKind):
66
+ self.kind = kind
67
+
68
+ class SameKindPolicy(CompatibilityPolicy[LayerKind, Layer]):
69
+ def is_compatible(self, item: Layer, deck_type: LayerKind) -> bool:
70
+ return item.kind == deck_type
71
+
72
+ image_deck = PolicyBoundDeck(deck_type=LayerKind.IMAGE, policy=SameKindPolicy())
73
+ image_deck.add("img1", Layer(LayerKind.IMAGE)) # OK
74
+ image_deck.add("vec1", Layer(LayerKind.VECTOR)) # Raises ValueError
75
+ """
76
+
77
+ __slots__ = ("deck_type", "policy")
78
+
79
+ def __init__(
80
+ self,
81
+ deck_type: DeckTypeT,
82
+ policy: CompatibilityPolicy[DeckTypeT, ItemT],
83
+ ) -> None:
84
+ """Initialize with a deck type and a bound compatibility policy.
85
+
86
+ Args:
87
+ deck_type: The type/category of this deck, forwarded to the policy.
88
+ policy: The compatibility policy that governs item admission.
89
+ """
90
+ super().__init__()
91
+ self.deck_type = deck_type
92
+ self.policy = policy
93
+
94
+ def _is_compatible(self, item: ItemT) -> bool:
95
+ """Delegate compatibility check to the bound policy."""
96
+ return self.policy.is_compatible(item, self.deck_type)
97
+
98
+ def __repr__(self) -> str:
99
+ return f"PolicyBoundDeck(type={self.deck_type}, items={len(self)})"
File without changes
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: vcti-deck
3
+ Version: 1.0.1
4
+ Summary: Generic ordered collection with named items and policy-based compatibility
5
+ Author: Visual Collaboration Technologies Inc.
6
+ Requires-Python: >=3.12
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Provides-Extra: test
10
+ Requires-Dist: pytest; extra == "test"
11
+ Requires-Dist: pytest-cov; extra == "test"
12
+ Provides-Extra: lint
13
+ Requires-Dist: ruff; extra == "lint"
14
+ Provides-Extra: typecheck
15
+ Requires-Dist: pyright; extra == "typecheck"
16
+ Dynamic: license-file
17
+
18
+ # Deck
19
+
20
+ Generic ordered collection with named items and policy-based compatibility
21
+ for Python.
22
+
23
+ ## Purpose
24
+
25
+ VCollab applications need ordered collections where items are referenced
26
+ by name, support custom ordering, and optionally enforce compatibility
27
+ rules. `Deck` provides this as a lightweight, zero-dependency building
28
+ block.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install vcti-deck
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ### Basic Deck
39
+
40
+ ```python
41
+ from vcti.deck import Deck
42
+
43
+ deck = Deck[str]()
44
+ deck.add("header", "Welcome")
45
+ deck.add("body", "Content")
46
+ deck.add("footer", "Copyright")
47
+
48
+ deck.get("header") # "Welcome"
49
+ deck.has("body") # True
50
+ "footer" in deck # True
51
+ len(deck) # 3
52
+ list(deck) # ["Welcome", "Content", "Copyright"]
53
+ ```
54
+
55
+ ### Custom Ordering
56
+
57
+ ```python
58
+ deck.set_order(["footer", "header"])
59
+ list(deck) # ["Copyright", "Welcome"] — body skipped
60
+ deck.ids() # ["footer", "header"]
61
+
62
+ deck.clear_order()
63
+ list(deck) # ["Welcome", "Content", "Copyright"] — insertion order
64
+ ```
65
+
66
+ ### PolicyBoundDeck
67
+
68
+ Use `PolicyBoundDeck` when items must satisfy compatibility rules:
69
+
70
+ ```python
71
+ from enum import StrEnum, auto
72
+ from vcti.deck import PolicyBoundDeck, CompatibilityPolicy
73
+
74
+ class LayerKind(StrEnum):
75
+ IMAGE = auto()
76
+ VECTOR = auto()
77
+
78
+ class Layer:
79
+ def __init__(self, kind: LayerKind):
80
+ self.kind = kind
81
+
82
+ class SameKindPolicy(CompatibilityPolicy[LayerKind, Layer]):
83
+ def is_compatible(self, item: Layer, deck_type: LayerKind) -> bool:
84
+ return item.kind == deck_type
85
+
86
+ image_deck = PolicyBoundDeck(deck_type=LayerKind.IMAGE, policy=SameKindPolicy())
87
+ image_deck.add("photo", Layer(LayerKind.IMAGE)) # OK
88
+ image_deck.add("lines", Layer(LayerKind.VECTOR)) # Raises ValueError
89
+ ```
90
+
91
+ ### Subclass Compatibility
92
+
93
+ For simpler cases, override `_is_compatible` directly:
94
+
95
+ ```python
96
+ from vcti.deck import Deck
97
+
98
+ class PositiveOnlyDeck(Deck[int]):
99
+ def _is_compatible(self, item: int) -> bool:
100
+ return item > 0
101
+
102
+ deck = PositiveOnlyDeck()
103
+ deck.add("a", 5) # OK
104
+ deck.add("b", -1) # Raises ValueError
105
+ ```
106
+
107
+ ## API Summary
108
+
109
+ ### Deck[ItemT]
110
+
111
+ | Method | Description |
112
+ |--------|-------------|
113
+ | `add(id, item, *, replace=False)` | Add item; raise if duplicate unless replace=True |
114
+ | `get(id)` | Return item or None |
115
+ | `has(id)` | Check if ID exists |
116
+ | `remove(id)` | Remove item; raise if missing |
117
+ | `set_order(ids)` | Set custom iteration order |
118
+ | `clear_order()` | Reset to insertion order |
119
+ | `ids()` | IDs in effective order |
120
+ | `items()` | Items in effective order |
121
+
122
+ ### PolicyBoundDeck[DeckTypeT, ItemT]
123
+
124
+ Extends `Deck` with:
125
+
126
+ | Attribute/Method | Description |
127
+ |-----------------|-------------|
128
+ | `deck_type` | The type/category this deck is bound to |
129
+ | `policy` | The `CompatibilityPolicy` instance |
130
+
131
+ ### CompatibilityPolicy[DeckTypeT, ItemT]
132
+
133
+ | Method | Description |
134
+ |--------|-------------|
135
+ | `is_compatible(item, deck_type)` | Return True if item is allowed |
136
+
137
+ ## Dependencies
138
+
139
+ None. Standard library only.
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/vcti/deck/__init__.py
5
+ src/vcti/deck/deck.py
6
+ src/vcti/deck/policy_bound_deck.py
7
+ src/vcti/deck/py.typed
8
+ src/vcti_deck.egg-info/PKG-INFO
9
+ src/vcti_deck.egg-info/SOURCES.txt
10
+ src/vcti_deck.egg-info/dependency_links.txt
11
+ src/vcti_deck.egg-info/requires.txt
12
+ src/vcti_deck.egg-info/top_level.txt
13
+ src/vcti_deck.egg-info/zip-safe
14
+ tests/test_deck.py
15
+ tests/test_policy_bound_deck.py
@@ -0,0 +1,10 @@
1
+
2
+ [lint]
3
+ ruff
4
+
5
+ [test]
6
+ pytest
7
+ pytest-cov
8
+
9
+ [typecheck]
10
+ pyright
@@ -0,0 +1,404 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Tests for Deck."""
4
+
5
+ import re
6
+
7
+ import pytest
8
+
9
+ import vcti.deck
10
+ from vcti.deck import Deck
11
+
12
+
13
+ class TestVersion:
14
+ def test_version_exists(self):
15
+ assert hasattr(vcti.deck, "__version__")
16
+
17
+ def test_version_is_valid_semver(self):
18
+ assert re.match(r"^\d+\.\d+\.\d+", vcti.deck.__version__)
19
+
20
+
21
+ class TestDeckAdd:
22
+ def test_add_single(self):
23
+ deck = Deck[str]()
24
+ deck.add("a", "alpha")
25
+ assert deck.get("a") == "alpha"
26
+ assert len(deck) == 1
27
+
28
+ def test_add_multiple_preserves_insertion_order(self):
29
+ deck = Deck[int]()
30
+ deck.add("x", 1)
31
+ deck.add("y", 2)
32
+ deck.add("z", 3)
33
+ assert deck.ids() == ["x", "y", "z"]
34
+ assert deck.items() == [1, 2, 3]
35
+
36
+ def test_add_duplicate_raises(self):
37
+ deck = Deck[str]()
38
+ deck.add("a", "first")
39
+ with pytest.raises(KeyError, match="already exists"):
40
+ deck.add("a", "second")
41
+
42
+ def test_add_duplicate_with_replace(self):
43
+ deck = Deck[str]()
44
+ deck.add("a", "first")
45
+ deck.add("a", "second", replace=True)
46
+ assert deck.get("a") == "second"
47
+ assert len(deck) == 1
48
+
49
+ def test_add_during_custom_order(self):
50
+ deck = Deck[str]()
51
+ deck.add("a", "alpha")
52
+ deck.add("b", "beta")
53
+ deck.set_order(["b", "a"])
54
+ deck.add("c", "gamma")
55
+ assert deck.ids() == ["b", "a", "c"]
56
+
57
+ def test_add_non_string_id_raises(self):
58
+ deck = Deck[str]()
59
+ with pytest.raises(ValueError, match="non-empty string"):
60
+ deck.add(123, "alpha") # type: ignore[arg-type]
61
+
62
+ def test_add_none_id_raises(self):
63
+ deck = Deck[str]()
64
+ with pytest.raises(ValueError, match="non-empty string"):
65
+ deck.add(None, "alpha") # type: ignore[arg-type]
66
+
67
+ def test_add_empty_string_id_raises(self):
68
+ deck = Deck[str]()
69
+ with pytest.raises(ValueError, match="non-empty string"):
70
+ deck.add("", "alpha")
71
+
72
+
73
+ class TestDeckGet:
74
+ def test_get_existing(self):
75
+ deck = Deck[str]()
76
+ deck.add("a", "alpha")
77
+ assert deck.get("a") == "alpha"
78
+
79
+ def test_get_missing_returns_none(self):
80
+ deck = Deck[str]()
81
+ assert deck.get("missing") is None
82
+
83
+ def test_get_missing_returns_default(self):
84
+ deck = Deck[str]()
85
+ assert deck.get("missing", "fallback") == "fallback"
86
+
87
+ def test_get_existing_ignores_default(self):
88
+ deck = Deck[str]()
89
+ deck.add("a", "alpha")
90
+ assert deck.get("a", "fallback") == "alpha"
91
+
92
+ def test_get_distinguishes_none_value_from_missing(self):
93
+ sentinel = object()
94
+ deck = Deck[object | None]()
95
+ deck.add("a", None)
96
+ assert deck.get("a", sentinel) is None
97
+ assert deck.get("missing", sentinel) is sentinel
98
+
99
+
100
+ class TestDeckHas:
101
+ def test_has_existing(self):
102
+ deck = Deck[str]()
103
+ deck.add("a", "alpha")
104
+ assert deck.has("a") is True
105
+
106
+ def test_has_missing(self):
107
+ deck = Deck[str]()
108
+ assert deck.has("missing") is False
109
+
110
+
111
+ class TestDeckContains:
112
+ def test_contains(self):
113
+ deck = Deck[str]()
114
+ deck.add("a", "alpha")
115
+ assert "a" in deck
116
+ assert "b" not in deck
117
+
118
+ def test_contains_non_string_returns_false(self):
119
+ deck = Deck[str]()
120
+ deck.add("a", "alpha")
121
+ assert 123 not in deck # type: ignore[operator]
122
+ assert None not in deck # type: ignore[operator]
123
+
124
+
125
+ class TestDeckRemove:
126
+ def test_remove_existing(self):
127
+ deck = Deck[str]()
128
+ deck.add("a", "alpha")
129
+ deck.remove("a")
130
+ assert len(deck) == 0
131
+ assert deck.get("a") is None
132
+
133
+ def test_remove_missing_raises(self):
134
+ deck = Deck[str]()
135
+ with pytest.raises(KeyError, match="not found"):
136
+ deck.remove("missing")
137
+
138
+ def test_remove_updates_custom_order(self):
139
+ deck = Deck[str]()
140
+ deck.add("a", "alpha")
141
+ deck.add("b", "beta")
142
+ deck.set_order(["b", "a"])
143
+ deck.remove("b")
144
+ assert deck.ids() == ["a"]
145
+
146
+
147
+ class TestDeckOrder:
148
+ def test_default_insertion_order(self):
149
+ deck = Deck[str]()
150
+ deck.add("c", "gamma")
151
+ deck.add("a", "alpha")
152
+ deck.add("b", "beta")
153
+ assert deck.ids() == ["c", "a", "b"]
154
+
155
+ def test_set_order(self):
156
+ deck = Deck[str]()
157
+ deck.add("a", "alpha")
158
+ deck.add("b", "beta")
159
+ deck.add("c", "gamma")
160
+ deck.set_order(["c", "a", "b"])
161
+ assert deck.ids() == ["c", "a", "b"]
162
+ assert deck.items() == ["gamma", "alpha", "beta"]
163
+
164
+ def test_set_order_partial(self):
165
+ deck = Deck[str]()
166
+ deck.add("a", "alpha")
167
+ deck.add("b", "beta")
168
+ deck.add("c", "gamma")
169
+ deck.set_order(["b"])
170
+ assert deck.ids() == ["b"]
171
+
172
+ def test_set_order_unknown_id_raises(self):
173
+ deck = Deck[str]()
174
+ deck.add("a", "alpha")
175
+ with pytest.raises(KeyError, match="Unknown"):
176
+ deck.set_order(["a", "missing"])
177
+
178
+ def test_set_order_duplicate_ids_raises(self):
179
+ deck = Deck[str]()
180
+ deck.add("a", "alpha")
181
+ deck.add("b", "beta")
182
+ with pytest.raises(ValueError, match="Duplicate"):
183
+ deck.set_order(["a", "b", "a"])
184
+
185
+ def test_set_order_empty(self):
186
+ deck = Deck[str]()
187
+ deck.add("a", "alpha")
188
+ deck.add("b", "beta")
189
+ deck.set_order([])
190
+ assert deck.ids() == []
191
+ assert deck.items() == []
192
+ assert list(deck) == []
193
+
194
+ def test_clear_order(self):
195
+ deck = Deck[str]()
196
+ deck.add("a", "alpha")
197
+ deck.add("b", "beta")
198
+ deck.set_order(["b", "a"])
199
+ deck.clear_order()
200
+ assert deck.ids() == ["a", "b"]
201
+
202
+ def test_clear_order_noop_when_no_custom_order(self):
203
+ deck = Deck[str]()
204
+ deck.add("a", "alpha")
205
+ deck.clear_order()
206
+ assert deck.ids() == ["a"]
207
+
208
+
209
+ class TestDeckIteration:
210
+ def test_iter(self):
211
+ deck = Deck[str]()
212
+ deck.add("a", "alpha")
213
+ deck.add("b", "beta")
214
+ assert list(deck) == ["alpha", "beta"]
215
+
216
+ def test_iter_with_custom_order(self):
217
+ deck = Deck[str]()
218
+ deck.add("a", "alpha")
219
+ deck.add("b", "beta")
220
+ deck.set_order(["b", "a"])
221
+ assert list(deck) == ["beta", "alpha"]
222
+
223
+ def test_len(self):
224
+ deck = Deck[str]()
225
+ assert len(deck) == 0
226
+ deck.add("a", "alpha")
227
+ assert len(deck) == 1
228
+
229
+ def test_repr(self):
230
+ deck = Deck[str]()
231
+ deck.add("a", "alpha")
232
+ assert "Deck" in repr(deck)
233
+ assert "'a'" in repr(deck)
234
+
235
+
236
+ class TestDeckIterationMutation:
237
+ def test_remove_during_iteration_skips_removed(self):
238
+ deck = Deck[str]()
239
+ deck.add("a", "alpha")
240
+ deck.add("b", "beta")
241
+ deck.add("c", "gamma")
242
+ collected = []
243
+ for item in deck:
244
+ collected.append(item)
245
+ if item == "alpha":
246
+ deck.remove("b")
247
+ assert collected == ["alpha", "gamma"]
248
+
249
+ def test_add_during_iteration_not_visited(self):
250
+ deck = Deck[str]()
251
+ deck.add("a", "alpha")
252
+ deck.add("b", "beta")
253
+ collected = []
254
+ for item in deck:
255
+ collected.append(item)
256
+ if item == "alpha":
257
+ deck.add("c", "gamma")
258
+ assert collected == ["alpha", "beta"]
259
+ assert deck.has("c")
260
+
261
+ def test_set_order_during_iteration_no_effect(self):
262
+ deck = Deck[str]()
263
+ deck.add("a", "alpha")
264
+ deck.add("b", "beta")
265
+ deck.add("c", "gamma")
266
+ collected = []
267
+ for item in deck:
268
+ collected.append(item)
269
+ if item == "alpha":
270
+ deck.set_order(["c", "b", "a"])
271
+ assert collected == ["alpha", "beta", "gamma"]
272
+
273
+ def test_clear_order_during_iteration_no_effect(self):
274
+ deck = Deck[str]()
275
+ deck.add("a", "alpha")
276
+ deck.add("b", "beta")
277
+ deck.set_order(["b", "a"])
278
+ collected = []
279
+ for item in deck:
280
+ collected.append(item)
281
+ if item == "beta":
282
+ deck.clear_order()
283
+ assert collected == ["beta", "alpha"]
284
+
285
+ def test_remove_all_during_iteration(self):
286
+ deck = Deck[str]()
287
+ deck.add("a", "alpha")
288
+ deck.add("b", "beta")
289
+ deck.add("c", "gamma")
290
+ collected = []
291
+ for item in deck:
292
+ collected.append(item)
293
+ deck.remove("a")
294
+ deck.remove("b")
295
+ deck.remove("c")
296
+ break
297
+ assert collected == ["alpha"]
298
+ assert len(deck) == 0
299
+
300
+
301
+ class TestDeckEmpty:
302
+ def test_empty_iter(self):
303
+ assert list(Deck()) == []
304
+
305
+ def test_empty_ids(self):
306
+ assert Deck().ids() == []
307
+
308
+ def test_empty_items(self):
309
+ assert Deck().items() == []
310
+
311
+ def test_empty_len(self):
312
+ assert len(Deck()) == 0
313
+
314
+ def test_empty_repr(self):
315
+ assert repr(Deck()) == "Deck([])"
316
+
317
+
318
+ class TestDeckCompatibility:
319
+ def test_default_compatibility_accepts_all(self):
320
+ deck = Deck[object]()
321
+ deck.add("a", 42)
322
+ deck.add("b", "hello")
323
+ deck.add("c", [1, 2, 3])
324
+ assert len(deck) == 3
325
+
326
+ def test_subclass_can_reject(self):
327
+ class PositiveOnlyDeck(Deck[int]):
328
+ def _is_compatible(self, item: int) -> bool:
329
+ return item > 0
330
+
331
+ deck = PositiveOnlyDeck()
332
+ deck.add("a", 5)
333
+ with pytest.raises(ValueError, match="Incompatible"):
334
+ deck.add("b", -1)
335
+
336
+ def test_none_item_allowed(self):
337
+ deck = Deck[None]()
338
+ deck.add("a", None)
339
+ assert deck.get("a") is None
340
+ assert list(deck) == [None]
341
+
342
+
343
+ class TestDeckReentrant:
344
+ def test_set_order_inside_is_compatible(self):
345
+ """Calling set_order from within _is_compatible must not corrupt state."""
346
+
347
+ class ReorderingDeck(Deck[str]):
348
+ def _is_compatible(self, item: str) -> bool:
349
+ if len(self._items) >= 2:
350
+ self.set_order(sorted(self._items.keys()))
351
+ return True
352
+
353
+ deck = ReorderingDeck()
354
+ deck.add("b", "beta")
355
+ deck.add("a", "alpha")
356
+ deck.add("c", "gamma")
357
+ # set_order was called inside _is_compatible for "c"
358
+ assert deck.has("c")
359
+ assert set(deck.ids()) == {"a", "b", "c"}
360
+
361
+ def test_remove_inside_is_compatible(self):
362
+ """Calling remove from within _is_compatible must not corrupt state."""
363
+
364
+ class EvictingDeck(Deck[str]):
365
+ def _is_compatible(self, item: str) -> bool:
366
+ if self.has("evict_me"):
367
+ self.remove("evict_me")
368
+ return True
369
+
370
+ deck = EvictingDeck()
371
+ deck.add("evict_me", "temporary")
372
+ deck.add("keeper", "permanent")
373
+ assert not deck.has("evict_me")
374
+ assert deck.has("keeper")
375
+
376
+
377
+ class TestDeckScale:
378
+ def test_add_and_iterate_1000_items(self):
379
+ deck = Deck[int]()
380
+ n = 1_000
381
+ for i in range(n):
382
+ deck.add(f"item{i}", i)
383
+ assert len(deck) == n
384
+ assert list(deck) == list(range(n))
385
+
386
+ def test_custom_order_1000_items(self):
387
+ deck = Deck[int]()
388
+ n = 1_000
389
+ for i in range(n):
390
+ deck.add(f"item{i}", i)
391
+ reversed_ids = [f"item{i}" for i in reversed(range(n))]
392
+ deck.set_order(reversed_ids)
393
+ assert deck.ids() == reversed_ids
394
+ assert deck.items() == list(reversed(range(n)))
395
+
396
+ def test_add_remove_cycle_1000(self):
397
+ deck = Deck[int]()
398
+ n = 1_000
399
+ for i in range(n):
400
+ deck.add(f"item{i}", i)
401
+ for i in range(n):
402
+ deck.remove(f"item{i}")
403
+ assert len(deck) == 0
404
+ assert list(deck) == []
@@ -0,0 +1,123 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Tests for PolicyBoundDeck."""
4
+
5
+ from enum import StrEnum, auto
6
+
7
+ import pytest
8
+
9
+ from vcti.deck import CompatibilityPolicy, PolicyBoundDeck
10
+
11
+
12
+ class Kind(StrEnum):
13
+ ALPHA = auto()
14
+ BETA = auto()
15
+
16
+
17
+ class Item:
18
+ def __init__(self, kind: Kind):
19
+ self.kind = kind
20
+
21
+
22
+ class SameKindPolicy(CompatibilityPolicy[Kind, Item]):
23
+ def is_compatible(self, item: Item, deck_type: Kind) -> bool:
24
+ return item.kind == deck_type
25
+
26
+
27
+ class AcceptAllPolicy(CompatibilityPolicy[str, int]):
28
+ def is_compatible(self, item: int, deck_type: str) -> bool:
29
+ return True
30
+
31
+
32
+ class RejectAllPolicy(CompatibilityPolicy[str, int]):
33
+ def is_compatible(self, item: int, deck_type: str) -> bool:
34
+ return False
35
+
36
+
37
+ class TestPolicyBoundDeckBasic:
38
+ def test_compatible_item_accepted(self):
39
+ deck = PolicyBoundDeck(deck_type=Kind.ALPHA, policy=SameKindPolicy())
40
+ item = Item(Kind.ALPHA)
41
+ deck.add("a", item)
42
+ assert deck.get("a") is item
43
+
44
+ def test_incompatible_item_rejected(self):
45
+ deck = PolicyBoundDeck(deck_type=Kind.ALPHA, policy=SameKindPolicy())
46
+ item = Item(Kind.BETA)
47
+ with pytest.raises(ValueError, match="Incompatible"):
48
+ deck.add("b", item)
49
+
50
+ def test_accept_all_policy(self):
51
+ deck = PolicyBoundDeck(deck_type="any", policy=AcceptAllPolicy())
52
+ deck.add("a", 1)
53
+ deck.add("b", 2)
54
+ assert len(deck) == 2
55
+
56
+ def test_reject_all_policy(self):
57
+ deck = PolicyBoundDeck(deck_type="none", policy=RejectAllPolicy())
58
+ with pytest.raises(ValueError, match="Incompatible"):
59
+ deck.add("a", 1)
60
+
61
+
62
+ class TestPolicyBoundDeckAttributes:
63
+ def test_deck_type_stored(self):
64
+ deck = PolicyBoundDeck(deck_type=Kind.ALPHA, policy=SameKindPolicy())
65
+ assert deck.deck_type == Kind.ALPHA
66
+
67
+ def test_policy_stored(self):
68
+ policy = SameKindPolicy()
69
+ deck = PolicyBoundDeck(deck_type=Kind.ALPHA, policy=policy)
70
+ assert deck.policy is policy
71
+
72
+
73
+ class TestPolicyBoundDeckInheritsOrdering:
74
+ def test_custom_order(self):
75
+ deck = PolicyBoundDeck(deck_type=Kind.ALPHA, policy=SameKindPolicy())
76
+ deck.add("a", Item(Kind.ALPHA))
77
+ deck.add("b", Item(Kind.ALPHA))
78
+ deck.add("c", Item(Kind.ALPHA))
79
+ deck.set_order(["c", "a", "b"])
80
+ assert deck.ids() == ["c", "a", "b"]
81
+
82
+ def test_remove(self):
83
+ deck = PolicyBoundDeck(deck_type=Kind.ALPHA, policy=SameKindPolicy())
84
+ deck.add("a", Item(Kind.ALPHA))
85
+ deck.remove("a")
86
+ assert len(deck) == 0
87
+
88
+ def test_replace(self):
89
+ deck = PolicyBoundDeck(deck_type=Kind.ALPHA, policy=SameKindPolicy())
90
+ item1 = Item(Kind.ALPHA)
91
+ item2 = Item(Kind.ALPHA)
92
+ deck.add("a", item1)
93
+ deck.add("a", item2, replace=True)
94
+ assert deck.get("a") is item2
95
+
96
+
97
+ class TestCompatibilityPolicyIsAbstract:
98
+ def test_cannot_instantiate_base(self):
99
+ with pytest.raises(TypeError, match="abstract"):
100
+ CompatibilityPolicy()
101
+
102
+ def test_incomplete_subclass_cannot_instantiate(self):
103
+ class Incomplete(CompatibilityPolicy[str, int]):
104
+ pass
105
+
106
+ with pytest.raises(TypeError, match="abstract"):
107
+ Incomplete()
108
+
109
+ def test_replace_still_checks_compatibility(self):
110
+ deck = PolicyBoundDeck(deck_type=Kind.ALPHA, policy=SameKindPolicy())
111
+ deck.add("a", Item(Kind.ALPHA))
112
+ with pytest.raises(ValueError, match="Incompatible"):
113
+ deck.add("a", Item(Kind.BETA), replace=True)
114
+
115
+
116
+ class TestPolicyBoundDeckRepr:
117
+ def test_repr(self):
118
+ deck = PolicyBoundDeck(deck_type=Kind.ALPHA, policy=SameKindPolicy())
119
+ deck.add("a", Item(Kind.ALPHA))
120
+ r = repr(deck)
121
+ assert "PolicyBoundDeck" in r
122
+ assert "alpha" in r
123
+ assert "items=1" in r