IDKit 1.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.
- idkit/__init__.py +51 -0
- idkit/enums/__init__.py +12 -0
- idkit/enums/group.py +158 -0
- idkit/enums/operation.py +135 -0
- idkit/enums/role.py +85 -0
- idkit/enums/source.py +231 -0
- idkit/exceptions.py +10 -0
- idkit/identifier.py +440 -0
- idkit/idkit.py +58 -0
- idkit/protocols.py +86 -0
- idkit/py.typed +1 -0
- idkit/root.py +47 -0
- idkit-1.0.0.dist-info/METADATA +135 -0
- idkit-1.0.0.dist-info/RECORD +17 -0
- idkit-1.0.0.dist-info/WHEEL +5 -0
- idkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- idkit-1.0.0.dist-info/top_level.txt +1 -0
idkit/identifier.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from typing import Any, Iterator, Self
|
|
6
|
+
|
|
7
|
+
from .exceptions import IdentifierParseError, IdentifierValidationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Identifier[G: StrEnum, S: StrEnum, R: StrEnum]:
|
|
12
|
+
"""
|
|
13
|
+
Immutable typed identifier value.
|
|
14
|
+
|
|
15
|
+
The identifier format is:
|
|
16
|
+
|
|
17
|
+
Group::Source[-Component][+role]
|
|
18
|
+
|
|
19
|
+
The same source enum is used for both the required ``Source`` segment and
|
|
20
|
+
the optional ``Component`` segment. Instances support fluent construction
|
|
21
|
+
through attribute access when created from an ``IdentifierRoot``.
|
|
22
|
+
|
|
23
|
+
Examples:
|
|
24
|
+
system::runtime
|
|
25
|
+
system::runtime-agent
|
|
26
|
+
system::runtime-agent+analyzer
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
group: G
|
|
30
|
+
source: S | None = None
|
|
31
|
+
component: S | None = None
|
|
32
|
+
role: R | None = None
|
|
33
|
+
|
|
34
|
+
source_enum: type[S] | None = field(default=None, repr=False, compare=False)
|
|
35
|
+
role_enum: type[R] | None = field(default=None, repr=False, compare=False)
|
|
36
|
+
|
|
37
|
+
def __post_init__(self) -> None:
|
|
38
|
+
if self.component is not None and self.source is None:
|
|
39
|
+
raise IdentifierValidationError(
|
|
40
|
+
"Identifier cannot have a component without a source."
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def __getattr__(self, name: str) -> Identifier[G, S, R]:
|
|
44
|
+
if self.source_enum and name in self.source_enum.__members__:
|
|
45
|
+
if self.source is None or self.component is None:
|
|
46
|
+
return self.with_source_or_component(self.source_enum[name])
|
|
47
|
+
|
|
48
|
+
if self.role_enum and name in self.role_enum.__members__:
|
|
49
|
+
return self.with_role(self.role_enum[name])
|
|
50
|
+
|
|
51
|
+
raise AttributeError(name)
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def has_source(self) -> bool:
|
|
55
|
+
return self.source is not None
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def has_component(self) -> bool:
|
|
59
|
+
return self.component is not None
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def has_role(self) -> bool:
|
|
63
|
+
return self.role is not None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def has_action(self) -> bool:
|
|
67
|
+
return self.has_role
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def is_complete(self) -> bool:
|
|
71
|
+
return self.source is not None and self.role is not None
|
|
72
|
+
|
|
73
|
+
def require_source(self) -> Self:
|
|
74
|
+
if self.source is None:
|
|
75
|
+
raise IdentifierValidationError("Identifier requires a source.")
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def require_role(self) -> Self:
|
|
79
|
+
if self.role is None:
|
|
80
|
+
raise IdentifierValidationError("Identifier requires an role.")
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
def require_action(self) -> Self:
|
|
84
|
+
return self.require_role()
|
|
85
|
+
|
|
86
|
+
def require_complete(self) -> Self:
|
|
87
|
+
self.require_source()
|
|
88
|
+
self.require_role()
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def with_source(self, source: S) -> Self:
|
|
92
|
+
if self.source is not None:
|
|
93
|
+
raise IdentifierValidationError("Identifier already has a source.")
|
|
94
|
+
|
|
95
|
+
return type(self)(
|
|
96
|
+
group=self.group,
|
|
97
|
+
source=source,
|
|
98
|
+
component=self.component,
|
|
99
|
+
role=self.role,
|
|
100
|
+
source_enum=self.source_enum,
|
|
101
|
+
role_enum=self.role_enum,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def with_component(self, component: S) -> Self:
|
|
105
|
+
if self.source is None:
|
|
106
|
+
raise IdentifierValidationError("Cannot set component before source.")
|
|
107
|
+
|
|
108
|
+
if self.component is not None:
|
|
109
|
+
raise IdentifierValidationError("Identifier already has a component.")
|
|
110
|
+
|
|
111
|
+
return type(self)(
|
|
112
|
+
group=self.group,
|
|
113
|
+
source=self.source,
|
|
114
|
+
component=component,
|
|
115
|
+
role=self.role,
|
|
116
|
+
source_enum=self.source_enum,
|
|
117
|
+
role_enum=self.role_enum,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def with_role(self, role: R) -> Self:
|
|
121
|
+
if self.role is not None:
|
|
122
|
+
raise IdentifierValidationError("Identifier already has an role.")
|
|
123
|
+
|
|
124
|
+
return type(self)(
|
|
125
|
+
group=self.group,
|
|
126
|
+
source=self.source,
|
|
127
|
+
component=self.component,
|
|
128
|
+
role=role,
|
|
129
|
+
source_enum=self.source_enum,
|
|
130
|
+
role_enum=self.role_enum,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def with_source_or_component(self, source: S) -> Self:
|
|
134
|
+
if self.source is None:
|
|
135
|
+
return self.with_source(source)
|
|
136
|
+
|
|
137
|
+
if self.component is None:
|
|
138
|
+
return self.with_component(source)
|
|
139
|
+
|
|
140
|
+
raise IdentifierValidationError(
|
|
141
|
+
"Identifier already has both source and component."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def without_role(self) -> Self:
|
|
145
|
+
return type(self)(
|
|
146
|
+
group=self.group,
|
|
147
|
+
source=self.source,
|
|
148
|
+
component=self.component,
|
|
149
|
+
role=None,
|
|
150
|
+
source_enum=self.source_enum,
|
|
151
|
+
role_enum=self.role_enum,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def without_component(self) -> Self:
|
|
155
|
+
return type(self)(
|
|
156
|
+
group=self.group,
|
|
157
|
+
source=self.source,
|
|
158
|
+
component=None,
|
|
159
|
+
role=self.role,
|
|
160
|
+
source_enum=self.source_enum,
|
|
161
|
+
role_enum=self.role_enum,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def without_source(self) -> Self:
|
|
165
|
+
return type(self)(
|
|
166
|
+
group=self.group,
|
|
167
|
+
source=None,
|
|
168
|
+
component=None,
|
|
169
|
+
role=self.role,
|
|
170
|
+
source_enum=self.source_enum,
|
|
171
|
+
role_enum=self.role_enum,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def parent(self) -> Self:
|
|
176
|
+
if self.role is not None:
|
|
177
|
+
return self.without_role()
|
|
178
|
+
|
|
179
|
+
if self.component is not None:
|
|
180
|
+
return self.without_component()
|
|
181
|
+
|
|
182
|
+
if self.source is not None:
|
|
183
|
+
return type(self)(
|
|
184
|
+
group=self.group,
|
|
185
|
+
source_enum=self.source_enum,
|
|
186
|
+
role_enum=self.role_enum,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def parents(self) -> tuple[Self, ...]:
|
|
193
|
+
items: list[Self] = []
|
|
194
|
+
current = self
|
|
195
|
+
|
|
196
|
+
while current.parent != current:
|
|
197
|
+
current = current.parent
|
|
198
|
+
items.append(current)
|
|
199
|
+
|
|
200
|
+
return tuple(items)
|
|
201
|
+
|
|
202
|
+
def matches(self, other: Identifier[G, S, R] | str) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Prefix-style hierarchy match.
|
|
205
|
+
|
|
206
|
+
ID.system.runtime.agent.analyzer.matches(ID.system.runtime) == True
|
|
207
|
+
"""
|
|
208
|
+
if isinstance(other, str):
|
|
209
|
+
if self.source_enum is None or self.role_enum is None:
|
|
210
|
+
return self.value == other
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
other = type(self).parse(
|
|
214
|
+
other,
|
|
215
|
+
group_enum=type(self.group),
|
|
216
|
+
source_enum=self.source_enum,
|
|
217
|
+
role_enum=self.role_enum,
|
|
218
|
+
)
|
|
219
|
+
except IdentifierParseError:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
if self.group != other.group:
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
if other.source is not None and self.source != other.source:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
if other.component is not None and self.component != other.component:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
if other.role is not None and self.role != other.role:
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
return True
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def parts(self) -> tuple[G, S | None, S | None, R | None]:
|
|
238
|
+
return self.group, self.source, self.component, self.role
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def string_parts(self) -> tuple[str, ...]:
|
|
242
|
+
return tuple(
|
|
243
|
+
part.value
|
|
244
|
+
for part in (self.group, self.source, self.component, self.role)
|
|
245
|
+
if part is not None
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
@property
|
|
249
|
+
def namespace(self) -> str:
|
|
250
|
+
if self.source is None:
|
|
251
|
+
return self.group.value
|
|
252
|
+
|
|
253
|
+
value = f"{self.group.value}::{self.source.value}"
|
|
254
|
+
|
|
255
|
+
if self.component is not None:
|
|
256
|
+
value += f"-{self.component.value}"
|
|
257
|
+
|
|
258
|
+
return value
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def qualified(self) -> str:
|
|
262
|
+
if self.role is None:
|
|
263
|
+
return self.namespace
|
|
264
|
+
|
|
265
|
+
return f"{self.namespace}+{self.role.value}"
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def value(self) -> str:
|
|
269
|
+
return self.qualified
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def path(self) -> str:
|
|
273
|
+
return "/".join(self.string_parts)
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def slug(self) -> str:
|
|
277
|
+
return "-".join(self.string_parts)
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def metric_key(self) -> str:
|
|
281
|
+
return ".".join(self.string_parts)
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def cache_key(self) -> str:
|
|
285
|
+
return self.value
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def event_topic(self) -> str:
|
|
289
|
+
return self.path
|
|
290
|
+
|
|
291
|
+
def to_dict(self) -> dict[str, Any]:
|
|
292
|
+
return {
|
|
293
|
+
"group": self.group.value,
|
|
294
|
+
"source": self.source.value if self.source else None,
|
|
295
|
+
"component": self.component.value if self.component else None,
|
|
296
|
+
"role": self.role.value if self.role else None,
|
|
297
|
+
"namespace": self.namespace,
|
|
298
|
+
"qualified": self.qualified,
|
|
299
|
+
"path": self.path,
|
|
300
|
+
"slug": self.slug,
|
|
301
|
+
"metric_key": self.metric_key,
|
|
302
|
+
"cache_key": self.cache_key,
|
|
303
|
+
"event_topic": self.event_topic,
|
|
304
|
+
"value": self.value,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
def __iter__(self) -> Iterator[StrEnum]:
|
|
308
|
+
yield self.group
|
|
309
|
+
|
|
310
|
+
if self.source is not None:
|
|
311
|
+
yield self.source
|
|
312
|
+
|
|
313
|
+
if self.component is not None:
|
|
314
|
+
yield self.component
|
|
315
|
+
|
|
316
|
+
if self.role is not None:
|
|
317
|
+
yield self.role
|
|
318
|
+
|
|
319
|
+
def __str__(self) -> str:
|
|
320
|
+
return self.value
|
|
321
|
+
|
|
322
|
+
def __repr__(self) -> str:
|
|
323
|
+
return f"Identifier('{self.value}')"
|
|
324
|
+
|
|
325
|
+
def __eq__(self, other: object) -> bool:
|
|
326
|
+
if isinstance(other, Identifier):
|
|
327
|
+
return self.parts == other.parts
|
|
328
|
+
|
|
329
|
+
if isinstance(other, str):
|
|
330
|
+
return self.value == other
|
|
331
|
+
|
|
332
|
+
return NotImplemented
|
|
333
|
+
|
|
334
|
+
def __lt__(self, other: object) -> bool:
|
|
335
|
+
if isinstance(other, Identifier):
|
|
336
|
+
return self.value < other.value
|
|
337
|
+
|
|
338
|
+
if isinstance(other, str):
|
|
339
|
+
return self.value < other
|
|
340
|
+
|
|
341
|
+
return NotImplemented
|
|
342
|
+
|
|
343
|
+
def __hash__(self) -> int:
|
|
344
|
+
return hash(self.parts)
|
|
345
|
+
|
|
346
|
+
@classmethod
|
|
347
|
+
def parse(
|
|
348
|
+
cls,
|
|
349
|
+
value: str,
|
|
350
|
+
*,
|
|
351
|
+
group_enum: type[G],
|
|
352
|
+
source_enum: type[S],
|
|
353
|
+
role_enum: type[R],
|
|
354
|
+
) -> Identifier[G, S, R]:
|
|
355
|
+
if not value:
|
|
356
|
+
raise IdentifierParseError("Identifier cannot be empty.")
|
|
357
|
+
|
|
358
|
+
if "::" not in value:
|
|
359
|
+
try:
|
|
360
|
+
return cls(
|
|
361
|
+
group=group_enum(value),
|
|
362
|
+
source_enum=source_enum,
|
|
363
|
+
role_enum=role_enum,
|
|
364
|
+
)
|
|
365
|
+
except ValueError as exc:
|
|
366
|
+
raise IdentifierParseError(
|
|
367
|
+
f"Invalid identifier '{value}'. Expected format: Group::Source[-Component][+role]."
|
|
368
|
+
) from exc
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
group_part, rest = value.split("::", 1)
|
|
372
|
+
except ValueError as exc:
|
|
373
|
+
raise IdentifierParseError(
|
|
374
|
+
f"Invalid identifier '{value}'. Expected format: Group::Source[-Component][+role]."
|
|
375
|
+
) from exc
|
|
376
|
+
|
|
377
|
+
if not group_part:
|
|
378
|
+
raise IdentifierParseError("Group cannot be empty.")
|
|
379
|
+
|
|
380
|
+
try:
|
|
381
|
+
parsed_group = group_enum(group_part)
|
|
382
|
+
except ValueError as exc:
|
|
383
|
+
raise IdentifierParseError(f"Invalid group '{group_part}'.") from exc
|
|
384
|
+
|
|
385
|
+
if not rest:
|
|
386
|
+
raise IdentifierParseError("Source cannot be empty.")
|
|
387
|
+
|
|
388
|
+
parsed_role: R | None = None
|
|
389
|
+
|
|
390
|
+
if "+" in rest:
|
|
391
|
+
rest, role_part = rest.split("+", 1)
|
|
392
|
+
|
|
393
|
+
if not role_part:
|
|
394
|
+
raise IdentifierParseError("role cannot be empty.")
|
|
395
|
+
|
|
396
|
+
try:
|
|
397
|
+
parsed_role = role_enum(role_part)
|
|
398
|
+
except ValueError as exc:
|
|
399
|
+
raise IdentifierParseError(f"Invalid role '{role_part}'.") from exc
|
|
400
|
+
|
|
401
|
+
if not rest:
|
|
402
|
+
raise IdentifierParseError("Source cannot be empty.")
|
|
403
|
+
|
|
404
|
+
parsed_component: S | None = None
|
|
405
|
+
|
|
406
|
+
if "-" in rest:
|
|
407
|
+
source_part, component_part = rest.split("-", 1)
|
|
408
|
+
|
|
409
|
+
if not source_part:
|
|
410
|
+
raise IdentifierParseError("Source cannot be empty.")
|
|
411
|
+
|
|
412
|
+
if not component_part:
|
|
413
|
+
raise IdentifierParseError("Component cannot be empty.")
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
parsed_source = source_enum(source_part)
|
|
417
|
+
except ValueError as exc:
|
|
418
|
+
raise IdentifierParseError(f"Invalid source '{source_part}'.") from exc
|
|
419
|
+
|
|
420
|
+
try:
|
|
421
|
+
parsed_component = source_enum(component_part)
|
|
422
|
+
except ValueError as exc:
|
|
423
|
+
raise IdentifierParseError(
|
|
424
|
+
f"Invalid component '{component_part}'."
|
|
425
|
+
) from exc
|
|
426
|
+
|
|
427
|
+
else:
|
|
428
|
+
try:
|
|
429
|
+
parsed_source = source_enum(rest)
|
|
430
|
+
except ValueError as exc:
|
|
431
|
+
raise IdentifierParseError(f"Invalid source '{rest}'.") from exc
|
|
432
|
+
|
|
433
|
+
return cls(
|
|
434
|
+
group=parsed_group,
|
|
435
|
+
source=parsed_source,
|
|
436
|
+
component=parsed_component,
|
|
437
|
+
role=parsed_role,
|
|
438
|
+
source_enum=source_enum,
|
|
439
|
+
role_enum=role_enum,
|
|
440
|
+
)
|
idkit/idkit.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .identifier import Identifier
|
|
4
|
+
from .protocols import Identifiable, SupportsIdentifier
|
|
5
|
+
from .root import IdentifierRoot
|
|
6
|
+
from .enums import (
|
|
7
|
+
IdentifierGroup,
|
|
8
|
+
IdentifierOperation,
|
|
9
|
+
IdentifierRole,
|
|
10
|
+
IdentifierSource,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
type IDKitIdentifier = Identifier[IdentifierGroup, IdentifierSource, IdentifierRole]
|
|
15
|
+
|
|
16
|
+
type IDKitOperationIdentifier = Identifier[
|
|
17
|
+
IdentifierGroup, IdentifierSource, IdentifierOperation
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
type IDKitIdentifierRoot = IdentifierRoot[
|
|
21
|
+
IdentifierGroup, IdentifierSource, IdentifierRole
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
type IDKitIdentifierLike = SupportsIdentifier[
|
|
25
|
+
IdentifierGroup, IdentifierSource, IdentifierRole
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
type IDKitIdentifiable = Identifiable[IdentifierGroup, IdentifierSource, IdentifierRole]
|
|
29
|
+
|
|
30
|
+
type AppIdentifier = IDKitIdentifier
|
|
31
|
+
type AppIdentifierRoot = IDKitIdentifierRoot
|
|
32
|
+
type AppIdentifierLike = IDKitIdentifierLike
|
|
33
|
+
type AppIdentifiable = IDKitIdentifiable
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
ID: IDKitIdentifierRoot = IdentifierRoot(
|
|
37
|
+
group=IdentifierGroup,
|
|
38
|
+
source=IdentifierSource,
|
|
39
|
+
role=IdentifierRole,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"ID",
|
|
45
|
+
"AppIdentifier",
|
|
46
|
+
"AppIdentifierRoot",
|
|
47
|
+
"AppIdentifierLike",
|
|
48
|
+
"AppIdentifiable",
|
|
49
|
+
"IDKitIdentifier",
|
|
50
|
+
"IDKitOperationIdentifier",
|
|
51
|
+
"IDKitIdentifierRoot",
|
|
52
|
+
"IDKitIdentifierLike",
|
|
53
|
+
"IDKitIdentifiable",
|
|
54
|
+
"IdentifierGroup",
|
|
55
|
+
"IdentifierSource",
|
|
56
|
+
"IdentifierRole",
|
|
57
|
+
"IdentifierOperation",
|
|
58
|
+
]
|
idkit/protocols.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
from typing import Any, Protocol, Self
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SupportsIdentifier[G: StrEnum, S: StrEnum, R: StrEnum](Protocol):
|
|
8
|
+
"""Structural interface implemented by identifier-like values.
|
|
9
|
+
|
|
10
|
+
Use this protocol when a function only needs the public identifier API and
|
|
11
|
+
should accept either ``Identifier`` instances or compatible implementations.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def group(self) -> G: ...
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def source(self) -> S | None: ...
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def component(self) -> S | None: ...
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def role(self) -> R | None: ...
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def parts(self) -> tuple[G, S | None, S | None, R | None]: ...
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def string_parts(self) -> tuple[str, ...]: ...
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def namespace(self) -> str: ...
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def qualified(self) -> str: ...
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def value(self) -> str: ...
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def path(self) -> str: ...
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def slug(self) -> str: ...
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def metric_key(self) -> str: ...
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def cache_key(self) -> str: ...
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def event_topic(self) -> str: ...
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def has_source(self) -> bool: ...
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def has_component(self) -> bool: ...
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def has_role(self) -> bool: ...
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def has_action(self) -> bool: ...
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_complete(self) -> bool: ...
|
|
70
|
+
|
|
71
|
+
def require_source(self) -> Self: ...
|
|
72
|
+
|
|
73
|
+
def require_role(self) -> Self: ...
|
|
74
|
+
|
|
75
|
+
def require_action(self) -> Self: ...
|
|
76
|
+
|
|
77
|
+
def require_complete(self) -> Self: ...
|
|
78
|
+
|
|
79
|
+
def to_dict(self) -> dict[str, Any]: ...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Identifiable[G: StrEnum, S: StrEnum, R: StrEnum](Protocol):
|
|
83
|
+
"""Structural interface for objects that expose an ``identifier`` property."""
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def identifier(self) -> SupportsIdentifier[G, S, R]: ...
|
idkit/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
idkit/root.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from .identifier import Identifier
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IdentifierRoot[G: StrEnum, S: StrEnum, R: StrEnum]:
|
|
9
|
+
"""Entry point for building and parsing typed identifiers.
|
|
10
|
+
|
|
11
|
+
An ``IdentifierRoot`` binds the enum types that define the valid groups,
|
|
12
|
+
sources/components, and actions for a package. Attribute access on the root
|
|
13
|
+
starts a new identifier from a group name, for example ``ID.system``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
*,
|
|
19
|
+
group: type[G],
|
|
20
|
+
source: type[S],
|
|
21
|
+
role: type[R],
|
|
22
|
+
):
|
|
23
|
+
self.group = group
|
|
24
|
+
self.source = source
|
|
25
|
+
self.role = role
|
|
26
|
+
|
|
27
|
+
def __getattr__(self, name: str) -> Identifier[G, S, R]:
|
|
28
|
+
if name in self.group.__members__:
|
|
29
|
+
return Identifier(
|
|
30
|
+
group=self.group[name],
|
|
31
|
+
source_enum=self.source,
|
|
32
|
+
role_enum=self.role,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
raise AttributeError(name)
|
|
36
|
+
|
|
37
|
+
def parse(self, value: str) -> Identifier[G, S, R]:
|
|
38
|
+
return Identifier.parse(
|
|
39
|
+
value,
|
|
40
|
+
group_enum=self.group,
|
|
41
|
+
source_enum=self.source,
|
|
42
|
+
role_enum=self.role,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
IDRoot = IdentifierRoot
|
|
47
|
+
"""Short alias for `IdentifierRoot`"""
|