Skip to content

project

project

Project facade to orchestrate models, experiments, and analysis.

Project

Bases: GuardedBase

Central API for managing a diffraction data analysis project.

Provides access to structures, experiments, analysis, and summary.

Source code in src/easydiffraction/project/project.py
 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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
class Project(GuardedBase):
    """
    Central API for managing a diffraction data analysis project.

    Provides access to structures, experiments, analysis, and summary.
    """

    # ------------------------------------------------------------------
    # Initialization
    # ------------------------------------------------------------------
    # Class-level sentinel: True while load() is constructing a project.
    _loading: bool = False

    def __init__(
        self,
        name: str = 'untitled_project',
        title: str = 'Untitled Project',
        description: str = '',
    ) -> None:
        super().__init__()

        self._info: ProjectInfo = ProjectInfo(name, title, description)
        self._structures = Structures()
        self._experiments = Experiments()
        self._tabler = TableRenderer.get()
        self._plotter = Plotter()
        self._plotter._set_project(self)
        self._analysis = Analysis(self)
        self._summary = Summary(self)
        self._saved = False
        self._varname = 'project' if type(self)._loading else varname()
        self._verbosity: VerbosityEnum = VerbosityEnum.FULL

    # ------------------------------------------------------------------
    # Dunder methods
    # ------------------------------------------------------------------
    def __str__(self) -> str:
        """Human-readable representation."""
        class_name = self.__class__.__name__
        project_name = self.name
        structures_count = len(self.structures)
        experiments_count = len(self.experiments)
        return (
            f"{class_name} '{project_name}' "
            f'({structures_count} structures, '
            f'{experiments_count} experiments)'
        )

    # ------------------------------------------------------------------
    # Public read-only properties
    # ------------------------------------------------------------------

    @property
    def info(self) -> ProjectInfo:
        """Project metadata container."""
        return self._info

    @property
    def name(self) -> str:
        """Convenience property for the project name."""
        return self._info.name

    @property
    def full_name(self) -> str:
        """
        Return the full project name (alias for :attr:`name`).

        Returns
        -------
        str
            The project name.
        """
        return self.name

    @property
    def structures(self) -> Structures:
        """Collection of structures in the project."""
        return self._structures

    @structures.setter
    @typechecked
    def structures(self, structures: Structures) -> None:
        self._structures = structures

    @property
    def experiments(self) -> Experiments:
        """Collection of experiments in the project."""
        return self._experiments

    @experiments.setter
    @typechecked
    def experiments(self, experiments: Experiments) -> None:
        self._experiments = experiments

    @property
    def plotter(self) -> Plotter:
        """Plotting facade bound to the project."""
        return self._plotter

    @property
    def tabler(self) -> TableRenderer:
        """Tables rendering facade bound to the project."""
        return self._tabler

    @property
    def analysis(self) -> Analysis:
        """Analysis entry-point bound to the project."""
        return self._analysis

    @property
    def summary(self) -> Summary:
        """Summary report builder bound to the project."""
        return self._summary

    @property
    def parameters(self) -> list:
        """Return parameters from all structures and experiments."""
        return self.structures.parameters + self.experiments.parameters

    @property
    def as_cif(self) -> str:
        """Export whole project as CIF text."""
        # Concatenate sections using centralized CIF serializers
        return project_to_cif(self)

    @property
    def verbosity(self) -> str:
        """
        Project-wide console output verbosity.

        Returns
        -------
        str
            One of ``'full'``, ``'short'``, or ``'silent'``.
        """
        return self._verbosity.value

    @verbosity.setter
    def verbosity(self, value: str) -> None:
        """
        Set project-wide console output verbosity.

        Parameters
        ----------
        value : str
            ``'full'`` for multi-line output, ``'short'`` for one-line
            status messages, or ``'silent'`` for no output.
        """
        self._verbosity = VerbosityEnum(value)

    # ------------------------------------------
    #  Project File I/O
    # ------------------------------------------

    @classmethod
    def load(cls, dir_path: str) -> Project:
        """
        Load a project from a saved directory.

        Reads ``project.cif``, ``structures/*.cif``,
        ``experiments/*.cif``, and ``analysis.cif`` from *dir_path* and
        reconstructs the full project state.

        Parameters
        ----------
        dir_path : str
            Path to the project directory previously created by
            :meth:`save_as`.

        Returns
        -------
        Project
            A fully reconstructed project instance.

        Raises
        ------
        FileNotFoundError
            If *dir_path* does not exist.
        """
        from easydiffraction.io.cif.serialize import analysis_from_cif  # noqa: PLC0415
        from easydiffraction.io.cif.serialize import project_info_from_cif  # noqa: PLC0415

        project_path = pathlib.Path(dir_path)
        if not project_path.is_dir():
            msg = f"Project directory not found: '{dir_path}'"
            raise FileNotFoundError(msg)

        # Create a minimal project.
        # Use _loading sentinel to skip varname() inside __init__.
        cls._loading = True
        try:
            project = cls()
        finally:
            cls._loading = False
        project._saved = True

        # 1. Load project info
        project_cif_path = project_path / 'project.cif'
        if project_cif_path.is_file():
            cif_text = project_cif_path.read_text()
            project_info_from_cif(project._info, cif_text)

        project._info.path = project_path

        # 2. Load structures
        structures_dir = project_path / 'structures'
        if structures_dir.is_dir():
            for cif_file in sorted(structures_dir.glob('*.cif')):
                project._structures.add_from_cif_path(str(cif_file))

        # 3. Load experiments
        experiments_dir = project_path / 'experiments'
        if experiments_dir.is_dir():
            for cif_file in sorted(experiments_dir.glob('*.cif')):
                project._experiments.add_from_cif_path(str(cif_file))

        # 4. Load analysis
        #    Check analysis/analysis.cif first (future layout), then
        #    fall back to analysis.cif at root (current layout).
        analysis_cif_path = project_path / 'analysis' / 'analysis.cif'
        if not analysis_cif_path.is_file():
            analysis_cif_path = project_path / 'analysis.cif'
        if analysis_cif_path.is_file():
            cif_text = analysis_cif_path.read_text()
            analysis_from_cif(project._analysis, cif_text)

        # 5. Resolve alias param references
        project._resolve_alias_references()

        # 6. Apply symmetry constraints and update categories
        for structure in project._structures:
            structure._update_categories()

        log.info(f"Project '{project.name}' loaded from '{dir_path}'.")
        return project

    def _resolve_alias_references(self) -> None:
        """
        Resolve alias ``param_unique_name`` strings to live objects.

        After loading structures and experiments from CIF, aliases only
        contain the ``param_unique_name`` string.  This method builds a
        ``{unique_name: param}`` map from all project parameters and
        wires each alias's ``_param_ref``.
        """
        aliases = self._analysis.aliases
        if not aliases._items:
            return

        # Build unique_name → parameter map
        all_params = self._structures.parameters + self._experiments.parameters
        param_map: dict[str, object] = {}
        for p in all_params:
            uname = getattr(p, 'unique_name', None)
            if uname is not None:
                param_map[uname] = p

        for alias in aliases:
            uname = alias.param_unique_name.value
            if uname in param_map:
                alias._set_param(param_map[uname])
            else:
                log.warning(
                    f"Alias '{alias.label.value}' references unknown "
                    f"parameter '{uname}'. Reference not resolved."
                )

    def save(self) -> None:
        """Save the project into the existing project directory."""
        if self._info.path is None:
            log.error('Project path not specified. Use save_as() to define the path first.')
            return

        console.paragraph(f"Saving project 📦 '{self.name}' to")
        console.print(self.info.path.resolve())

        # Apply constraints so dependent parameters are flagged
        # before serialization (constrained params are written
        # without brackets).
        self._analysis._update_categories()

        # Ensure project directory exists
        self._info.path.mkdir(parents=True, exist_ok=True)

        # Save project info
        with (self._info.path / 'project.cif').open('w') as f:
            f.write(self._info.as_cif())
            console.print('├── 📄 project.cif')

        # Save structures
        sm_dir = self._info.path / 'structures'
        sm_dir.mkdir(parents=True, exist_ok=True)
        console.print('├── 📁 structures/')
        for structure in self.structures.values():
            file_name: str = f'{structure.name}.cif'
            file_path = sm_dir / file_name
            with file_path.open('w') as f:
                f.write(structure.as_cif)
                console.print(f'│   └── 📄 {file_name}')

        # Save experiments
        expt_dir = self._info.path / 'experiments'
        expt_dir.mkdir(parents=True, exist_ok=True)
        console.print('├── 📁 experiments/')
        for experiment in self.experiments.values():
            file_name: str = f'{experiment.name}.cif'
            file_path = expt_dir / file_name
            with file_path.open('w') as f:
                f.write(experiment.as_cif)
                console.print(f'│   └── 📄 {file_name}')

        # Save analysis
        analysis_dir = self._info.path / 'analysis'
        analysis_dir.mkdir(parents=True, exist_ok=True)
        with (analysis_dir / 'analysis.cif').open('w') as f:
            f.write(self.analysis.as_cif())
            console.print('├── 📁 analysis/')
            console.print('│   └── 📄 analysis.cif')

        # Save summary
        with (self._info.path / 'summary.cif').open('w') as f:
            f.write(self.summary.as_cif())
            console.print('└── 📄 summary.cif')

        self._info.update_last_modified()
        self._saved = True

    def save_as(
        self,
        dir_path: str,
        *,
        temporary: bool = False,
    ) -> None:
        """Save the project into a new directory."""
        if temporary:
            tmp: str = tempfile.gettempdir()
            dir_path = pathlib.Path(tmp) / dir_path
        self._info.path = dir_path
        self.save()

    def apply_params_from_csv(self, row_index: int) -> None:
        """
        Load a single CSV row and apply its parameters to the project.

        Reads the row at *row_index* from ``analysis/results.csv``,
        overrides parameter values in the live project, and (for
        sequential-fit results where ``file_path`` points to a real
        file) reloads the measured data into the template experiment.

        After calling this method, ``plotter.plot_meas_vs_calc()`` will
        fit for that specific dataset.

        Parameters
        ----------
        row_index : int
            Row index in the CSV file. Supports Python-style negative
            indexing (e.g. ``-1`` for the last row).

        Raises
        ------
        FileNotFoundError
            If ``analysis/results.csv`` does not exist.
        IndexError
            If *row_index* is out of range.
        """
        import pandas as pd  # noqa: PLC0415

        from easydiffraction.analysis.sequential import _META_COLUMNS  # noqa: PLC0415
        from easydiffraction.core.variable import Parameter  # noqa: PLC0415

        if self.info.path is None:
            msg = 'Project has no saved path. Save the project first.'
            raise FileNotFoundError(msg)

        csv_path = pathlib.Path(self.info.path) / 'analysis' / 'results.csv'
        if not csv_path.is_file():
            msg = f"Results CSV not found: '{csv_path}'"
            raise FileNotFoundError(msg)

        df = pd.read_csv(csv_path)
        n_rows = len(df)

        # Support Python-style negative indexing
        if row_index < 0:
            row_index += n_rows

        if row_index < 0 or row_index >= n_rows:
            msg = f'Row index {row_index} out of range (CSV has {n_rows} rows).'
            raise IndexError(msg)

        row = df.iloc[row_index]

        # 1. Reload data if file_path points to a real file
        file_path = row.get('file_path', '')
        if file_path and pathlib.Path(file_path).is_file():
            experiment = next(iter(self.experiments.values()))
            experiment._load_ascii_data_to_experiment(file_path)

        # 2. Override parameter values and uncertainties
        all_params = self.structures.parameters + self.experiments.parameters
        param_map = {
            p.unique_name: p
            for p in all_params
            if isinstance(p, Parameter) and hasattr(p, 'unique_name')
        }
        _apply_csv_row_to_params(row, df.columns, param_map, set(_META_COLUMNS))

        # 4. Force recalculation: data was replaced directly (bypassing
        #    value setters), so the dirty flag may not be set.
        for structure in self.structures:
            structure._need_categories_update = True
        for experiment in self.experiments.values():
            experiment._need_categories_update = True

        log.info(f'Applied parameters from CSV row {row_index} (file: {file_path}).')

