Skip to content

App

cibmangotree.app

Modules:

Name Description
analysis_context
gui_progress_reporter
logger

Application-wide logging system for Mango Tango CLI.

Classes

Modules

analysis_context

Classes:

Name Description
AnalysisContext
Classes
AnalysisContext

Bases: BaseModel

Methods:

Name Description
run_worker

Static method to run analysis in a separate process.

Source code in src/cibmangotree/app/analysis_context.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
class AnalysisContext(BaseModel):
    model_config = ConfigDict(arbitrary_types_allowed=True)

    app_context: AppContext
    project_context: ProjectContext
    model: AnalysisModel
    is_deleted: bool = False

    @property
    def display_name(self):
        return self.model.display_name

    @property
    def id(self):
        return self.model.analysis_id

    @property
    def analyzer_id(self):
        return self.model.primary_analyzer_id

    @property
    def analyzer_spec(self):
        analyzer = self.app_context.suite.get_primary_analyzer(self.analyzer_id)
        assert analyzer, f"Analyzer `{self.analyzer_id}` not found"
        return analyzer

    @property
    def column_mapping(self):
        return self.model.column_mapping

    @property
    def create_time(self):
        return self.model.create_time()

    @cached_property
    def backfilled_param_values(self):
        return backfill_param_values(self.model.param_values, self.analyzer_spec)

    @property
    def is_draft(self):
        return self.model.is_draft

    def rename(self, new_name: str):
        self.model.display_name = new_name
        self.app_context.storage.save_analysis(self.model)

    def delete(self):
        self.is_deleted = True
        self.app_context.storage.delete_analysis(self.model)

    @staticmethod
    def run_worker(
        analysis_model: AnalysisModel,
        analyzer_id: str,
        column_mapping: dict[str, str],
        input_columns_data: dict[str, tuple[str, str]],
        secondary_analyzer_ids: list[str],
        storage: Storage,
        queue: Queue,
        cancel_event: Event,
    ) -> AnalysisModel:
        """
        Static method to run analysis in a separate process.

        This method is designed to be called via ui.run.cpu_bound().

        See: https://github.com/zauberzeug/nicegui/discussions/1046#discussioncomment-6225227

        Args:
            analysis_model: The analysis model
            analyzer_id: The primary analyzer ID
            column_mapping: Mapping from analyzer column names to user column names
            input_columns_data: Dict of analyzer_column_name -> (user_column_name, semantic_name)
            secondary_analyzer_ids: List of secondary analyzer IDs
            storage: Storage instance
            queue: Multiprocessing queue for progress messages
            cancel_event: Multiprocessing event to signal cancellation

        Returns:
            The updated analysis model (is_draft=False on success)
        """
        from cibmangotree.analyzers import suite
        from cibmangotree.preprocessing.series_semantic import all_semantics

        from .gui_progress_reporter import GUIProgressReporter

        analyzer_spec = suite.get_primary_analyzer(analyzer_id)
        if analyzer_spec is None:
            queue.put(
                AnalysisQueueMessage(
                    type="error",
                    message=f"Analyzer '{analyzer_id}' not found",
                ).model_dump()
            )
            return analysis_model

        secondary_analyzers = [
            sec
            for sec_id in secondary_analyzer_ids
            if (sec := suite.get_secondary_analyzer_by_id(analyzer_id, sec_id))
            is not None
        ]

        semantic_lookup = {s.semantic_name: s for s in all_semantics}

        def send_message(msg: AnalysisQueueMessage):
            queue.put(msg.model_dump())

        def check_cancelled() -> bool:
            return cancel_event.is_set()

        def make_progress_reporter(
            analyzer_id: str, analyzer_name: str
        ) -> Callable[[str], ProgressReporterProtocol]:
            def factory(step_name: str) -> ProgressReporterProtocol:
                return GUIProgressReporter(
                    queue=queue,
                    analyzer_id=analyzer_id,
                    analyzer_name=analyzer_name,
                    step_name=step_name,
                )

            return factory

        try:
            with TemporaryDirectory() as temp_dir:
                if check_cancelled():
                    send_message(AnalysisQueueMessage(type="cancelled"))
                    return analysis_model

                send_message(
                    AnalysisQueueMessage(
                        type="analyzer_start",
                        analyzer_id=analyzer_spec.id,
                        analyzer_name=analyzer_spec.name,
                    )
                )

                input_columns = {
                    analyzer_col_name: InputColumnProvider(
                        user_column_name=user_col_name,
                        semantic=semantic_lookup[semantic_name],
                    )
                    for analyzer_col_name, (
                        user_col_name,
                        semantic_name,
                    ) in input_columns_data.items()
                }

                analyzer_context = PrimaryAnalyzerContext(
                    analysis=analysis_model,
                    analyzer=analyzer_spec,
                    store=storage,
                    temp_dir=temp_dir,
                    input_columns=input_columns,
                    progress_reporter=make_progress_reporter(
                        analyzer_spec.id, analyzer_spec.name
                    ),
                )
                analyzer_context.prepare()

                if check_cancelled():
                    send_message(AnalysisQueueMessage(type="cancelled"))
                    return analysis_model

                analyzer_spec.entry_point(analyzer_context)

                if check_cancelled():
                    send_message(AnalysisQueueMessage(type="cancelled"))
                    return analysis_model

                send_message(
                    AnalysisQueueMessage(
                        type="analyzer_finish",
                        analyzer_id=analyzer_spec.id,
                        analyzer_name=analyzer_spec.name,
                    )
                )

            for secondary_spec in secondary_analyzers:
                if check_cancelled():
                    send_message(AnalysisQueueMessage(type="cancelled"))
                    return analysis_model

                send_message(
                    AnalysisQueueMessage(
                        type="analyzer_start",
                        analyzer_id=secondary_spec.id,
                        analyzer_name=secondary_spec.name,
                    )
                )

                with TemporaryDirectory() as temp_dir:
                    secondary_context = SecondaryAnalyzerContext(
                        analysis=analysis_model,
                        secondary_analyzer=secondary_spec,
                        temp_dir=temp_dir,
                        store=storage,
                        progress_reporter=make_progress_reporter(
                            secondary_spec.id, secondary_spec.name
                        ),
                    )
                    secondary_context.prepare()

                    if check_cancelled():
                        send_message(AnalysisQueueMessage(type="cancelled"))
                        return analysis_model

                    secondary_spec.entry_point(secondary_context)

                send_message(
                    AnalysisQueueMessage(
                        type="analyzer_finish",
                        analyzer_id=secondary_spec.id,
                        analyzer_name=secondary_spec.name,
                    )
                )

            analysis_model.is_draft = False
            storage.save_analysis(analysis_model)
            send_message(AnalysisQueueMessage(type="complete"))
            return analysis_model

        except Exception as e:
            send_message(
                AnalysisQueueMessage(
                    type="error",
                    message=str(e),
                )
            )
            raise

    @property
    def export_root_path(self):
        return self.app_context.storage._get_project_exports_root_path(self.model)

    def export_directory_exists(self) -> bool:
        return os.path.exists(self.export_root_path)

    def get_all_exportable_outputs(self):
        from .analysis_output_context import AnalysisOutputContext

        return [
            *(
                AnalysisOutputContext(
                    app_context=self.app_context,
                    analysis_context=self,
                    secondary_spec=None,
                    output_spec=output,
                )
                for output in self.analyzer_spec.outputs
                if not output.internal
            ),
            *(
                AnalysisOutputContext(
                    app_context=self.app_context,
                    analysis_context=self,
                    secondary_spec=secondary,
                    output_spec=output,
                )
                for secondary_id in self.app_context.storage.list_secondary_analyses(
                    self.model
                )
                if (
                    secondary := self.app_context.suite.get_secondary_analyzer_by_id(
                        self.analyzer_id, secondary_id
                    )
                )
                is not None
                for output in secondary.outputs
                if not output.internal
            ),
        ]
