Skip to content

Documentation for the Cite2Urn class

urn_citation.Cite2Urn

Bases: Urn

A class representing a CITE2URN, which is a specific type of URN used in the CITE architecture.

Source code in src/urn_citation/cite2urn.py
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
class Cite2Urn(Urn):
    """
    A class representing a CITE2URN, which is a specific type of URN used in the CITE architecture.
    """
    namespace: str
    collection: str
    version: str | None = None
    object_id: str | None = None

    @model_validator(mode='after')
    def validate_subreferences(self):
        """Validate subreferences in object identifier.

        Ensures that:
        - object_id component has at most one @ per range part
        - subreferences are not empty

        Raises:
            ValueError: If the subreference constraints are violated.
        """
        if self.object_id is not None:
            range_parts = self.object_id.split("-")
            for part in range_parts:
                if part.count("@") > 1:
                    raise ValueError(f"Each object component can have at most one @ delimiter for subreference, found {part.count('@')} in '{part}'")
                # Check for empty subreferences
                if "@" in part:
                    subref_parts = part.split("@")
                    if len(subref_parts) != 2 or not subref_parts[1]:
                        raise ValueError(f"Subreference cannot be empty, found empty subreference in '{part}'")

        return self

    @classmethod
    def from_string(cls, raw_string: str) -> "Cite2Urn":
        """Parse a ``urn:cite2`` string into a ``Cite2Urn`` instance.

        The string must be in the form ``urn:cite2:<namespace>:<collection[.version]>:<object[-range]>``.
        """
        if not raw_string.startswith("urn:cite2:"):
            raise ValueError("CITE2 URN must start with 'urn:cite2:'")

        parts = raw_string.split(":")
        if len(parts) != 5:
            raise ValueError(
                f"CITE2 URN must have 5 colon-delimited parts, got {len(parts)} from {raw_string}."
            )

        header, urn_type, namespace, collection_info, object_info = parts

        if header != "urn":
            raise ValueError("CITE2 URN must start with 'urn'")
        if urn_type != "cite2":
            raise ValueError("CITE2 URN must include the cite2 type identifier")

        if not namespace:
            raise ValueError("Namespace component cannot be empty")
        if not collection_info:
            raise ValueError("Collection info component cannot be empty")
        if not object_info:
            raise ValueError("Object component cannot be empty")

        if collection_info.endswith("."):
            raise ValueError("Collection info cannot end with a period")
        collection_parts = collection_info.split(".")
        if len(collection_parts) > 2:
            raise ValueError("Collection info can contain at most one period to separate collection and version")
        if any(part == "" for part in collection_parts):
            raise ValueError("Collection info must contain non-empty collection/version values")

        collection = collection_parts[0]
        version = collection_parts[1] if len(collection_parts) == 2 else None

        if object_info.endswith("-"):
            raise ValueError("Object component cannot end with a hyphen")
        object_parts = object_info.split("-")
        if len(object_parts) > 2:
            raise ValueError("Object component can contain at most one hyphen to indicate a range")
        if any(part == "" for part in object_parts):
            raise ValueError("Object component must contain non-empty identifiers")

        # Validate subreferences (at most one @ per range part, no empty subreferences)
        for part in object_parts:
            if part.count("@") > 1:
                raise ValueError(f"Each object component can have at most one @ delimiter for subreference, found {part.count('@')} in '{part}'")
            # Check for empty subreferences
            if "@" in part:
                subref_parts = part.split("@")
                if len(subref_parts) != 2 or not subref_parts[1]:
                    raise ValueError(f"Subreference cannot be empty, found empty subreference in '{part}'")

        object_id = object_info

        return cls(
            urn_type=urn_type,
            namespace=namespace,
            collection=collection,
            version=version,
            object_id=object_id,
        )

    def __str__(self) -> str:
        """Serialize the Cite2Urn to its canonical string form."""
        collection_part = self.collection
        if self.version is not None:
            collection_part = f"{collection_part}.{self.version}"

        object_part = self.object_id or ""

        return f"urn:{self.urn_type}:{self.namespace}:{collection_part}:{object_part}"

    def is_range(self) -> bool:
        """Return True when the object component encodes a range (single hyphen)."""
        if self.object_id is None:
            return False

        range_parts = self.object_id.split("-")
        return len(range_parts) == 2

    def range_begin(self) -> str | None:
        """Return the first identifier when the object component is a range."""
        if not self.is_range():
            return None
        return self.object_id.split("-")[0]

    def range_end(self) -> str | None:
        """Return the second identifier when the object component is a range."""
        if not self.is_range():
            return None
        return self.object_id.split("-")[1]

    def has_subreference(self) -> bool:
        """Check if the object identifier has a subreference.

        An object identifier has a subreference if it contains at least one @ character,
        which may appear on either or both parts of a range reference, or on
        a single reference.

        Returns:
            bool: True if the object identifier contains a subreference (@ character), False otherwise.
        """
        if self.object_id is None:
            return False

        return "@" in self.object_id

    def has_subreference1(self) -> bool:
        """Check if the range begin part has a subreference.

        Returns True if the URN is a range and the range begin part contains
        a @ character indicating a subreference.

        Returns:
            bool: True if the range begin part has a subreference, False otherwise.

        Raises:
            ValueError: If the URN is not a range.
        """
        if not self.is_range():
            raise ValueError("has_subreference1 can only be called on range URNs")

        range_begin = self.range_begin()
        return "@" in range_begin if range_begin else False

    def has_subreference2(self) -> bool:
        """Check if the range end part has a subreference.

        Returns True if the URN is a range and the range end part contains
        a @ character indicating a subreference.

        Returns:
            bool: True if the range end part has a subreference, False otherwise.

        Raises:
            ValueError: If the URN is not a range.
        """
        if not self.is_range():
            raise ValueError("has_subreference2 can only be called on range URNs")

        range_end = self.range_end()
        return "@" in range_end if range_end else False

    def subreference(self) -> str | None:
        """Get the subreference part of an object identifier.

        Returns the subreference part (the text after @) if the object identifier has a subreference.
        Returns None if the object identifier has no subreference.

        Returns:
            str | None: The subreference part, or None if no subreference exists.

        Raises:
            ValueError: If the URN is a range reference.
        """
        if self.is_range():
            raise ValueError("subreference can only be called on non-range URNs")

        if self.object_id is None or "@" not in self.object_id:
            return None

        parts = self.object_id.split("@")
        return parts[1]

    def subreference1(self) -> str | None:
        """Get the subreference part of the range begin reference.

        Returns the subreference part (the text after @) of the range begin part
        if it has a subreference. Returns None if the range begin part has no subreference.

        Returns:
            str | None: The subreference part of the range begin, or None if no subreference exists.

        Raises:
            ValueError: If the URN is not a range reference.
        """
        if not self.is_range():
            raise ValueError("subreference1 can only be called on range URNs")

        range_begin = self.range_begin()
        if range_begin is None or "@" not in range_begin:
            return None

        parts = range_begin.split("@")
        return parts[1]

    def subreference2(self) -> str | None:
        """Get the subreference part of the range end reference.

        Returns the subreference part (the text after @) of the range end part
        if it has a subreference. Returns None if the range end part has no subreference.

        Returns:
            str | None: The subreference part of the range end, or None if no subreference exists.

        Raises:
            ValueError: If the URN is not a range reference.
        """
        if not self.is_range():
            raise ValueError("subreference2 can only be called on range URNs")

        range_end = self.range_end()
        if range_end is None or "@" not in range_end:
            return None

        parts = range_end.split("@")
        return parts[1]

    @classmethod
    def valid_string(cls, raw_string: str) -> bool:
        """Return True when the string can be parsed into a Cite2Urn."""
        try:
            if not raw_string.startswith("urn:cite2:"):
                return False

            parts = raw_string.split(":")
            if len(parts) != 5:
                return False

            header, urn_type, namespace, collection_info, object_info = parts

            if header != "urn" or urn_type != "cite2":
                return False
            if not namespace:
                return False
            if not collection_info or not object_info:
                return False

            # Collection rules: at most one period, not ending with a period, non-empty segments
            if collection_info.endswith("."):
                return False
            collection_parts = collection_info.split(".")
            if len(collection_parts) > 2:
                return False
            if any(part == "" for part in collection_parts):
                return False

            # Object rules: at most one hyphen, not ending with hyphen, non-empty segments
            if object_info.endswith("-"):
                return False
            object_parts = object_info.split("-")
            if len(object_parts) > 2:
                return False
            if any(part == "" for part in object_parts):
                return False

            return True
        except Exception:
            return False
    def collection_equals(self, other: "Cite2Urn") -> bool:
        """Check if the collection hierarchy equals another Cite2Urn.

        Compares the namespace, collection, and version fields.

        Args:
            other (Cite2Urn): The Cite2Urn to compare with.

        Returns:
            bool: True if all collection hierarchy fields are equal, False otherwise.
        """
        return (
            self.namespace == other.namespace
            and self.collection == other.collection
            and self.version == other.version
        )

    def collection_contains(self, other: "Cite2Urn") -> bool:
        """Check if the collection hierarchy contains another Cite2Urn.

        Returns True if all non-None values of namespace, collection, and version
        in this Cite2Urn equal the corresponding values in the other Cite2Urn.

        Args:
            other (Cite2Urn): The Cite2Urn to compare with.

        Returns:
            bool: True if all non-None collection hierarchy fields match, False otherwise.
        """
        if self.namespace is not None and self.namespace != other.namespace:
            return False
        if self.collection is not None and self.collection != other.collection:
            return False
        if self.version is not None and self.version != other.version:
            return False
        return True

    def object_equals(self, other: "Cite2Urn") -> bool:
        """Check if the object identifier equals another Cite2Urn.

        Compares the object_id field of this Cite2Urn with the object_id field of another.

        Args:
            other (Cite2Urn): The Cite2Urn to compare with.

        Returns:
            bool: True if the object_id fields are equal, False otherwise.
        """
        return self.object_id == other.object_id

    def contains(self, other: Cite2Urn) -> bool:
        """Check if this Cite2Urn contains another Cite2Urn.

        Returns True if the collection hierarchy contains the other's collection hierarchy
        AND the object identifiers are exactly equal.

        Args:
            other (Cite2Urn): The Cite2Urn to compare with.

        Returns:
            bool: True if collection_contains and object_equals are both True, False otherwise.
        """
        return self.collection_contains(other) and self.object_equals(other)

    def drop_version(self) -> "Cite2Urn":
        """Create a new Cite2Urn without the version component.

        Returns a new Cite2Urn instance with the same collection and object
        but with the version set to None.

        Returns:
            Cite2Urn: A new Cite2Urn instance without the version component.
        """
        return Cite2Urn(
            urn_type=self.urn_type,
            namespace=self.namespace,
            collection=self.collection,
            version=None,
            object_id=self.object_id,
        )

    def drop_objectid(self) -> "Cite2Urn":
        """Create a new Cite2Urn without the object_id component.

        Returns a new Cite2Urn instance with the same collection hierarchy
        but with the object_id set to None.

        Returns:
            Cite2Urn: A new Cite2Urn instance without the object_id component.
        """
        return Cite2Urn(
            urn_type=self.urn_type,
            namespace=self.namespace,
            collection=self.collection,
            version=self.version,
            object_id=None,
        )

    def drop_subreference(self) -> "Cite2Urn":
        """Create a new Cite2Urn with all subreferences removed.

        Returns a new Cite2Urn instance with subreferences (text after @) removed
        from the object_id component. Works on both single objects and ranges.
        If there are no subreferences, returns a new instance with the same object_id.

        Returns:
            Cite2Urn: A new Cite2Urn instance without subreferences in the object_id.
        """
        if self.object_id is None or "@" not in self.object_id:
            # No subreference to drop, return copy with same object_id
            return Cite2Urn(
                urn_type=self.urn_type,
                namespace=self.namespace,
                collection=self.collection,
                version=self.version,
                object_id=self.object_id,
            )

        # Remove subreferences from object_id
        range_parts = self.object_id.split("-")
        cleaned_parts = []
        for part in range_parts:
            if "@" in part:
                # Keep only the part before @
                cleaned_parts.append(part.split("@")[0])
            else:
                cleaned_parts.append(part)

        new_object_id = "-".join(cleaned_parts)

        return Cite2Urn(
            urn_type=self.urn_type,
            namespace=self.namespace,
            collection=self.collection,
            version=self.version,
            object_id=new_object_id,
        )

