Skip to content

core

TierGuiProtocol

Bases: Protocol

Protocol for providing a gui for tiers.

Must provide a header method and take the tier to provide a gui for as the first argument.

Source code in cassini/core.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class TierGuiProtocol(Protocol):
    """
    Protocol for providing a gui for tiers.

    Must provide a header method and take the tier to provide a gui for as the first argument.
    """

    def __init__(self, tier: TierABC):
        pass

    def header(self):
        """
        The header is the UI that is goes at the top of a notebook.
        """
        pass

header

header()

The header is the UI that is goes at the top of a notebook.

Source code in cassini/core.py
56
57
58
59
60
def header(self):
    """
    The header is the UI that is goes at the top of a notebook.
    """
    pass

TierABC

Bases: ABC

Abstract Base class for creating Tiers objects. Tiers should correspond to a folder on your disk.

Instances of this class or subclasses should not be created directly. Instead a Project instance should create them.

Parameters:

Name Type Description Default
*identifiers Tuple[str]

Sequence of strings that identify this tier. With the final identifier being unique.

()
project Project

The project this tier is associated with. This is implicily passed to Tiers if they are accessed via a project instance.

required

Attributes:

Name Type Description
id_regex str

(class attribute) regex used to restrict form of Tier object ids. Should contain 1 group that captures the id. See Project.parse_name for more details.

gui_cls TierGuiProtocol

(class attribute) The class called upon initialisation to use as gui for this object. Constructor should take self as first argument.

pretty_type str

Long name for the tier type, defaults to the class name. Used in dialogues/ ui.

short_type str

Short name for the tier type, defaults to lowercase of the type, with no vowels! Used in notebooks.

name_part_template str

Python template that's filled in with self.id to create segment of the Tier object's name.

name_part_regex str

Regex where first group matches id part of string. Default is fill in cls.name_part_template with cls.id_regex.

Source code in cassini/core.py
 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
class TierABC(ABC):
    """
    Abstract Base class for creating Tiers objects. Tiers should correspond to a folder on your disk.

    Instances of this class or subclasses should not be created directly. Instead a Project instance
    should create them.

    Parameters
    ----------
    *identifiers : Tuple[str]
        Sequence of strings that identify this tier. With the final identifier being unique.
    project : Project
        The project this tier is associated with. This is implicily passed to `Tier`s if they are accessed via a `project` instance.

    Attributes
    ----------
    id_regex : str
        (class attribute) regex used to restrict form of `Tier` object ids. Should contain 1 group that captures the id.
        See [Project.parse_name][cassini.core.Project] for more details.
    gui_cls : TierGuiProtocol
        (class attribute) The class called upon initialisation to use as gui for this object. Constructor should take ``self``
        as first argument.
    pretty_type : str
        Long name for the tier type, defaults to the class name. Used in dialogues/ ui.
    short_type : str
        Short name for the tier type, defaults to lowercase of the type, with no vowels! Used in notebooks.
    name_part_template : str
        Python template that's filled in with ``self.id`` to create segment of the ``Tier`` object's name.
    name_part_regex : str
        Regex where first group matches ``id`` part of string. Default is fill in ``cls.name_part_template`` with
        ``cls.id_regex``.

    """

    _cache: ClassVar[Dict[Tuple[str, ...], TierABC]] = env.create_cache()

    def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None:
        super().__init_subclass__(*args, **kwargs)
        cls._cache = env.create_cache()  # ensures each TierBase class has its own cache

    id_regex: ClassVar[str] = r"(\d+)"

    gui_cls: Type[TierGuiProtocol]

    @cached_class_prop
    @abstractmethod
    def _pretty_type(cls) -> str:
        """
        Name used to display this Tier. Must be explicity set.
        """
        raise AttributeError("This class attribute should be explicity set")

    pretty_type: str = _pretty_type  # to please type checker.

    @cached_class_prop
    def _short_type(cls) -> str:
        """
        Name used to programmatically refer to instances of this `Tier`.

        Default, take pretty type, make lowercase and remove vowels.
        """
        return cls.pretty_type.lower().translate(str.maketrans(dict.fromkeys("aeiou")))

    short_type: str = _short_type

    @cached_class_prop
    def _name_part_template(cls) -> str:
        """
        Python template that's filled in with `self.id` to create segment of the `Tier` object's name.
        """
        return cls.pretty_type + "{}"

    name_part_template = _name_part_template

    @cached_class_prop
    def _name_part_regex(cls) -> str:
        """
        Regex where first group matches `id` part of string. Default is fill in `cls.name_part_template` with
        `cls.id_regex`.
        """
        return cls.name_part_template.format(cls.id_regex)

    name_part_regex = _name_part_regex

    @classmethod
    @abstractmethod
    def iter_siblings(cls, parent: TierABC) -> Iterator[TierABC]:
        """
        Provide an iterator for the siblings of this tier for a given parent i.e.
        iterate over parent's children.
        """
        pass

    def __new__(cls, *args: str, **kwargs: Dict[str, Any]) -> TierABC:
        obj = cls._cache.get(args)
        if obj:
            return obj
        obj = object.__new__(cls)
        cls._cache[args] = obj
        return obj

    _identifiers: Tuple[str, ...]
    gui: TierGuiProtocol

    def __init__(self: Self, *identifiers: str, project: Project):
        self.project = project

        self._identifiers = tuple(filter(None, identifiers))
        self.gui = self.gui_cls(self)

        rank = self.project.rank_map[self.__class__]

        if len(self._identifiers) != rank:
            raise ValueError(
                f"Invalid number of identifiers in {self._identifiers}, expecting {rank}."
            )

        if self.parse_name(self.name) != self.identifiers:
            raise ValueError(
                f"Invalid identifiers - {self._identifiers}, resulting name ('{self.name}') not in a parsable form "
            )

    @cached_prop
    def _parent_cls(self) -> Union[Type[TierABC], None]:
        """
        `Tier` above this `Tier`, `None` if doesn't have one
        """
        return self.project.get_parent_cls(self.__class__)

    parent_cls = _parent_cls

    @cached_prop
    def _child_cls(self) -> Union[Type[TierABC], None]:
        """
        `Tier` below this `Tier`, `None` if doesn't have one
        """
        return self.project.get_child_cls(self.__class__)

    child_cls = _child_cls

    def parse_name(self, name: str) -> Tuple[str, ...]:
        """
        Ask `env.project` to parse name.
        """
        return self.project.parse_name(name)

    @abstractmethod
    def setup_files(
        self, template: Union[Path, None] = None, meta: Optional[MetaDict] = None
    ) -> None:
        """
        Create all the files needed for a valid `Tier` object to exist.

        These parameters are ignored if not used.

        Parameters
        ----------
        template : Path
            path to template file to render to create `.ipynb` file.
        meta : MetaDict
            Initial meta values to create the tier with.
        """
        pass

    @cached_prop
    def identifiers(self) -> Tuple[str, ...]:
        """
        Read only copy of identifiers that make up this `Tier` object.
        """
        return self._identifiers

    @cached_prop
    def name(self) -> str:
        """
        Full name of `Tier` object. Made by concatenating each parent's `self.name_part_template` filled with each parent's
        `self.id`.

        Examples
        --------

            >>> wp = WorkPackage('2')
            >>> exp = Experiment('2', '3')
            >>> smpl = Sample('2', '3', 'c')
            >>> wp.name_part_template.format(wp.id)
            WP2
            >>> exp.name_part_template.format(exp.id)
            .3
            >>> smpl.name_part_template.format(smpl.id)
            c
            >>> smpl.name  # all 3 joined together
            WP2.3c
        """
        return "".join(
            cls.name_part_template.format(id)
            for cls, id in zip(self.project.hierarchy[1:], self.identifiers)
        )

    @property
    @abstractmethod
    def folder(self) -> Path:
        """
        The folder this tier corresponds to.
        """
        pass

    def open_folder(self) -> None:
        """
        Open `self.folder` in explorer

        Notes
        -----
        Window is opened via the Jupyter server, not the browser, so if you are not accessing jupyter on localhost then
        the window won't open for you!
        """
        open_file(self.folder)

    @cached_prop
    def id(self) -> str:
        """
        Shortcut for getting final identifier.

        Examples
        --------

            >>> from my_project import project
            >>> smpl = project.env('WP2.3c')
            >>> smpl.identifiers
            ['2', '3', 'c']
            >>> smpl.id
            'c'
        """
        return self._identifiers[-1]

    @cached_prop
    def parent(self) -> Union[TierABC, None]:
        """
        Parent of this ``Tier`` instance, ``None`` if has no parent.
        """
        if self.parent_cls:
            return self.parent_cls(*self._identifiers[:-1], project=self.project)
        return None

    @cached_prop
    @abstractmethod
    def href(self) -> Union[str, None]:
        """
        href usable in notebook HTML giving link to ``self.file``.

        Assumes that ``os.getcwd()`` reflects the directory of the currently opened ``.ipynb`` (usually true, unless you're
        changing working dir).

        Does do escaping on the HTML, but is maybe pointless!

        Returns
        -------
        href : str
            href usable in notebook HTML.
        """
        pass

    @abstractmethod
    def exists(self) -> bool:
        """
        returns True if this ``Tier`` object has already been setup (e.g. by ``self.setup_files``)

        Note
        ----
        This currently only returns ``True`` if all parts of a Tier have been created, e.g. for a NotebookTier this means its folder,
        Notebook and meta file.
        """
        pass

    def get_child(self, id: str) -> TierABC:
        """
        Get a child according to the given ``id``.

        Parameters
        ----------
        id : str
            id to add ``self.identifiers`` to form new ``Tier`` object of tier below.

        Returns
        -------
        child : Type[TierBase]
            child ``Tier`` object.
        """
        assert self.child_cls
        return self.child_cls(*self._identifiers, id, project=self.project)

    def __truediv__(self, other: Any) -> Path:
        return cast(Path, self.folder / other)

    def __getitem__(self, item: str) -> TierABC:
        """
        Equivalent to [self.get_child(item)][cassini.core.TierABC.get_child].
        """
        return self.get_child(item)

    def __iter__(self) -> Iterator[Any]:
        """
        Iterates over all children (in no particular order). Children are found by using the
        [child_cls.iter_siblings()][cassini.core.TierABC.iter_siblings] method.

        Empty iterator if no children.
        """
        if not self.child_cls:
            raise NotImplementedError()

        yield from self.child_cls.iter_siblings(self)

    def __repr__(self) -> str:
        return f'<{self.__class__.__name__} "{self.name}">'

    def _repr_html_(self) -> str:
        block = f'h{len(self.identifiers) + 1} style="display: inline;"'
        return (
            f'<a href="{self.href}"'
            f' target="_blank"><{block}>{html.escape(self.name)}</{block}</a>'
        )

    @abstractmethod
    def remove_files(self) -> None:
        """
        Deletes files associated with a ``Tier``
        """
        pass