Methods:
run_worker(analysis_model, analyzer_id, column_mapping, input_columns_data, secondary_analyzer_ids, storage, queue, cancel_event) staticmethod

Static method to run analysis in a separate process.

This method is designed to be called via ui.run.cpu_bound().

See: https://github.com/zauberzeug/nicegui/discussions/1046#discussioncomment-6225227

Parameters:

Name Type Description Default
analysis_model AnalysisModel

The analysis model

required
analyzer_id str

The primary analyzer ID

required
column_mapping dict[str, str]

Mapping from analyzer column names to user column names

required
input_columns_data dict[str, tuple[str, str]]

Dict of analyzer_column_name -> (user_column_name, semantic_name)

required
secondary_analyzer_ids list[str]

List of secondary analyzer IDs

required
storage Storage

Storage instance

required
queue Queue

Multiprocessing queue for progress messages

required
cancel_event Event

Multiprocessing event to signal cancellation

required

Returns:

Type Description
AnalysisModel

The updated analysis model (is_draft=False on success)

Source code in src/cibmangotree/app/analysis_context.py
 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
@staticmethod
def run_worker(
    analysis_model: AnalysisModel,
    analyzer_id: str,
    column_mapping: dict[str, str],
    input_columns_data: dict[str, tuple[str, str]],
    secondary_analyzer_ids: list[str],
    storage: Storage,
    queue: Queue,
    cancel_event: Event,
) -> AnalysisModel:
    """
    Static method to run analysis in a separate process.

    This method is designed to be called via ui.run.cpu_bound().

    See: https://github.com/zauberzeug/nicegui/discussions/1046#discussioncomment-6225227

    Args:
        analysis_model: The analysis model
        analyzer_id: The primary analyzer ID
        column_mapping: Mapping from analyzer column names to user column names
        input_columns_data: Dict of analyzer_column_name -> (user_column_name, semantic_name)
        secondary_analyzer_ids: List of secondary analyzer IDs
        storage: Storage instance
        queue: Multiprocessing queue for progress messages
        cancel_event: Multiprocessing event to signal cancellation

    Returns:
        The updated analysis model (is_draft=False on success)
    """
    from cibmangotree.analyzers import suite
    from cibmangotree.preprocessing.series_semantic import all_semantics

    from .gui_progress_reporter import GUIProgressReporter

    analyzer_spec = suite.get_primary_analyzer(analyzer_id)
    if analyzer_spec is None:
        queue.put(
            AnalysisQueueMessage(
                type="error",
                message=f"Analyzer '{analyzer_id}' not found",
            ).model_dump()
        )
        return analysis_model

    secondary_analyzers = [
        sec
        for sec_id in secondary_analyzer_ids
        if (sec := suite.get_secondary_analyzer_by_id(analyzer_id, sec_id))
        is not None
    ]

    semantic_lookup = {s.semantic_name: s for s in all_semantics}

    def send_message(msg: AnalysisQueueMessage):
        queue.put(msg.model_dump())

    def check_cancelled() -> bool:
        return cancel_event.is_set()

    def make_progress_reporter(
        analyzer_id: str, analyzer_name: str
    ) -> Callable[[str], ProgressReporterProtocol]:
        def factory(step_name: str) -> ProgressReporterProtocol:
            return GUIProgressReporter(
                queue=queue,
                analyzer_id=analyzer_id,
                analyzer_name=analyzer_name,
                step_name=step_name,
            )

        return factory

    try:
        with TemporaryDirectory() as temp_dir:
            if check_cancelled():
                send_message(AnalysisQueueMessage(type="cancelled"))
                return analysis_model

            send_message(
                AnalysisQueueMessage(
                    type="analyzer_start",
                    analyzer_id=analyzer_spec.id,
                    analyzer_name=analyzer_spec.name,
                )
            )

            input_columns = {
                analyzer_col_name: InputColumnProvider(
                    user_column_name=user_col_name,
                    semantic=semantic_lookup[semantic_name],
                )
                for analyzer_col_name, (
                    user_col_name,
                    semantic_name,
                ) in input_columns_data.items()
            }

            analyzer_context = PrimaryAnalyzerContext(
                analysis=analysis_model,
                analyzer=analyzer_spec,
                store=storage,
                temp_dir=temp_dir,
                input_columns=input_columns,
                progress_reporter=make_progress_reporter(
                    analyzer_spec.id, analyzer_spec.name
                ),
            )
            analyzer_context.prepare()

            if check_cancelled():
                send_message(AnalysisQueueMessage(type="cancelled"))
                return analysis_model

            analyzer_spec.entry_point(analyzer_context)

            if check_cancelled():
                send_message(AnalysisQueueMessage(type="cancelled"))
                return analysis_model

            send_message(
                AnalysisQueueMessage(
                    type="analyzer_finish",
                    analyzer_id=analyzer_spec.id,
                    analyzer_name=analyzer_spec.name,
                )
            )

        for secondary_spec in secondary_analyzers:
            if check_cancelled():
                send_message(AnalysisQueueMessage(type="cancelled"))
                return analysis_model

            send_message(
                AnalysisQueueMessage(
                    type="analyzer_start",
                    analyzer_id=secondary_spec.id,
                    analyzer_name=secondary_spec.name,
                )
            )

            with TemporaryDirectory() as temp_dir:
                secondary_context = SecondaryAnalyzerContext(
                    analysis=analysis_model,
                    secondary_analyzer=secondary_spec,
                    temp_dir=temp_dir,
                    store=storage,
                    progress_reporter=make_progress_reporter(
                        secondary_spec.id, secondary_spec.name
                    ),
                )
                secondary_context.prepare()

                if check_cancelled():
                    send_message(AnalysisQueueMessage(type="cancelled"))
                    return analysis_model

                secondary_spec.entry_point(secondary_context)

            send_message(
                AnalysisQueueMessage(
                    type="analyzer_finish",
                    analyzer_id=secondary_spec.id,
                    analyzer_name=secondary_spec.name,
                )
            )

        analysis_model.is_draft = False
        storage.save_analysis(analysis_model)
        send_message(AnalysisQueueMessage(type="complete"))
        return analysis_model

    except Exception as e:
        send_message(
            AnalysisQueueMessage(
                type="error",
                message=str(e),
            )
        )
        raise