__str__()

Serialize the Cite2Urn to its canonical string form.

Source code in src/urn_citation/cite2urn.py
111
112
113
114
115
116
117
118
119
def __str__(self) -> str:
    """Serialize the Cite2Urn to its canonical string form."""
    collection_part = self.collection
    if self.version is not None:
        collection_part = f"{collection_part}.{self.version}"

    object_part = self.object_id or ""

    return f"urn:{self.urn_type}:{self.namespace}:{collection_part}:{object_part}"

collection_contains(other)

Check if the collection hierarchy contains another Cite2Urn.

Returns True if all non-None values of namespace, collection, and version in this Cite2Urn equal the corresponding values in the other Cite2Urn.

Parameters:

Name Type Description Default
other Cite2Urn

The Cite2Urn to compare with.

required

Returns:

Name Type Description
bool bool

True if all non-None collection hierarchy fields match, False otherwise.

Source code in src/urn_citation/cite2urn.py
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
def collection_contains(self, other: "Cite2Urn") -> bool:
    """Check if the collection hierarchy contains another Cite2Urn.

    Returns True if all non-None values of namespace, collection, and version
    in this Cite2Urn equal the corresponding values in the other Cite2Urn.

    Args:
        other (Cite2Urn): The Cite2Urn to compare with.

    Returns:
        bool: True if all non-None collection hierarchy fields match, False otherwise.
    """
    if self.namespace is not None and self.namespace != other.namespace:
        return False
    if self.collection is not None and self.collection != other.collection:
        return False
    if self.version is not None and self.version != other.version:
        return False
    return True