__str__()

Human-readable representation.

Source code in src/easydiffraction/project/project.py
 96
 97
 98
 99
100
101
102
103
104
105
106
def __str__(self) -> str:
    """Human-readable representation."""
    class_name = self.__class__.__name__
    project_name = self.name
    structures_count = len(self.structures)
    experiments_count = len(self.experiments)
    return (
        f"{class_name} '{project_name}' "
        f'({structures_count} structures, '
        f'{experiments_count} experiments)'
    )

analysis property

Analysis entry-point bound to the project.

apply_params_from_csv(row_index)

Load a single CSV row and apply its parameters to the project.

Reads the row at row_index from analysis/results.csv, overrides parameter values in the live project, and (for sequential-fit results where file_path points to a real file) reloads the measured data into the template experiment.

After calling this method, plotter.plot_meas_vs_calc() will fit for that specific dataset.

Parameters:

Name Type Description Default
row_index int

Row index in the CSV file. Supports Python-style negative indexing (e.g. -1 for the last row).

required

Raises:

Type Description
FileNotFoundError

If analysis/results.csv does not exist.

IndexError

If row_index is out of range.

Source code in src/easydiffraction/project/project.py
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
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
def apply_params_from_csv(self, row_index: int) -> None:
    """
    Load a single CSV row and apply its parameters to the project.

    Reads the row at *row_index* from ``analysis/results.csv``,
    overrides parameter values in the live project, and (for
    sequential-fit results where ``file_path`` points to a real
    file) reloads the measured data into the template experiment.

    After calling this method, ``plotter.plot_meas_vs_calc()`` will
    fit for that specific dataset.

    Parameters
    ----------
    row_index : int
        Row index in the CSV file. Supports Python-style negative
        indexing (e.g. ``-1`` for the last row).

    Raises
    ------
    FileNotFoundError
        If ``analysis/results.csv`` does not exist.
    IndexError
        If *row_index* is out of range.
    """
    import pandas as pd  # noqa: PLC0415

    from easydiffraction.analysis.sequential import _META_COLUMNS  # noqa: PLC0415
    from easydiffraction.core.variable import Parameter  # noqa: PLC0415

    if self.info.path is None:
        msg = 'Project has no saved path. Save the project first.'
        raise FileNotFoundError(msg)

    csv_path = pathlib.Path(self.info.path) / 'analysis' / 'results.csv'
    if not csv_path.is_file():
        msg = f"Results CSV not found: '{csv_path}'"
        raise FileNotFoundError(msg)

    df = pd.read_csv(csv_path)
    n_rows = len(df)

    # Support Python-style negative indexing
    if row_index < 0:
        row_index += n_rows

    if row_index < 0 or row_index >= n_rows:
        msg = f'Row index {row_index} out of range (CSV has {n_rows} rows).'
        raise IndexError(msg)

    row = df.iloc[row_index]

    # 1. Reload data if file_path points to a real file
    file_path = row.get('file_path', '')
    if file_path and pathlib.Path(file_path).is_file():
        experiment = next(iter(self.experiments.values()))
        experiment._load_ascii_data_to_experiment(file_path)

    # 2. Override parameter values and uncertainties
    all_params = self.structures.parameters + self.experiments.parameters
    param_map = {
        p.unique_name: p
        for p in all_params
        if isinstance(p, Parameter) and hasattr(p, 'unique_name')
    }
    _apply_csv_row_to_params(row, df.columns, param_map, set(_META_COLUMNS))

    # 4. Force recalculation: data was replaced directly (bypassing
    #    value setters), so the dirty flag may not be set.
    for structure in self.structures:
        structure._need_categories_update = True
    for experiment in self.experiments.values():
        experiment._need_categories_update = True

    log.info(f'Applied parameters from CSV row {row_index} (file: {file_path}).')