gui_progress_reporter

Classes:

Name Description
GUIProgressReporter

Progress reporter that sends messages to GUI queue.

Classes
GUIProgressReporter

Progress reporter that sends messages to GUI queue.

Source code in src/cibmangotree/app/gui_progress_reporter.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class GUIProgressReporter:
    """Progress reporter that sends messages to GUI queue."""

    def __init__(
        self,
        queue: Queue,
        analyzer_id: str,
        analyzer_name: str,
        step_name: str,
    ):
        self.queue = queue
        self.analyzer_id = analyzer_id
        self.analyzer_name = analyzer_name
        self.step_name = step_name

    def update(self, value: float):
        self.queue.put(
            AnalysisQueueMessage(
                type="step_progress",
                analyzer_id=self.analyzer_id,
                step_name=self.step_name,
                step_progress=value,
            ).model_dump()
        )

    def finish(self, done_text: str = "Done!"):
        self.queue.put(
            AnalysisQueueMessage(
                type="step_finish",
                analyzer_id=self.analyzer_id,
                analyzer_name=self.analyzer_name,
                step_name=self.step_name,
            ).model_dump()
        )

    def __enter__(self):
        self.queue.put(
            AnalysisQueueMessage(
                type="step_start",
                analyzer_id=self.analyzer_id,
                analyzer_name=self.analyzer_name,
                step_name=self.step_name,
            ).model_dump()
        )
        return self

    def __exit__(self, *args):
        self.finish()