collection_equals(other)

Check if the collection hierarchy equals another Cite2Urn.

Compares the namespace, collection, and version fields.

Parameters:

Name Type Description Default
other Cite2Urn

The Cite2Urn to compare with.

required

Returns:

Name Type Description
bool bool

True if all collection hierarchy fields are equal, False otherwise.

Source code in src/urn_citation/cite2urn.py
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def collection_equals(self, other: "Cite2Urn") -> bool:
    """Check if the collection hierarchy equals another Cite2Urn.

    Compares the namespace, collection, and version fields.

    Args:
        other (Cite2Urn): The Cite2Urn to compare with.

    Returns:
        bool: True if all collection hierarchy fields are equal, False otherwise.
    """
    return (
        self.namespace == other.namespace
        and self.collection == other.collection
        and self.version == other.version
    )

contains(other)

Check if this Cite2Urn contains another Cite2Urn.

Returns True if the collection hierarchy contains the other's collection hierarchy AND the object identifiers are exactly equal.

Parameters:

Name Type Description Default
other Cite2Urn

The Cite2Urn to compare with.

required

Returns:

Name Type Description
bool bool

True if collection_contains and object_equals are both True, False otherwise.

Source code in src/urn_citation/cite2urn.py
348
349
350
351
352
353
354
355
356
357
358
359
360
def contains(self, other: Cite2Urn) -> bool:
    """Check if this Cite2Urn contains another Cite2Urn.

    Returns True if the collection hierarchy contains the other's collection hierarchy
    AND the object identifiers are exactly equal.

    Args:
        other (Cite2Urn): The Cite2Urn to compare with.

    Returns:
        bool: True if collection_contains and object_equals are both True, False otherwise.
    """
    return self.collection_contains(other) and self.object_equals(other)