folder abstractmethod property

folder

The folder this tier corresponds to.

iter_siblings abstractmethod classmethod

iter_siblings(parent)

Provide an iterator for the siblings of this tier for a given parent i.e. iterate over parent's children.

Source code in cassini/core.py
154
155
156
157
158
159
160
161
@classmethod
@abstractmethod
def iter_siblings(cls, parent: TierABC) -> Iterator[TierABC]:
    """
    Provide an iterator for the siblings of this tier for a given parent i.e.
    iterate over parent's children.
    """
    pass

parse_name

parse_name(name)

Ask env.project to parse name.

Source code in cassini/core.py
210
211
212
213
214
def parse_name(self, name: str) -> Tuple[str, ...]:
    """
    Ask `env.project` to parse name.
    """
    return self.project.parse_name(name)

setup_files abstractmethod

setup_files(template=None, meta=None)

Create all the files needed for a valid Tier object to exist.

These parameters are ignored if not used.

Parameters:

Name Type Description Default
template Path

path to template file to render to create .ipynb file.

None
meta MetaDict

Initial meta values to create the tier with.

None
Source code in cassini/core.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
@abstractmethod
def setup_files(
    self, template: Union[Path, None] = None, meta: Optional[MetaDict] = None
) -> None:
    """
    Create all the files needed for a valid `Tier` object to exist.

    These parameters are ignored if not used.

    Parameters
    ----------
    template : Path
        path to template file to render to create `.ipynb` file.
    meta : MetaDict
        Initial meta values to create the tier with.
    """
    pass

identifiers

identifiers()

Read only copy of identifiers that make up this Tier object.

Source code in cassini/core.py
234
235
236
237
238
239
@cached_prop
def identifiers(self) -> Tuple[str, ...]:
    """
    Read only copy of identifiers that make up this `Tier` object.
    """
    return self._identifiers

name

name()

Full name of Tier object. Made by concatenating each parent's self.name_part_template filled with each parent's self.id.

Examples:

>>> wp = WorkPackage('2')
>>> exp = Experiment('2', '3')
>>> smpl = Sample('2', '3', 'c')
>>> wp.name_part_template.format(wp.id)
WP2
>>> exp.name_part_template.format(exp.id)
.3
>>> smpl.name_part_template.format(smpl.id)
c
>>> smpl.name  # all 3 joined together
WP2.3c
Source code in cassini/core.py
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
@cached_prop
def name(self) -> str:
    """
    Full name of `Tier` object. Made by concatenating each parent's `self.name_part_template` filled with each parent's
    `self.id`.

    Examples
    --------

        >>> wp = WorkPackage('2')
        >>> exp = Experiment('2', '3')
        >>> smpl = Sample('2', '3', 'c')
        >>> wp.name_part_template.format(wp.id)
        WP2
        >>> exp.name_part_template.format(exp.id)
        .3
        >>> smpl.name_part_template.format(smpl.id)
        c
        >>> smpl.name  # all 3 joined together
        WP2.3c
    """
    return "".join(
        cls.name_part_template.format(id)
        for cls, id in zip(self.project.hierarchy[1:], self.identifiers)
    )

open_folder

open_folder()

Open self.folder in explorer

Notes

Window is opened via the Jupyter server, not the browser, so if you are not accessing jupyter on localhost then the window won't open for you!

Source code in cassini/core.py
275
276
277
278
279
280
281
282
283
284
def open_folder(self) -> None:
    """
    Open `self.folder` in explorer

    Notes
    -----
    Window is opened via the Jupyter server, not the browser, so if you are not accessing jupyter on localhost then
    the window won't open for you!
    """
    open_file(self.folder)

id

id()

Shortcut for getting final identifier.

Examples:

>>> from my_project import project
>>> smpl = project.env('WP2.3c')
>>> smpl.identifiers
['2', '3', 'c']
>>> smpl.id
'c'
Source code in cassini/core.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
@cached_prop
def id(self) -> str:
    """
    Shortcut for getting final identifier.

    Examples
    --------

        >>> from my_project import project
        >>> smpl = project.env('WP2.3c')
        >>> smpl.identifiers
        ['2', '3', 'c']
        >>> smpl.id
        'c'
    """
    return self._identifiers[-1]

parent

parent()

Parent of this Tier instance, None if has no parent.

Source code in cassini/core.py
303
304
305
306
307
308
309
310
@cached_prop
def parent(self) -> Union[TierABC, None]:
    """
    Parent of this ``Tier`` instance, ``None`` if has no parent.
    """
    if self.parent_cls:
        return self.parent_cls(*self._identifiers[:-1], project=self.project)
    return None