logger

Application-wide logging system for Mango Tango CLI.

Provides structured JSON logging with: - Console output (ERROR and CRITICAL levels only) to stderr - File output (INFO and above) with automatic rotation - Configurable log levels via CLI flag

Classes:

Name Description
ContextEnrichmentFilter

Filter that enriches log records with contextual information.

Functions:

Name Description
get_logger

Get a logger instance for the specified module.

setup_logging

Configure application-wide logging with structured JSON output.

Classes
ContextEnrichmentFilter

Bases: Filter

Filter that enriches log records with contextual information.

Adds: - process_id: Current process ID - thread_id: Current thread ID - app_version: Application version (if available)

Source code in src/cibmangotree/app/logger.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ContextEnrichmentFilter(logging.Filter):
    """
    Filter that enriches log records with contextual information.

    Adds:
    - process_id: Current process ID
    - thread_id: Current thread ID
    - app_version: Application version (if available)
    """

    def __init__(self, app_version: str = "unknown"):
        super().__init__()
        self.app_version = app_version
        self.process_id = os.getpid()

    def filter(self, record: logging.LogRecord) -> bool:
        # Add contextual information to the log record
        record.process_id = self.process_id
        record.thread_id = threading.get_ident()
        record.app_version = self.app_version
        return True
Functions:
get_logger(name)