drop_objectid()

Create a new Cite2Urn without the object_id component.

Returns a new Cite2Urn instance with the same collection hierarchy but with the object_id set to None.

Returns:

Name Type Description
Cite2Urn 'Cite2Urn'

A new Cite2Urn instance without the object_id component.

Source code in src/urn_citation/cite2urn.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def drop_objectid(self) -> "Cite2Urn":
    """Create a new Cite2Urn without the object_id component.

    Returns a new Cite2Urn instance with the same collection hierarchy
    but with the object_id set to None.

    Returns:
        Cite2Urn: A new Cite2Urn instance without the object_id component.
    """
    return Cite2Urn(
        urn_type=self.urn_type,
        namespace=self.namespace,
        collection=self.collection,
        version=self.version,
        object_id=None,
    )

drop_subreference()

Create a new Cite2Urn with all subreferences removed.

Returns a new Cite2Urn instance with subreferences (text after @) removed from the object_id component. Works on both single objects and ranges. If there are no subreferences, returns a new instance with the same object_id.

Returns:

Name Type Description
Cite2Urn 'Cite2Urn'

A new Cite2Urn instance without subreferences in the object_id.

Source code in src/urn_citation/cite2urn.py
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def drop_subreference(self) -> "Cite2Urn":
    """Create a new Cite2Urn with all subreferences removed.

    Returns a new Cite2Urn instance with subreferences (text after @) removed
    from the object_id component. Works on both single objects and ranges.
    If there are no subreferences, returns a new instance with the same object_id.

    Returns:
        Cite2Urn: A new Cite2Urn instance without subreferences in the object_id.
    """
    if self.object_id is None or "@" not in self.object_id:
        # No subreference to drop, return copy with same object_id
        return Cite2Urn(
            urn_type=self.urn_type,
            namespace=self.namespace,
            collection=self.collection,
            version=self.version,
            object_id=self.object_id,
        )

    # Remove subreferences from object_id
    range_parts = self.object_id.split("-")
    cleaned_parts = []
    for part in range_parts:
        if "@" in part:
            # Keep only the part before @
            cleaned_parts.append(part.split("@")[0])
        else:
            cleaned_parts.append(part)

    new_object_id = "-".join(cleaned_parts)

    return Cite2Urn(
        urn_type=self.urn_type,
        namespace=self.namespace,
        collection=self.collection,
        version=self.version,
        object_id=new_object_id,
    )