href abstractmethod

href()

href usable in notebook HTML giving link to self.file.

Assumes that os.getcwd() reflects the directory of the currently opened .ipynb (usually true, unless you're changing working dir).

Does do escaping on the HTML, but is maybe pointless!

Returns:

Name Type Description
href str

href usable in notebook HTML.

Source code in cassini/core.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
@cached_prop
@abstractmethod
def href(self) -> Union[str, None]:
    """
    href usable in notebook HTML giving link to ``self.file``.

    Assumes that ``os.getcwd()`` reflects the directory of the currently opened ``.ipynb`` (usually true, unless you're
    changing working dir).

    Does do escaping on the HTML, but is maybe pointless!

    Returns
    -------
    href : str
        href usable in notebook HTML.
    """
    pass

exists abstractmethod

exists()

returns True if this Tier object has already been setup (e.g. by self.setup_files)

Note

This currently only returns True if all parts of a Tier have been created, e.g. for a NotebookTier this means its folder, Notebook and meta file.

Source code in cassini/core.py
330
331
332
333
334
335
336
337
338
339
340
@abstractmethod
def exists(self) -> bool:
    """
    returns True if this ``Tier`` object has already been setup (e.g. by ``self.setup_files``)

    Note
    ----
    This currently only returns ``True`` if all parts of a Tier have been created, e.g. for a NotebookTier this means its folder,
    Notebook and meta file.
    """
    pass

get_child

get_child(id)

Get a child according to the given id.

Parameters:

Name Type Description Default
id str

id to add self.identifiers to form new Tier object of tier below.

required

Returns:

Name Type Description
child Type[TierBase]

child Tier object.

Source code in cassini/core.py
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
def get_child(self, id: str) -> TierABC:
    """
    Get a child according to the given ``id``.

    Parameters
    ----------
    id : str
        id to add ``self.identifiers`` to form new ``Tier`` object of tier below.

    Returns
    -------
    child : Type[TierBase]
        child ``Tier`` object.
    """
    assert self.child_cls
    return self.child_cls(*self._identifiers, id, project=self.project)

__getitem__

__getitem__(item)

Equivalent to self.get_child(item).

Source code in cassini/core.py
362
363
364
365
366
def __getitem__(self, item: str) -> TierABC:
    """
    Equivalent to [self.get_child(item)][cassini.core.TierABC.get_child].
    """
    return self.get_child(item)

__iter__

__iter__()

Iterates over all children (in no particular order). Children are found by using the child_cls.iter_siblings() method.

Empty iterator if no children.

Source code in cassini/core.py
368
369
370
371
372
373
374
375
376
377
378
def __iter__(self) -> Iterator[Any]:
    """
    Iterates over all children (in no particular order). Children are found by using the
    [child_cls.iter_siblings()][cassini.core.TierABC.iter_siblings] method.

    Empty iterator if no children.
    """
    if not self.child_cls:
        raise NotImplementedError()

    yield from self.child_cls.iter_siblings(self)

remove_files abstractmethod

remove_files()

Deletes files associated with a Tier

Source code in cassini/core.py
390
391
392
393
394
395
@abstractmethod
def remove_files(self) -> None:
    """
    Deletes files associated with a ``Tier``
    """
    pass

FolderTierBase

Bases: TierABC

Base class for a tier which has a folder, but not notebook/ meta.

Source code in cassini/core.py
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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
class FolderTierBase(TierABC):
    """
    Base class for a tier which has a folder, but not notebook/ meta.
    """

    gui_cls = JLGui

    @classmethod
    def iter_siblings(cls, parent: TierABC) -> Iterator[TierABC]:
        # TODO: shouldn't project also handle this?
        if not parent.folder.exists():
            return

        for folder in os.scandir(parent.folder):
            if not folder.is_dir():
                continue
            yield cls(*parent.parse_name(folder.name), project=parent.project)

    @cached_prop
    def folder(self) -> Path:
        """
        Path to folder where the contents of this ``Tier`` lives.

        Defaults to `self.parent.folder / self.name`.
        """
        if self.parent:
            return self.parent.folder / self.name
        else:  # this is bad
            return Path(self.name)

    def setup_files(self, template: Union[Path, None] = None, meta=None) -> None:
        print(f"Creating Folder for {self} at {self.folder}")

        with FileMaker() as maker:
            maker.mkdir(self.folder.parent, exist_ok=True)
            maker.mkdir(self.folder)

        print("Success")

    def remove_files(self) -> None:
        pass

    def exists(self) -> bool:
        """
        returns True if ``self.folder`` exists.
        """
        return self.folder.exists()

    @cached_prop
    def href(self) -> Union[str, None]:
        return html.escape(Path(os.path.relpath(self.folder, os.getcwd())).as_posix())

folder

folder()

Path to folder where the contents of this Tier lives.

Defaults to self.parent.folder / self.name.

Source code in cassini/core.py
416
417
418
419
420
421
422
423
424
425
426
@cached_prop
def folder(self) -> Path:
    """
    Path to folder where the contents of this ``Tier`` lives.

    Defaults to `self.parent.folder / self.name`.
    """
    if self.parent:
        return self.parent.folder / self.name
    else:  # this is bad
        return Path(self.name)

exists

exists()

returns True if self.folder exists.

Source code in cassini/core.py
440
441
442
443
444
def exists(self) -> bool:
    """
    returns True if ``self.folder`` exists.
    """
    return self.folder.exists()

NotebookTierBase

Bases: FolderTierBase

Base class for tiers which have a notebook and meta associated with them.

Attributes:

Name Type Description
meta Meta

Object for storing meta data for this tier.

meta_model MetaCache

pydantic Model for this Tier's meta. This is generated once and then cached. This is done by looking for MetaAttr attributes in the class.

Source code in cassini/core.py
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
class NotebookTierBase(FolderTierBase):
    """
    Base class for tiers which have a notebook and meta associated with them.

    Attributes
    ----------
    meta : cassini.meta.Meta
        Object for storing meta data for this tier.
    meta_model : cassini.meta.MetaCache
        pydantic Model for this ``Tier``'s meta. This is generated once and then cached. This is done by looking
        for [MetaAttr][cassini.meta.MetaAttr] attributes in the class.

    """

    meta: Meta

    @cached_class_prop
    def meta_model(cls):
        return Meta.build_meta_model(cls)

    @cached_class_prop
    def _default_template(cls) -> Path:
        """
        Template used to render a tier file by default.
        """
        return Path(cls.pretty_type) / f"{cls.pretty_type}.tmplt.ipynb"

    default_template = _default_template

    @cached_class_prop
    def _meta_folder_name(cls) -> str:
        """
        Form of meta folder name. (Just fills in `config.META_DIR_TEMPLATE` with `cls.short_type`).
        """
        return config.META_DIR_TEMPLATE.format(cls.short_type)

    meta_folder_name = _meta_folder_name

    @classmethod
    def iter_siblings(cls, parent):
        meta_folder = parent.folder / config.META_DIR_TEMPLATE.format(cls.short_type)

        if not meta_folder.exists():
            return

        for meta_file in os.scandir(meta_folder):
            if not meta_file.is_file() or not meta_file.name.endswith(".json"):
                continue
            yield cls(
                *parent.parse_name(meta_file.name[:-5]), project=parent.project
            )  # I don't like this.

    def __init__(self, *identifiers: str, project: Project):
        super().__init__(*identifiers, project=project)
        self.meta: Meta = Meta.create_meta(self.meta_file, owner=self)

    def setup_files(
        self, template: Union[Path, None] = None, meta: Optional[MetaDict] = None
    ) -> None:
        """
        Create all the files needed for a valid `Tier` object to exist.

        This includes its `.ipynb` file, its parent folder, its own folder and its `Meta` file.

        Will render `.ipynb` file as Jinja template engine, passing to the template `self` with names given by
        `self.short_type` and `tier`.

        Parameters
        ----------
        template : Path
            path to template file to render to create `.ipynb` file.
        meta : MetaDict
            Initial meta values to create the tier with.
        """
        if template is None:
            template = self.default_template

        if meta is None:
            meta = {}

        if self.exists():
            raise FileExistsError(f"Meta for {self.name} exists already")

        if self.file and self.file.exists():
            raise FileExistsError(f"Notebook for {self.name} exists already")

        print(f"Creating files for {self.name}")

        print(f"Meta ({self.meta_file})")

        with FileMaker() as maker:
            maker.mkdir(self.meta.file.parent, exist_ok=True)
            maker.write_file(self.meta.file, json.dumps(meta))

            print("Writing Meta Data")

            self.started = datetime.datetime.now(datetime.timezone.utc)

            print("Success")

            print(f"Creating Tier File ({self.file}) using template ({template})")
            maker.write_file(self.file, self.render_template(template))
            print("Success")

            print(f"Creating Tier Folder ({self.folder})")

            self.folder.mkdir(exist_ok=True)

            print("Success")

        print("All Done")

    def exists(self) -> bool:
        """
        returns True if this `Tier` object has already been setup (e.g. by `self.setup_files`)
        """
        return bool(self.file and self.folder.exists() and self.meta_file.exists())

    description = MetaAttr(str, str, cas_field="core")
    conclusion = MetaAttr(str, str, cas_field="core")
    started = MetaAttr(AwareDatetime, datetime.datetime, cas_field="core")

    @cached_prop
    def meta_file(self) -> Path:
        """
        Path to where meta file for this `Tier` object should be.

        Returns
        -------
        meta_file : Path
            Defaults to `self.parent.folder / self.meta_folder_name / (self.name + '.json')`
        """
        assert self.parent
        return self.parent.folder / self.meta_folder_name / (self.name + ".json")

    @cached_prop
    def highlights_file(self) -> Union[Path, None]:
        """
        Path to where highlights file for this `Tier` object should be.

        Returns
        -------
        highlights_file : Path
            Defaults to `self.parent.folder / self._meta_folder_name / (self.name + '.hlts')`
        """
        assert self.parent
        return self.parent.folder / self._meta_folder_name / (self.name + ".hlts")

    @cached_prop
    def file(self) -> Path:
        """
        Path to where `.ipynb` file for this `Tier` instance will be.

        Returns
        -------
        file : Path
            Defaults to self.parent.folder / (self.name + '.ipynb').
        """
        assert self.parent
        return self.parent.folder / (self.name + ".ipynb")

    @classmethod
    def get_templates(cls, project: Project) -> List[Path]:
        """
        Get all the templates for this `Tier`.
        """
        return [
            Path(cls.pretty_type) / entry.name
            for entry in os.scandir(project.template_folder / cls.pretty_type)
            if entry.is_file()
        ]

    def render_template(self, template_path: Path) -> str:
        """
        Render template file passing `self` as `self.short_type` and as `tier`.

        Parameters
        ----------
        template_path : Path
            path to template file. Must be relative to `project.template_folder`

        Returns
        -------
        rendered_text : str
            template rendered with `self`.
        """
        template = self.project.template_env.get_template(template_path)
        return template.render(**{self.short_type: self, "tier": self})

    def get_highlights(self) -> Union[HighlightsType, None]:
        """
        Get dictionary of highlights for this `Tier` instance.

        This dictionary is in a form that can be rendered in the notebook using `IPython.display.publish_display_data`
        see Examples.

        Returns
        -------
        highlights : dict
            Get dictionary of highlights for this `Tier` instance. If the highlights file doesn't exist, just returns an
            empty dict.

        Examples
        --------

            >>> wp = project['WP1']
            >>> for title, outputs in wp.get_highlights():
            ...     print("Displaying Highlight:", title)
            ...     for output in outputs:
            ...         IPython.display.publish_display_data(**output)
        """
        if self.highlights_file and self.highlights_file.exists():
            highlights = cast(
                HighlightsType, json.loads(self.highlights_file.read_text())
            )
            return highlights
        else:
            return {}

    def add_highlight(
        self, name: str, data: HighlightType, overwrite: bool = True
    ) -> None:
        """
        Add a highlight to `self.highlights_file`.

        This is usually done behind the scenes using the `%%hlt My Title` magic.

        Parameters
        ----------
        name : str
            Name of highlight (also taken as the title).
        data : HighlightType
            list of data and metadata that can be passed to `IPython.display.publish_display_data` to render.
        overwrite : bool
            If `False` will raise an exception if a highlight of the same `name` exists. Default is `True`
        """
        highlights = self.get_highlights()

        if self.highlights_file and highlights is not None:
            highlights = highlights.copy()
        else:
            return

        if not overwrite and name in highlights:
            raise KeyError("Attempting to overwrite existing meta value")

        highlights[name] = data
        self.highlights_file.write_text(json.dumps(highlights), encoding="utf-8")

    def remove_highlight(self, name: str) -> None:
        """
        Remove highlight from highlight file. Performed by calling `get_highlights()`, then deleting the key `name` from
        the dictionary, then re-writing the highlights... if you're interested!
        """
        highlights = self.get_highlights()

        if not highlights or not self.highlights_file:
            return

        del highlights[name]
        self.highlights_file.write_text(json.dumps(highlights), encoding="utf-8")

    @cached_prop
    def href(self) -> Union[str, None]:
        """
        href usable in notebook HTML giving link to `self.file`.

        Assumes that `os.getcwd()` reflects the directory of the currently opened `.ipynb` (usually true, unless you're
        changing working dir).

        Does do escaping on the HTML, but is maybe pointless!

        Returns
        -------
        href : str
            href usable in notebook HTML.
        """
        return html.escape(Path(os.path.relpath(self.file, os.getcwd())).as_posix())

    def remove_files(self) -> None:
        """
        Deletes files associated with a `Tier`
        """
        if self.file:
            self.file.unlink()

        if self.meta_file:
            self.meta_file.unlink()

setup_files

setup_files(template=None, meta=None)

Create all the files needed for a valid Tier object to exist.

This includes its .ipynb file, its parent folder, its own folder and its Meta file.

Will render .ipynb file as Jinja template engine, passing to the template self with names given by self.short_type and tier.

Parameters:

Name Type Description Default
template Path

path to template file to render to create .ipynb file.

None
meta MetaDict

Initial meta values to create the tier with.

None
Source code in cassini/core.py
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
def setup_files(
    self, template: Union[Path, None] = None, meta: Optional[MetaDict] = None
) -> None:
    """
    Create all the files needed for a valid `Tier` object to exist.

    This includes its `.ipynb` file, its parent folder, its own folder and its `Meta` file.

    Will render `.ipynb` file as Jinja template engine, passing to the template `self` with names given by
    `self.short_type` and `tier`.

    Parameters
    ----------
    template : Path
        path to template file to render to create `.ipynb` file.
    meta : MetaDict
        Initial meta values to create the tier with.
    """
    if template is None:
        template = self.default_template

    if meta is None:
        meta = {}

    if self.exists():
        raise FileExistsError(f"Meta for {self.name} exists already")

    if self.file and self.file.exists():
        raise FileExistsError(f"Notebook for {self.name} exists already")

    print(f"Creating files for {self.name}")

    print(f"Meta ({self.meta_file})")

    with FileMaker() as maker:
        maker.mkdir(self.meta.file.parent, exist_ok=True)
        maker.write_file(self.meta.file, json.dumps(meta))

        print("Writing Meta Data")

        self.started = datetime.datetime.now(datetime.timezone.utc)

        print("Success")

        print(f"Creating Tier File ({self.file}) using template ({template})")
        maker.write_file(self.file, self.render_template(template))
        print("Success")

        print(f"Creating Tier Folder ({self.folder})")

        self.folder.mkdir(exist_ok=True)

        print("Success")

    print("All Done")

exists

exists()

returns True if this Tier object has already been setup (e.g. by self.setup_files)

Source code in cassini/core.py
563
564
565
566
567
def exists(self) -> bool:
    """
    returns True if this `Tier` object has already been setup (e.g. by `self.setup_files`)
    """
    return bool(self.file and self.folder.exists() and self.meta_file.exists())

meta_file

meta_file()

Path to where meta file for this Tier object should be.

Returns:

Name Type Description
meta_file Path

Defaults to self.parent.folder / self.meta_folder_name / (self.name + '.json')

Source code in cassini/core.py
573
574
575
576
577
578
579
580
581
582
583
584
@cached_prop
def meta_file(self) -> Path:
    """
    Path to where meta file for this `Tier` object should be.

    Returns
    -------
    meta_file : Path
        Defaults to `self.parent.folder / self.meta_folder_name / (self.name + '.json')`
    """
    assert self.parent
    return self.parent.folder / self.meta_folder_name / (self.name + ".json")

highlights_file

highlights_file()

Path to where highlights file for this Tier object should be.

Returns:

Name Type Description
highlights_file Path

Defaults to self.parent.folder / self._meta_folder_name / (self.name + '.hlts')

Source code in cassini/core.py
586
587
588
589
590
591
592
593
594
595
596
597
@cached_prop
def highlights_file(self) -> Union[Path, None]:
    """
    Path to where highlights file for this `Tier` object should be.

    Returns
    -------
    highlights_file : Path
        Defaults to `self.parent.folder / self._meta_folder_name / (self.name + '.hlts')`
    """
    assert self.parent
    return self.parent.folder / self._meta_folder_name / (self.name + ".hlts")

file

file()

Path to where .ipynb file for this Tier instance will be.

Returns:

Name Type Description
file Path

Defaults to self.parent.folder / (self.name + '.ipynb').

Source code in cassini/core.py
599
600
601
602
603
604
605
606
607
608
609
610
@cached_prop
def file(self) -> Path:
    """
    Path to where `.ipynb` file for this `Tier` instance will be.

    Returns
    -------
    file : Path
        Defaults to self.parent.folder / (self.name + '.ipynb').
    """
    assert self.parent
    return self.parent.folder / (self.name + ".ipynb")

get_templates classmethod

get_templates(project)

Get all the templates for this Tier.

Source code in cassini/core.py
612
613
614
615
616
617
618
619
620
621
@classmethod
def get_templates(cls, project: Project) -> List[Path]:
    """
    Get all the templates for this `Tier`.
    """
    return [
        Path(cls.pretty_type) / entry.name
        for entry in os.scandir(project.template_folder / cls.pretty_type)
        if entry.is_file()
    ]

render_template

render_template(template_path)

Render template file passing self as self.short_type and as tier.

Parameters:

Name Type Description Default
template_path Path

path to template file. Must be relative to project.template_folder

required

Returns:

Name Type Description
rendered_text str

template rendered with self.

Source code in cassini/core.py
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
def render_template(self, template_path: Path) -> str:
    """
    Render template file passing `self` as `self.short_type` and as `tier`.

    Parameters
    ----------
    template_path : Path
        path to template file. Must be relative to `project.template_folder`

    Returns
    -------
    rendered_text : str
        template rendered with `self`.
    """
    template = self.project.template_env.get_template(template_path)
    return template.render(**{self.short_type: self, "tier": self})

get_highlights

get_highlights()

Get dictionary of highlights for this Tier instance.

This dictionary is in a form that can be rendered in the notebook using IPython.display.publish_display_data see Examples.

Returns:

Name Type Description
highlights dict

Get dictionary of highlights for this Tier instance. If the highlights file doesn't exist, just returns an empty dict.

Examples:

>>> wp = project['WP1']
>>> for title, outputs in wp.get_highlights():
...     print("Displaying Highlight:", title)
...     for output in outputs:
...         IPython.display.publish_display_data(**output)
Source code in cassini/core.py
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
def get_highlights(self) -> Union[HighlightsType, None]:
    """
    Get dictionary of highlights for this `Tier` instance.

    This dictionary is in a form that can be rendered in the notebook using `IPython.display.publish_display_data`
    see Examples.

    Returns
    -------
    highlights : dict
        Get dictionary of highlights for this `Tier` instance. If the highlights file doesn't exist, just returns an
        empty dict.

    Examples
    --------

        >>> wp = project['WP1']
        >>> for title, outputs in wp.get_highlights():
        ...     print("Displaying Highlight:", title)
        ...     for output in outputs:
        ...         IPython.display.publish_display_data(**output)
    """
    if self.highlights_file and self.highlights_file.exists():
        highlights = cast(
            HighlightsType, json.loads(self.highlights_file.read_text())
        )
        return highlights
    else:
        return {}

add_highlight

add_highlight(name, data, overwrite=True)

Add a highlight to self.highlights_file.

This is usually done behind the scenes using the %%hlt My Title magic.

Parameters:

Name Type Description Default
name str

Name of highlight (also taken as the title).

required
data HighlightType

list of data and metadata that can be passed to IPython.display.publish_display_data to render.

required
overwrite bool

If False will raise an exception if a highlight of the same name exists. Default is True

True
Source code in cassini/core.py
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
def add_highlight(
    self, name: str, data: HighlightType, overwrite: bool = True
) -> None:
    """
    Add a highlight to `self.highlights_file`.

    This is usually done behind the scenes using the `%%hlt My Title` magic.

    Parameters
    ----------
    name : str
        Name of highlight (also taken as the title).
    data : HighlightType
        list of data and metadata that can be passed to `IPython.display.publish_display_data` to render.
    overwrite : bool
        If `False` will raise an exception if a highlight of the same `name` exists. Default is `True`
    """
    highlights = self.get_highlights()

    if self.highlights_file and highlights is not None:
        highlights = highlights.copy()
    else:
        return

    if not overwrite and name in highlights:
        raise KeyError("Attempting to overwrite existing meta value")

    highlights[name] = data
    self.highlights_file.write_text(json.dumps(highlights), encoding="utf-8")

remove_highlight

remove_highlight(name)

Remove highlight from highlight file. Performed by calling get_highlights(), then deleting the key name from the dictionary, then re-writing the highlights... if you're interested!

Source code in cassini/core.py
700
701
702
703
704
705
706
707
708
709
710
711
def remove_highlight(self, name: str) -> None:
    """
    Remove highlight from highlight file. Performed by calling `get_highlights()`, then deleting the key `name` from
    the dictionary, then re-writing the highlights... if you're interested!
    """
    highlights = self.get_highlights()

    if not highlights or not self.highlights_file:
        return

    del highlights[name]
    self.highlights_file.write_text(json.dumps(highlights), encoding="utf-8")

href

href()

href usable in notebook HTML giving link to self.file.

Assumes that os.getcwd() reflects the directory of the currently opened .ipynb (usually true, unless you're changing working dir).

Does do escaping on the HTML, but is maybe pointless!

Returns:

Name Type Description
href str

href usable in notebook HTML.

Source code in cassini/core.py
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
@cached_prop
def href(self) -> Union[str, None]:
    """
    href usable in notebook HTML giving link to `self.file`.

    Assumes that `os.getcwd()` reflects the directory of the currently opened `.ipynb` (usually true, unless you're
    changing working dir).

    Does do escaping on the HTML, but is maybe pointless!

    Returns
    -------
    href : str
        href usable in notebook HTML.
    """
    return html.escape(Path(os.path.relpath(self.file, os.getcwd())).as_posix())

remove_files

remove_files()

Deletes files associated with a Tier

Source code in cassini/core.py
730
731
732
733
734
735
736
737
738
def remove_files(self) -> None:
    """
    Deletes files associated with a `Tier`
    """
    if self.file:
        self.file.unlink()

    if self.meta_file:
        self.meta_file.unlink()

HomeTierBase

Bases: FolderTierBase

Home Tier.

This, or a subclass of this should generally be the first entry in your hierarchy, essentially represents the top level folder in your hierarchy.

Creates the Home.ipynb notebook that allows easy navigation of your project.

Source code in cassini/core.py
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
class HomeTierBase(FolderTierBase):
    """
    Home `Tier`.

    This, or a subclass of this should generally be the first entry in your hierarchy, essentially represents the top
    level folder in your hierarchy.

    Creates the `Home.ipynb` notebook that allows easy navigation of your project.
    """

    @cached_prop
    def name(self) -> str:
        return self.pretty_type

    @classmethod
    def iter_siblings(cls, parent: TierABC) -> Iterator[TierABC]:
        raise NotImplementedError("Home tier cannot be iterated over.")

    @cached_prop
    def folder(self) -> Path:
        assert self.child_cls
        return self.project.project_folder / (self.child_cls.pretty_type + "s")

    def setup_files(self, template: Union[Path, None] = None, meta=None) -> None:
        assert self.child_cls

        with FileMaker() as maker:
            print(f"Creating {self.child_cls.pretty_type} folder")
            maker.mkdir(self.folder, exist_ok=True)
            print("Success")

Project

Represents your project. Understands your naming convention, and your project hierarchy.

Some hooks are provided to customize setup and launching behaviour, see __before_setup_files__, __after_setup_files__, __before_launch__ and __after_launch__.

Parameters:

Name Type Description Default
hierarchy Sequence[Type[BaseTier]]

Sequence of TierBase subclasses representing the hierarchy for this project. i.e. earlier entries are stored in higher level directories.

required
project_folder Union[str, Path]

path to home directory. Note this also accepts a path to a file, but will take project_folder.parent in that case. This enables __file__ to be used if you want project_folder to be based in the same dir.

required

Attributes:

Name Type Description
__before_setup_files__ List[Callable[[Project], None]]

Sequence of callables that are called, in order, first thing when project.setup_files() is called. Project is the calling project instance.

__after_setup_files__ List[Callable[[Project], None]]

Sequence of callables that are called, in order, last thing project.setup_files() is called.

Note

__after_setup_files__ are only called if the project hasn't already been setup.

__before_launch__ List[Callable[[Project, Union[LabApp, None]], None]]

Sequence of callables that are called first thing when project.launch() is ran. Project is the current project and LabApp is the lab app being launched.

__after_launch__ List[Callable[[Project, Union[LabApp, None]], None]]

Sequence of callables that are called last thing after project.launch() is ran. Project is the current project and LabApp is the lab app being launched.

Notes

This class is a singleton i.e. only 1 instance per interpreter can be created.

Source code in cassini/core.py
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
class Project:
    """
    Represents your project. Understands your naming convention, and your project hierarchy.

    Some hooks are provided to customize setup and launching behaviour, see
    `__before_setup_files__`, `__after_setup_files__`, `__before_launch__` and `__after_launch__`.

    Parameters
    ----------
    hierarchy : Sequence[Type[BaseTier]]
        Sequence of `TierBase` subclasses representing the hierarchy for this project. i.e. earlier entries are stored
        in higher level directories.
    project_folder : Union[str, Path]
        path to home directory. Note this also accepts a path to a file, but will take `project_folder.parent` in that
        case. This enables `__file__` to be used if you want `project_folder` to be based in the same dir.


    Attributes
    ----------
    __before_setup_files__ : List[Callable[[Project], None]]
        Sequence of callables that are called, in order, first thing when `project.setup_files()` is called.
        `Project` is the calling project instance.
    __after_setup_files__ : List[Callable[[Project], None]]
        Sequence of callables that are called, in order, last thing `project.setup_files()` is called.

        Note
        ----
        `__after_setup_files__` are only called _if_ the project hasn't already been setup.

    __before_launch__ : List[Callable[[Project, Union[LabApp, None]], None]]
        Sequence of callables that are called first thing when `project.launch()` is ran.
        `Project` is the current project and `LabApp` is the lab app being launched.
    __after_launch__ : List[Callable[[Project, Union[LabApp, None]], None]]
        Sequence of callables that are called last thing after `project.launch()` is ran.
        `Project` is the current project and `LabApp` is the lab app being launched.

    Notes
    -----
    This class is a singleton i.e. only 1 instance per interpreter can be created.
    """

    def __new__(cls, *args: Any, **kwargs: Any) -> Project:
        if env.project:
            raise RuntimeError(
                "Attempted to create new Project instance, only 1 instance permitted per interpreter"
            )
        instance = object.__new__(cls)
        env.project = instance
        return instance

    def __init__(
        self, hierarchy: Sequence[Type[TierABC]], project_folder: Union[str, Path]
    ) -> None:
        self._rank_map: Dict[Type[TierABC], int] = {}
        self._hierarchy: Sequence[Type[TierABC]] = []

        self.__before_setup_files__: List[Callable[[Project], None]] = []
        self.__after_setup_files__: List[Callable[[Project], None]] = []

        self.__before_launch__: List[
            Callable[[Project, Union[LabApp, None]], None]
        ] = []
        self.__after_launch__: List[Callable[[Project, Union[LabApp, None]], None]] = []

        self.hierarchy: Sequence[Type[TierABC]] = hierarchy

        project_folder_path = Path(project_folder).resolve()
        self.project_folder: Path = (
            project_folder_path
            if project_folder_path.is_dir()
            else project_folder_path.parent
        )

        self.template_env: PathLibEnv = PathLibEnv(
            autoescape=jinja2.select_autoescape(["html", "xml"]),
            loader=jinja2.FileSystemLoader(self.template_folder),
        )

    @property
    def hierarchy(self) -> Sequence[Type[TierABC]]:
        """
        Sequence of `TierBase` subclasses representing the hierarchy for this project. i.e. earlier entries are stored
        in higher level directories.
        """
        return self._hierarchy

    @hierarchy.setter
    def hierarchy(self, hierarchy: Sequence[Type[TierABC]]):
        self._hierarchy = hierarchy

        for rank, tier_cls in enumerate(hierarchy):
            self._rank_map[tier_cls] = rank

    @property
    def rank_map(self):
        """
        Maps `Tier` types to their position in the hierarchy.
        """
        return self._rank_map

    @property
    def home(self) -> TierABC:
        """
        Get the home `Tier`.
        """
        return self.hierarchy[0](project=self)

    def env(self, name: str) -> TierABC:
        """
        Initialise the global environment to a particular `Tier` that is retrieved by parsing `name`.

        This will set the value of `env.o`.

        Warnings
        --------
        This should only really be called once (or only with 1 name). Otherwise this could create some unexpected
        behaviour.
        """
        obj = self.__getitem__(name)

        if env.o and name != env.o.name:
            warn(
                (
                    f"Overwriting the global Tier {env.o} for this interpreter. This may cause unexpected behaviour. "
                    f"If you wish to create Tier objects that aren't the current Tier I recommend initialising them "
                    f"directly e.g. obj = MyTier('id1', 'id2')"
                )
            )

        env.update(obj)
        return obj

    def get_tier(self, identifiers: Tuple[str, ...]) -> TierABC:
        """
        Get a tier for a given set of identifiers.
        """
        cls = self.hierarchy[len(identifiers)]
        return cls(*identifiers, project=self)

    def get_child_cls(self, tier_cls: Type[TierABC]) -> Union[None, Type[TierABC]]:
        """
        Get the child class of a given tier class. Returns None if there is no child class
        """
        rank = self.rank_map[tier_cls]
        if rank + 1 > (len(self.hierarchy) - 1):
            return None
        else:
            cls = self.hierarchy[rank + 1]  # I don't understand why annotation needed?
            return cls

    def get_parent_cls(self, tier_cls: Type[TierABC]) -> Union[None, Type[TierABC]]:
        """
        Get the parent class of a given tier class. Returns None if there is no parent class
        """
        rank = self.rank_map[tier_cls]
        if rank - 1 < 0:
            return None
        else:
            cls = self.hierarchy[rank - 1]
            return cls

    def __getitem__(self, name: str) -> TierABC:
        """
        Retrieve a tier object from the project by name.

        Parameters
        ----------
        name : str
            Parsable name to get the tier object by. To get your `Home` just provide `Home.name`.

        Returns
        -------
        tier : TierBase
            Tier retrieved from project.
        """
        if name == self.home.name:
            obj = self.home
        else:
            identifiers = self.parse_name(name)
            if not identifiers:
                raise ValueError(f"Name {name} not recognised as identifying any Tier")
            obj = self.get_tier(identifiers)
        return obj

    @soft_prop
    def template_folder(self) -> Path:
        """
        Overwritable property providing where templates will be stored for this project.
        """
        return self.project_folder / "templates"

    def setup_files(self) -> TierABC:
        """
        Setup files needed for this project.

        Will put everything you need in `project_folder` to get going.

        Note
        ----
        This does not call `__after_setup_files__` if the project already exists.

        """
        for func in self.__before_setup_files__:
            func(self)

        home = self.home

        if home.exists():
            return home

        print("Setting up project.")

        with FileMaker() as maker:
            print("Creating templates folder")
            maker.mkdir(self.template_folder)
            print("Success")

            for tier_cls in self.hierarchy:
                if not issubclass(tier_cls, NotebookTierBase):
                    continue

                maker.mkdir(self.template_folder / tier_cls.pretty_type)
                print("Copying over default template")
                maker.copy_file(
                    config.BASE_TEMPLATE,
                    self.template_folder / tier_cls.default_template,
                )
                print("Done")

        print("Setting up Home Tier")
        home.setup_files()
        print("Success")

        for func in self.__after_setup_files__:
            func(self)

        return home

    def launch(
        self, app: Union[LabApp, None] = None, patch_pythonpath: bool = True
    ) -> LabApp:
        """
        Jump off point for a cassini project.

        Sets up required files for your project, monkeypatches `PYTHONPATH` to make your project available throughout
        and launches a jupyterlab server.

        This explicitly associates an instance of the Jupyter server with a particular project.

        Parameters
        ----------
        app : LabApp
            A ready made Jupyter Lab app (By defuault will just create a new one).
        patch_pythonpath : bool
            Add `self.project_folder` to the `PYTHONPATH`? (defaults to `True`)
        """
        for func in self.__before_launch__:
            func(self, app)

        self.setup_files()

        if patch_pythonpath:
            py_path = os.environ.get("PYTHONPATH", "")
            project_path = str(self.project_folder.resolve())
            os.environ["PYTHONPATH"] = (
                py_path + os.pathsep + project_path if py_path else project_path
            )

        if app is None:
            app = CassiniLabApp()

        app.launch_instance()

        for func in self.__after_launch__:
            func(self, app)

        return app

    def parse_name(self, name: str) -> Tuple[str, ...]:
        """
        Parses a string that corresponds to a `Tier` and returns a list of its identifiers.

        returns an empty tuple if not a valid name.

        Parameters
        ----------
        name : str
            name to parse

        Returns
        -------
        identifiers : tuple
            identifiers extracted from name, empty tuple if `None` found

        Notes
        -----
        This works in a slightly strange - but robust way!

        e.g.

            >>> name = 'WP2.3c'

        it will loop through each entry in `cls.hierarchy` (skipping home!), and then perform a search on `name` with
        that regex:

            >>> WorkPackage.name_part_regex
            WP(\\d+)
            >>> match = re.search(WorkPackage.name_part_regex, name)

        If there's no match, it will return `()`, if there is, it stores the `id` part:

            >>> wp_id = match.group(1)  # in python group 0 is the whole match
            >>> wp_id
            2

        Then it removes the whole match from the name:

            >>> name = name[match.end(0):]
            >>> name
            .3c

        Then it moves on to the next tier

            >>> Experiment.name_part_regex
            '\\.(\\d+)'
            >>> match = re.search(WorkPackage.name_part_regex, name)

        If there's a match it extracts the id, and substracts the whole string from name and moves on, continuing this
        loop until it's gone through the whole hierarchy.

        The whole name has to be a valid id, or it will return `()` e.g.

            >>> TierBase.parse_name('WP2.3')
            ('2', '3')
            >>> TierBase.parse_name('WP2.u3')
            ()
        """
        parts = self.hierarchy[1:]
        ids: List[str] = []
        for tier_cls in parts:
            pattern = tier_cls.name_part_regex
            match = re.search(pattern, name)
            if match and match.start(0) == 0:
                ids.append(match.group(1))
                name = name[match.end(0) :]
            else:
                break
        if name:  # if there's any residual text then it's not a valid name!
            return tuple()
        else:
            return tuple(ids)

    def __repr__(self) -> str:
        return f"<Project at: '{self.project_folder}' hierarchy: '{self.hierarchy}' ({env})>"

hierarchy property writable

hierarchy

Sequence of TierBase subclasses representing the hierarchy for this project. i.e. earlier entries are stored in higher level directories.

rank_map property

rank_map

Maps Tier types to their position in the hierarchy.

home property

home

Get the home Tier.

env

env(name)

Initialise the global environment to a particular Tier that is retrieved by parsing name.

This will set the value of env.o.

Warnings

This should only really be called once (or only with 1 name). Otherwise this could create some unexpected behaviour.

Source code in cassini/core.py
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
def env(self, name: str) -> TierABC:
    """
    Initialise the global environment to a particular `Tier` that is retrieved by parsing `name`.

    This will set the value of `env.o`.

    Warnings
    --------
    This should only really be called once (or only with 1 name). Otherwise this could create some unexpected
    behaviour.
    """
    obj = self.__getitem__(name)

    if env.o and name != env.o.name:
        warn(
            (
                f"Overwriting the global Tier {env.o} for this interpreter. This may cause unexpected behaviour. "
                f"If you wish to create Tier objects that aren't the current Tier I recommend initialising them "
                f"directly e.g. obj = MyTier('id1', 'id2')"
            )
        )

    env.update(obj)
    return obj

get_tier

get_tier(identifiers)

Get a tier for a given set of identifiers.

Source code in cassini/core.py
905
906
907
908
909
910
def get_tier(self, identifiers: Tuple[str, ...]) -> TierABC:
    """
    Get a tier for a given set of identifiers.
    """
    cls = self.hierarchy[len(identifiers)]
    return cls(*identifiers, project=self)

get_child_cls

get_child_cls(tier_cls)

Get the child class of a given tier class. Returns None if there is no child class

Source code in cassini/core.py
912
913
914
915
916
917
918
919
920
921
def get_child_cls(self, tier_cls: Type[TierABC]) -> Union[None, Type[TierABC]]:
    """
    Get the child class of a given tier class. Returns None if there is no child class
    """
    rank = self.rank_map[tier_cls]
    if rank + 1 > (len(self.hierarchy) - 1):
        return None
    else:
        cls = self.hierarchy[rank + 1]  # I don't understand why annotation needed?
        return cls

get_parent_cls

get_parent_cls(tier_cls)

Get the parent class of a given tier class. Returns None if there is no parent class

Source code in cassini/core.py
923
924
925
926
927
928
929
930
931
932
def get_parent_cls(self, tier_cls: Type[TierABC]) -> Union[None, Type[TierABC]]:
    """
    Get the parent class of a given tier class. Returns None if there is no parent class
    """
    rank = self.rank_map[tier_cls]
    if rank - 1 < 0:
        return None
    else:
        cls = self.hierarchy[rank - 1]
        return cls

__getitem__

__getitem__(name)

Retrieve a tier object from the project by name.

Parameters:

Name Type Description Default
name str

Parsable name to get the tier object by. To get your Home just provide Home.name.

required

Returns:

Name Type Description
tier TierBase

Tier retrieved from project.

Source code in cassini/core.py
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
def __getitem__(self, name: str) -> TierABC:
    """
    Retrieve a tier object from the project by name.

    Parameters
    ----------
    name : str
        Parsable name to get the tier object by. To get your `Home` just provide `Home.name`.

    Returns
    -------
    tier : TierBase
        Tier retrieved from project.
    """
    if name == self.home.name:
        obj = self.home
    else:
        identifiers = self.parse_name(name)
        if not identifiers:
            raise ValueError(f"Name {name} not recognised as identifying any Tier")
        obj = self.get_tier(identifiers)
    return obj

template_folder

template_folder()

Overwritable property providing where templates will be stored for this project.

Source code in cassini/core.py
957
958
959
960
961
962
@soft_prop
def template_folder(self) -> Path:
    """
    Overwritable property providing where templates will be stored for this project.
    """
    return self.project_folder / "templates"

setup_files

setup_files()

Setup files needed for this project.

Will put everything you need in project_folder to get going.

Note

This does not call __after_setup_files__ if the project already exists.

Source code in cassini/core.py
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
def setup_files(self) -> TierABC:
    """
    Setup files needed for this project.

    Will put everything you need in `project_folder` to get going.

    Note
    ----
    This does not call `__after_setup_files__` if the project already exists.

    """
    for func in self.__before_setup_files__:
        func(self)

    home = self.home

    if home.exists():
        return home

    print("Setting up project.")

    with FileMaker() as maker:
        print("Creating templates folder")
        maker.mkdir(self.template_folder)
        print("Success")

        for tier_cls in self.hierarchy:
            if not issubclass(tier_cls, NotebookTierBase):
                continue

            maker.mkdir(self.template_folder / tier_cls.pretty_type)
            print("Copying over default template")
            maker.copy_file(
                config.BASE_TEMPLATE,
                self.template_folder / tier_cls.default_template,
            )
            print("Done")

    print("Setting up Home Tier")
    home.setup_files()
    print("Success")

    for func in self.__after_setup_files__:
        func(self)

    return home

launch

launch(app=None, patch_pythonpath=True)

Jump off point for a cassini project.

Sets up required files for your project, monkeypatches PYTHONPATH to make your project available throughout and launches a jupyterlab server.

This explicitly associates an instance of the Jupyter server with a particular project.

Parameters:

Name Type Description Default
app LabApp

A ready made Jupyter Lab app (By defuault will just create a new one).

None
patch_pythonpath bool

Add self.project_folder to the PYTHONPATH? (defaults to True)

True
Source code in cassini/core.py
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
def launch(
    self, app: Union[LabApp, None] = None, patch_pythonpath: bool = True
) -> LabApp:
    """
    Jump off point for a cassini project.

    Sets up required files for your project, monkeypatches `PYTHONPATH` to make your project available throughout
    and launches a jupyterlab server.

    This explicitly associates an instance of the Jupyter server with a particular project.

    Parameters
    ----------
    app : LabApp
        A ready made Jupyter Lab app (By defuault will just create a new one).
    patch_pythonpath : bool
        Add `self.project_folder` to the `PYTHONPATH`? (defaults to `True`)
    """
    for func in self.__before_launch__:
        func(self, app)

    self.setup_files()

    if patch_pythonpath:
        py_path = os.environ.get("PYTHONPATH", "")
        project_path = str(self.project_folder.resolve())
        os.environ["PYTHONPATH"] = (
            py_path + os.pathsep + project_path if py_path else project_path
        )

    if app is None:
        app = CassiniLabApp()

    app.launch_instance()

    for func in self.__after_launch__:
        func(self, app)

    return app

parse_name

parse_name(name)

Parses a string that corresponds to a Tier and returns a list of its identifiers.

returns an empty tuple if not a valid name.

Parameters:

Name Type Description Default
name str

name to parse

required

Returns:

Name Type Description
identifiers tuple

identifiers extracted from name, empty tuple if None found

Notes

This works in a slightly strange - but robust way!

e.g.

>>> name = 'WP2.3c'

it will loop through each entry in cls.hierarchy (skipping home!), and then perform a search on name with that regex:

>>> WorkPackage.name_part_regex
WP(\d+)
>>> match = re.search(WorkPackage.name_part_regex, name)

If there's no match, it will return (), if there is, it stores the id part:

>>> wp_id = match.group(1)  # in python group 0 is the whole match
>>> wp_id
2

Then it removes the whole match from the name:

>>> name = name[match.end(0):]
>>> name
.3c

Then it moves on to the next tier

>>> Experiment.name_part_regex
'\.(\d+)'
>>> match = re.search(WorkPackage.name_part_regex, name)

If there's a match it extracts the id, and substracts the whole string from name and moves on, continuing this loop until it's gone through the whole hierarchy.

The whole name has to be a valid id, or it will return () e.g.

>>> TierBase.parse_name('WP2.3')
('2', '3')
>>> TierBase.parse_name('WP2.u3')
()
Source code in cassini/core.py
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
def parse_name(self, name: str) -> Tuple[str, ...]:
    """
    Parses a string that corresponds to a `Tier` and returns a list of its identifiers.

    returns an empty tuple if not a valid name.

    Parameters
    ----------
    name : str
        name to parse

    Returns
    -------
    identifiers : tuple
        identifiers extracted from name, empty tuple if `None` found

    Notes
    -----
    This works in a slightly strange - but robust way!

    e.g.

        >>> name = 'WP2.3c'

    it will loop through each entry in `cls.hierarchy` (skipping home!), and then perform a search on `name` with
    that regex:

        >>> WorkPackage.name_part_regex
        WP(\\d+)
        >>> match = re.search(WorkPackage.name_part_regex, name)

    If there's no match, it will return `()`, if there is, it stores the `id` part:

        >>> wp_id = match.group(1)  # in python group 0 is the whole match
        >>> wp_id
        2

    Then it removes the whole match from the name:

        >>> name = name[match.end(0):]
        >>> name
        .3c

    Then it moves on to the next tier

        >>> Experiment.name_part_regex
        '\\.(\\d+)'
        >>> match = re.search(WorkPackage.name_part_regex, name)

    If there's a match it extracts the id, and substracts the whole string from name and moves on, continuing this
    loop until it's gone through the whole hierarchy.

    The whole name has to be a valid id, or it will return `()` e.g.

        >>> TierBase.parse_name('WP2.3')
        ('2', '3')
        >>> TierBase.parse_name('WP2.u3')
        ()
    """
    parts = self.hierarchy[1:]
    ids: List[str] = []
    for tier_cls in parts:
        pattern = tier_cls.name_part_regex
        match = re.search(pattern, name)
        if match and match.start(0) == 0:
            ids.append(match.group(1))
            name = name[match.end(0) :]
        else:
            break
    if name:  # if there's any residual text then it's not a valid name!
        return tuple()
    else:
        return tuple(ids)