Get a logger instance for the specified module.

Parameters:

Name Type Description Default
name str

Logger name (typically name)

required

Returns:

Type Description
Logger

Configured logger instance

Source code in src/cibmangotree/app/logger.py
137
138
139
140
141
142
143
144
145
146
147
def get_logger(name: str) -> logging.Logger:
    """
    Get a logger instance for the specified module.

    Args:
        name: Logger name (typically __name__)

    Returns:
        Configured logger instance
    """
    return logging.getLogger(name)
setup_logging(log_file_path, level=logging.INFO, app_version='unknown')

Configure application-wide logging with structured JSON output.

Parameters:

Name Type Description Default
log_file_path Path

Path to the log file

required
level int

Minimum logging level (default: logging.INFO)

INFO
app_version str

Application version to include in logs

'unknown'
Source code in src/cibmangotree/app/logger.py
 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
def setup_logging(
    log_file_path: Path, level: int = logging.INFO, app_version: str = "unknown"
) -> None:
    """
    Configure application-wide logging with structured JSON output.

    Args:
        log_file_path: Path to the log file
        level: Minimum logging level (default: logging.INFO)
        app_version: Application version to include in logs
    """
    # Ensure the log directory exists
    log_file_path.parent.mkdir(parents=True, exist_ok=True)

    # Logging configuration dictionary
    config: Dict[str, Any] = {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "json": {
                "()": "pythonjsonlogger.json.JsonFormatter",
                "format": "%(asctime)s %(name)s %(levelname)s %(message)s %(process_id)s %(thread_id)s %(app_version)s",
                "rename_fields": {"levelname": "level", "asctime": "timestamp"},
            }
        },
        "filters": {
            "context_enrichment": {
                "()": ContextEnrichmentFilter,
                "app_version": app_version,
            }
        },
        "handlers": {
            "console": {
                "class": "logging.StreamHandler",
                "level": "ERROR",
                "formatter": "json",
                "filters": ["context_enrichment"],
                "stream": sys.stderr,
            },
            "file": {
                "class": "logging.handlers.RotatingFileHandler",
                "level": level,
                "formatter": "json",
                "filters": ["context_enrichment"],
                "filename": str(log_file_path),
                "maxBytes": 10485760,  # 10MB
                "backupCount": 5,
                "encoding": "utf-8",
            },
        },
        "root": {"level": level, "handlers": ["console", "file"]},
        "loggers": {
            # Third-party library loggers - keep them quieter by default
            "urllib3": {"level": "WARNING", "propagate": True},
            "requests": {"level": "WARNING", "propagate": True},
            "dash": {"level": "WARNING", "propagate": True},
            "plotly": {"level": "WARNING", "propagate": True},
            "shiny": {"level": "WARNING", "propagate": True},
            "uvicorn": {"level": "WARNING", "propagate": True},
            "starlette": {"level": "WARNING", "propagate": True},
            # Application loggers - inherit from root level
            "mangotango": {"level": level, "propagate": True},
            "app": {"level": level, "propagate": True},
            "analyzers": {"level": level, "propagate": True},
            "components": {"level": level, "propagate": True},
            "storage": {"level": level, "propagate": True},
            "importing": {"level": level, "propagate": True},
        },
    }

    # Apply the configuration
    logging.config.dictConfig(config)

    # Set up global exception handler
    def handle_exception(exc_type, exc_value, exc_traceback):
        """Handle uncaught exceptions by logging them."""
        if issubclass(exc_type, KeyboardInterrupt):
            # Let KeyboardInterrupt be handled normally
            sys.__excepthook__(exc_type, exc_value, exc_traceback)
            return

        logger = logging.getLogger("uncaught_exception")
        logger.critical(
            "Uncaught exception",
            exc_info=(exc_type, exc_value, exc_traceback),
            extra={
                "exception_type": exc_type.__name__,
                "exception_message": str(exc_value),
            },
        )

    # Install the global exception handler
    sys.excepthook = handle_exception