drop_version()

Create a new Cite2Urn without the version component.

Returns a new Cite2Urn instance with the same collection and object but with the version set to None.

Returns:

Name Type Description
Cite2Urn 'Cite2Urn'

A new Cite2Urn instance without the version component.

Source code in src/urn_citation/cite2urn.py
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
def drop_version(self) -> "Cite2Urn":
    """Create a new Cite2Urn without the version component.

    Returns a new Cite2Urn instance with the same collection and object
    but with the version set to None.

    Returns:
        Cite2Urn: A new Cite2Urn instance without the version component.
    """
    return Cite2Urn(
        urn_type=self.urn_type,
        namespace=self.namespace,
        collection=self.collection,
        version=None,
        object_id=self.object_id,
    )

from_string(raw_string) classmethod

Parse a urn:cite2 string into a Cite2Urn instance.

The string must be in the form urn:cite2:<namespace>:<collection[.version]>:<object[-range]>.

Source code in src/urn_citation/cite2urn.py
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
@classmethod
def from_string(cls, raw_string: str) -> "Cite2Urn":
    """Parse a ``urn:cite2`` string into a ``Cite2Urn`` instance.

    The string must be in the form ``urn:cite2:<namespace>:<collection[.version]>:<object[-range]>``.
    """
    if not raw_string.startswith("urn:cite2:"):
        raise ValueError("CITE2 URN must start with 'urn:cite2:'")

    parts = raw_string.split(":")
    if len(parts) != 5:
        raise ValueError(
            f"CITE2 URN must have 5 colon-delimited parts, got {len(parts)} from {raw_string}."
        )

    header, urn_type, namespace, collection_info, object_info = parts

    if header != "urn":
        raise ValueError("CITE2 URN must start with 'urn'")
    if urn_type != "cite2":
        raise ValueError("CITE2 URN must include the cite2 type identifier")

    if not namespace:
        raise ValueError("Namespace component cannot be empty")
    if not collection_info:
        raise ValueError("Collection info component cannot be empty")
    if not object_info:
        raise ValueError("Object component cannot be empty")

    if collection_info.endswith("."):
        raise ValueError("Collection info cannot end with a period")
    collection_parts = collection_info.split(".")
    if len(collection_parts) > 2:
        raise ValueError("Collection info can contain at most one period to separate collection and version")
    if any(part == "" for part in collection_parts):
        raise ValueError("Collection info must contain non-empty collection/version values")

    collection = collection_parts[0]
    version = collection_parts[1] if len(collection_parts) == 2 else None

    if object_info.endswith("-"):
        raise ValueError("Object component cannot end with a hyphen")
    object_parts = object_info.split("-")
    if len(object_parts) > 2:
        raise ValueError("Object component can contain at most one hyphen to indicate a range")
    if any(part == "" for part in object_parts):
        raise ValueError("Object component must contain non-empty identifiers")

    # Validate subreferences (at most one @ per range part, no empty subreferences)
    for part in object_parts:
        if part.count("@") > 1:
            raise ValueError(f"Each object component can have at most one @ delimiter for subreference, found {part.count('@')} in '{part}'")
        # Check for empty subreferences
        if "@" in part:
            subref_parts = part.split("@")
            if len(subref_parts) != 2 or not subref_parts[1]:
                raise ValueError(f"Subreference cannot be empty, found empty subreference in '{part}'")

    object_id = object_info

    return cls(
        urn_type=urn_type,
        namespace=namespace,
        collection=collection,
        version=version,
        object_id=object_id,
    )