as_cif property

Export whole project as CIF text.

experiments property writable

Collection of experiments in the project.

full_name property

Return the full project name (alias for :attr:name).

Returns:

Type Description
str

The project name.

info property

Project metadata container.

load(dir_path) classmethod

Load a project from a saved directory.

Reads project.cif, structures/*.cif, experiments/*.cif, and analysis.cif from dir_path and reconstructs the full project state.

Parameters:

Name Type Description Default
dir_path str

Path to the project directory previously created by :meth:save_as.

required

Returns:

Type Description
Project

A fully reconstructed project instance.

Raises:

Type Description
FileNotFoundError

If dir_path does not exist.

Source code in src/easydiffraction/project/project.py
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
@classmethod
def load(cls, dir_path: str) -> Project:
    """
    Load a project from a saved directory.

    Reads ``project.cif``, ``structures/*.cif``,
    ``experiments/*.cif``, and ``analysis.cif`` from *dir_path* and
    reconstructs the full project state.

    Parameters
    ----------
    dir_path : str
        Path to the project directory previously created by
        :meth:`save_as`.

    Returns
    -------
    Project
        A fully reconstructed project instance.

    Raises
    ------
    FileNotFoundError
        If *dir_path* does not exist.
    """
    from easydiffraction.io.cif.serialize import analysis_from_cif  # noqa: PLC0415
    from easydiffraction.io.cif.serialize import project_info_from_cif  # noqa: PLC0415

    project_path = pathlib.Path(dir_path)
    if not project_path.is_dir():
        msg = f"Project directory not found: '{dir_path}'"
        raise FileNotFoundError(msg)

    # Create a minimal project.
    # Use _loading sentinel to skip varname() inside __init__.
    cls._loading = True
    try:
        project = cls()
    finally:
        cls._loading = False
    project._saved = True

    # 1. Load project info
    project_cif_path = project_path / 'project.cif'
    if project_cif_path.is_file():
        cif_text = project_cif_path.read_text()
        project_info_from_cif(project._info, cif_text)

    project._info.path = project_path

    # 2. Load structures
    structures_dir = project_path / 'structures'
    if structures_dir.is_dir():
        for cif_file in sorted(structures_dir.glob('*.cif')):
            project._structures.add_from_cif_path(str(cif_file))

    # 3. Load experiments
    experiments_dir = project_path / 'experiments'
    if experiments_dir.is_dir():
        for cif_file in sorted(experiments_dir.glob('*.cif')):
            project._experiments.add_from_cif_path(str(cif_file))

    # 4. Load analysis
    #    Check analysis/analysis.cif first (future layout), then
    #    fall back to analysis.cif at root (current layout).
    analysis_cif_path = project_path / 'analysis' / 'analysis.cif'
    if not analysis_cif_path.is_file():
        analysis_cif_path = project_path / 'analysis.cif'
    if analysis_cif_path.is_file():
        cif_text = analysis_cif_path.read_text()
        analysis_from_cif(project._analysis, cif_text)

    # 5. Resolve alias param references
    project._resolve_alias_references()

    # 6. Apply symmetry constraints and update categories
    for structure in project._structures:
        structure._update_categories()

    log.info(f"Project '{project.name}' loaded from '{dir_path}'.")
    return project

name property

Convenience property for the project name.

parameters property

Return parameters from all structures and experiments.

plotter property

Plotting facade bound to the project.

save()

Save the project into the existing project directory.

Source code in src/easydiffraction/project/project.py
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
def save(self) -> None:
    """Save the project into the existing project directory."""
    if self._info.path is None:
        log.error('Project path not specified. Use save_as() to define the path first.')
        return

    console.paragraph(f"Saving project 📦 '{self.name}' to")
    console.print(self.info.path.resolve())

    # Apply constraints so dependent parameters are flagged
    # before serialization (constrained params are written
    # without brackets).
    self._analysis._update_categories()

    # Ensure project directory exists
    self._info.path.mkdir(parents=True, exist_ok=True)

    # Save project info
    with (self._info.path / 'project.cif').open('w') as f:
        f.write(self._info.as_cif())
        console.print('├── 📄 project.cif')

    # Save structures
    sm_dir = self._info.path / 'structures'
    sm_dir.mkdir(parents=True, exist_ok=True)
    console.print('├── 📁 structures/')
    for structure in self.structures.values():
        file_name: str = f'{structure.name}.cif'
        file_path = sm_dir / file_name
        with file_path.open('w') as f:
            f.write(structure.as_cif)
            console.print(f'│   └── 📄 {file_name}')

    # Save experiments
    expt_dir = self._info.path / 'experiments'
    expt_dir.mkdir(parents=True, exist_ok=True)
    console.print('├── 📁 experiments/')
    for experiment in self.experiments.values():
        file_name: str = f'{experiment.name}.cif'
        file_path = expt_dir / file_name
        with file_path.open('w') as f:
            f.write(experiment.as_cif)
            console.print(f'│   └── 📄 {file_name}')

    # Save analysis
    analysis_dir = self._info.path / 'analysis'
    analysis_dir.mkdir(parents=True, exist_ok=True)
    with (analysis_dir / 'analysis.cif').open('w') as f:
        f.write(self.analysis.as_cif())
        console.print('├── 📁 analysis/')
        console.print('│   └── 📄 analysis.cif')

    # Save summary
    with (self._info.path / 'summary.cif').open('w') as f:
        f.write(self.summary.as_cif())
        console.print('└── 📄 summary.cif')

    self._info.update_last_modified()
    self._saved = True

save_as(dir_path, *, temporary=False)

Save the project into a new directory.

Source code in src/easydiffraction/project/project.py
387
388
389
390
391
392
393
394
395
396
397
398
def save_as(
    self,
    dir_path: str,
    *,
    temporary: bool = False,
) -> None:
    """Save the project into a new directory."""
    if temporary:
        tmp: str = tempfile.gettempdir()
        dir_path = pathlib.Path(tmp) / dir_path
    self._info.path = dir_path
    self.save()

structures property writable

Collection of structures in the project.

summary property

Summary report builder bound to the project.

tabler property

Tables rendering facade bound to the project.

verbosity property writable

Project-wide console output verbosity.

Returns:

Type Description
str

One of 'full', 'short', or 'silent'.

project_info

Project metadata container used by Project.

ProjectInfo

Bases: GuardedBase

Store project metadata: name, title, description, paths.

Source code in src/easydiffraction/project/project_info.py
 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
class ProjectInfo(GuardedBase):
    """Store project metadata: name, title, description, paths."""

    def __init__(
        self,
        name: str = 'untitled_project',
        title: str = 'Untitled Project',
        description: str = '',
    ) -> None:
        super().__init__()

        self._name = name
        self._title = title
        self._description = description
        self._path: pathlib.Path | None = None  # pathlib.Path.cwd()
        self._created: datetime.datetime = datetime.datetime.now()
        self._last_modified: datetime.datetime = datetime.datetime.now()

    @property
    def name(self) -> str:
        """Return the project name."""
        return self._name

    @name.setter
    def name(self, value: str) -> None:
        """
        Set the project name.

        Parameters
        ----------
        value : str
            New project name.
        """
        self._name = value

    @property
    def unique_name(self) -> str:
        """Unique name for GuardedBase diagnostics."""
        return self.name

    @property
    def title(self) -> str:
        """Return the project title."""
        return self._title

    @title.setter
    def title(self, value: str) -> None:
        """
        Set the project title.

        Parameters
        ----------
        value : str
            New project title.
        """
        self._title = value

    @property
    def description(self) -> str:
        """Return sanitized description with single spaces."""
        return ' '.join(self._description.split())

    @description.setter
    def description(self, value: str) -> None:
        """
        Set the project description (whitespace normalized).

        Parameters
        ----------
        value : str
            New description text.
        """
        self._description = ' '.join(value.split())

    @property
    def path(self) -> pathlib.Path | None:
        """Return the project path as a Path object."""
        return self._path

    @path.setter
    def path(self, value: object) -> None:
        """
        Set the project directory path.

        Parameters
        ----------
        value : object
            New path as a :class:`str` or :class:`pathlib.Path`.
        """
        # Accept str or Path; normalize to Path
        self._path = pathlib.Path(value)

    @property
    def created(self) -> datetime.datetime:
        """Return the creation timestamp."""
        return self._created

    @property
    def last_modified(self) -> datetime.datetime:
        """Return the last modified timestamp."""
        return self._last_modified

    def update_last_modified(self) -> None:
        """Update the last modified timestamp."""
        self._last_modified = datetime.datetime.now()

    def parameters(self) -> None:
        """List parameters (not implemented)."""

    # TODO: Consider moving to io.cif.serialize
    def as_cif(self) -> str:
        """Export project metadata to CIF."""
        return project_info_to_cif(self)

    # TODO: Consider moving to io.cif.serialize
    def show_as_cif(self) -> None:
        """Pretty-print CIF via shared utilities."""
        paragraph_title: str = f"Project 📦 '{self.name}' info as CIF"
        cif_text: str = self.as_cif()
        console.paragraph(paragraph_title)
        render_cif(cif_text)

as_cif()

Export project metadata to CIF.

Source code in src/easydiffraction/project/project_info.py
124
125
126
def as_cif(self) -> str:
    """Export project metadata to CIF."""
    return project_info_to_cif(self)

created property

Return the creation timestamp.

description property writable

Return sanitized description with single spaces.

last_modified property

Return the last modified timestamp.

name property writable

Return the project name.

parameters()

List parameters (not implemented).

Source code in src/easydiffraction/project/project_info.py
120
121
def parameters(self) -> None:
    """List parameters (not implemented)."""

path property writable

Return the project path as a Path object.

show_as_cif()

Pretty-print CIF via shared utilities.

Source code in src/easydiffraction/project/project_info.py
129
130
131
132
133
134
def show_as_cif(self) -> None:
    """Pretty-print CIF via shared utilities."""
    paragraph_title: str = f"Project 📦 '{self.name}' info as CIF"
    cif_text: str = self.as_cif()
    console.paragraph(paragraph_title)
    render_cif(cif_text)

title property writable

Return the project title.

unique_name property

Unique name for GuardedBase diagnostics.

update_last_modified()

Update the last modified timestamp.

Source code in src/easydiffraction/project/project_info.py
116
117
118
def update_last_modified(self) -> None:
    """Update the last modified timestamp."""
    self._last_modified = datetime.datetime.now()