has_subreference()

Check if the object identifier has a subreference.

An object identifier has a subreference if it contains at least one @ character, which may appear on either or both parts of a range reference, or on a single reference.

Returns:

Name Type Description
bool bool

True if the object identifier contains a subreference (@ character), False otherwise.

Source code in src/urn_citation/cite2urn.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def has_subreference(self) -> bool:
    """Check if the object identifier has a subreference.

    An object identifier has a subreference if it contains at least one @ character,
    which may appear on either or both parts of a range reference, or on
    a single reference.

    Returns:
        bool: True if the object identifier contains a subreference (@ character), False otherwise.
    """
    if self.object_id is None:
        return False

    return "@" in self.object_id

has_subreference1()

Check if the range begin part has a subreference.

Returns True if the URN is a range and the range begin part contains a @ character indicating a subreference.

Returns:

Name Type Description
bool bool

True if the range begin part has a subreference, False otherwise.

Raises:

Type Description
ValueError

If the URN is not a range.

Source code in src/urn_citation/cite2urn.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
def has_subreference1(self) -> bool:
    """Check if the range begin part has a subreference.

    Returns True if the URN is a range and the range begin part contains
    a @ character indicating a subreference.

    Returns:
        bool: True if the range begin part has a subreference, False otherwise.

    Raises:
        ValueError: If the URN is not a range.
    """
    if not self.is_range():
        raise ValueError("has_subreference1 can only be called on range URNs")

    range_begin = self.range_begin()
    return "@" in range_begin if range_begin else False

has_subreference2()

Check if the range end part has a subreference.

Returns True if the URN is a range and the range end part contains a @ character indicating a subreference.

Returns:

Name Type Description
bool bool

True if the range end part has a subreference, False otherwise.

Raises:

Type Description
ValueError

If the URN is not a range.

Source code in src/urn_citation/cite2urn.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def has_subreference2(self) -> bool:
    """Check if the range end part has a subreference.

    Returns True if the URN is a range and the range end part contains
    a @ character indicating a subreference.

    Returns:
        bool: True if the range end part has a subreference, False otherwise.

    Raises:
        ValueError: If the URN is not a range.
    """
    if not self.is_range():
        raise ValueError("has_subreference2 can only be called on range URNs")

    range_end = self.range_end()
    return "@" in range_end if range_end else False

is_range()

Return True when the object component encodes a range (single hyphen).

Source code in src/urn_citation/cite2urn.py
121
122
123
124
125
126
127
def is_range(self) -> bool:
    """Return True when the object component encodes a range (single hyphen)."""
    if self.object_id is None:
        return False

    range_parts = self.object_id.split("-")
    return len(range_parts) == 2

object_equals(other)

Check if the object identifier equals another Cite2Urn.

Compares the object_id field of this Cite2Urn with the object_id field of another.

Parameters:

Name Type Description Default
other Cite2Urn

The Cite2Urn to compare with.

required

Returns:

Name Type Description
bool bool

True if the object_id fields are equal, False otherwise.

Source code in src/urn_citation/cite2urn.py
335
336
337
338
339
340
341
342
343
344
345
346
def object_equals(self, other: "Cite2Urn") -> bool:
    """Check if the object identifier equals another Cite2Urn.

    Compares the object_id field of this Cite2Urn with the object_id field of another.

    Args:
        other (Cite2Urn): The Cite2Urn to compare with.

    Returns:
        bool: True if the object_id fields are equal, False otherwise.
    """
    return self.object_id == other.object_id

range_begin()

Return the first identifier when the object component is a range.

Source code in src/urn_citation/cite2urn.py
129
130
131
132
133
def range_begin(self) -> str | None:
    """Return the first identifier when the object component is a range."""
    if not self.is_range():
        return None
    return self.object_id.split("-")[0]

range_end()

Return the second identifier when the object component is a range.

Source code in src/urn_citation/cite2urn.py
135
136
137
138
139
def range_end(self) -> str | None:
    """Return the second identifier when the object component is a range."""
    if not self.is_range():
        return None
    return self.object_id.split("-")[1]

subreference()

Get the subreference part of an object identifier.

Returns the subreference part (the text after @) if the object identifier has a subreference. Returns None if the object identifier has no subreference.

Returns:

Type Description
str | None

str | None: The subreference part, or None if no subreference exists.

Raises:

Type Description
ValueError

If the URN is a range reference.

Source code in src/urn_citation/cite2urn.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def subreference(self) -> str | None:
    """Get the subreference part of an object identifier.

    Returns the subreference part (the text after @) if the object identifier has a subreference.
    Returns None if the object identifier has no subreference.

    Returns:
        str | None: The subreference part, or None if no subreference exists.

    Raises:
        ValueError: If the URN is a range reference.
    """
    if self.is_range():
        raise ValueError("subreference can only be called on non-range URNs")

    if self.object_id is None or "@" not in self.object_id:
        return None

    parts = self.object_id.split("@")
    return parts[1]

subreference1()

Get the subreference part of the range begin reference.

Returns the subreference part (the text after @) of the range begin part if it has a subreference. Returns None if the range begin part has no subreference.

Returns:

Type Description
str | None

str | None: The subreference part of the range begin, or None if no subreference exists.

Raises:

Type Description
ValueError

If the URN is not a range reference.

Source code in src/urn_citation/cite2urn.py
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def subreference1(self) -> str | None:
    """Get the subreference part of the range begin reference.

    Returns the subreference part (the text after @) of the range begin part
    if it has a subreference. Returns None if the range begin part has no subreference.

    Returns:
        str | None: The subreference part of the range begin, or None if no subreference exists.

    Raises:
        ValueError: If the URN is not a range reference.
    """
    if not self.is_range():
        raise ValueError("subreference1 can only be called on range URNs")

    range_begin = self.range_begin()
    if range_begin is None or "@" not in range_begin:
        return None

    parts = range_begin.split("@")
    return parts[1]

subreference2()

Get the subreference part of the range end reference.

Returns the subreference part (the text after @) of the range end part if it has a subreference. Returns None if the range end part has no subreference.

Returns:

Type Description
str | None

str | None: The subreference part of the range end, or None if no subreference exists.

Raises:

Type Description
ValueError

If the URN is not a range reference.

Source code in src/urn_citation/cite2urn.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def subreference2(self) -> str | None:
    """Get the subreference part of the range end reference.

    Returns the subreference part (the text after @) of the range end part
    if it has a subreference. Returns None if the range end part has no subreference.

    Returns:
        str | None: The subreference part of the range end, or None if no subreference exists.

    Raises:
        ValueError: If the URN is not a range reference.
    """
    if not self.is_range():
        raise ValueError("subreference2 can only be called on range URNs")

    range_end = self.range_end()
    if range_end is None or "@" not in range_end:
        return None

    parts = range_end.split("@")
    return parts[1]

valid_string(raw_string) classmethod

Return True when the string can be parsed into a Cite2Urn.

Source code in src/urn_citation/cite2urn.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
@classmethod
def valid_string(cls, raw_string: str) -> bool:
    """Return True when the string can be parsed into a Cite2Urn."""
    try:
        if not raw_string.startswith("urn:cite2:"):
            return False

        parts = raw_string.split(":")
        if len(parts) != 5:
            return False

        header, urn_type, namespace, collection_info, object_info = parts

        if header != "urn" or urn_type != "cite2":
            return False
        if not namespace:
            return False
        if not collection_info or not object_info:
            return False

        # Collection rules: at most one period, not ending with a period, non-empty segments
        if collection_info.endswith("."):
            return False
        collection_parts = collection_info.split(".")
        if len(collection_parts) > 2:
            return False
        if any(part == "" for part in collection_parts):
            return False

        # Object rules: at most one hyphen, not ending with hyphen, non-empty segments
        if object_info.endswith("-"):
            return False
        object_parts = object_info.split("-")
        if len(object_parts) > 2:
            return False
        if any(part == "" for part in object_parts):
            return False

        return True
    except Exception:
        return False

validate_subreferences()

Validate subreferences in object identifier.

Ensures that: - object_id component has at most one @ per range part - subreferences are not empty

Raises:

Type Description
ValueError

If the subreference constraints are violated.

Source code in src/urn_citation/cite2urn.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@model_validator(mode='after')
def validate_subreferences(self):
    """Validate subreferences in object identifier.

    Ensures that:
    - object_id component has at most one @ per range part
    - subreferences are not empty

    Raises:
        ValueError: If the subreference constraints are violated.
    """
    if self.object_id is not None:
        range_parts = self.object_id.split("-")
        for part in range_parts:
            if part.count("@") > 1:
                raise ValueError(f"Each object component can have at most one @ delimiter for subreference, found {part.count('@')} in '{part}'")
            # Check for empty subreferences
            if "@" in part:
                subref_parts = part.split("@")
                if len(subref_parts) != 2 or not subref_parts[1]:
                    raise ValueError(f"Subreference cannot be empty, found empty subreference in '{part}'")

    return self