odibi 2.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. odibi/__init__.py +32 -0
  2. odibi/__main__.py +8 -0
  3. odibi/catalog.py +3011 -0
  4. odibi/cli/__init__.py +11 -0
  5. odibi/cli/__main__.py +6 -0
  6. odibi/cli/catalog.py +553 -0
  7. odibi/cli/deploy.py +69 -0
  8. odibi/cli/doctor.py +161 -0
  9. odibi/cli/export.py +66 -0
  10. odibi/cli/graph.py +150 -0
  11. odibi/cli/init_pipeline.py +242 -0
  12. odibi/cli/lineage.py +259 -0
  13. odibi/cli/main.py +215 -0
  14. odibi/cli/run.py +98 -0
  15. odibi/cli/schema.py +208 -0
  16. odibi/cli/secrets.py +232 -0
  17. odibi/cli/story.py +379 -0
  18. odibi/cli/system.py +132 -0
  19. odibi/cli/test.py +286 -0
  20. odibi/cli/ui.py +31 -0
  21. odibi/cli/validate.py +39 -0
  22. odibi/config.py +3541 -0
  23. odibi/connections/__init__.py +9 -0
  24. odibi/connections/azure_adls.py +499 -0
  25. odibi/connections/azure_sql.py +709 -0
  26. odibi/connections/base.py +28 -0
  27. odibi/connections/factory.py +322 -0
  28. odibi/connections/http.py +78 -0
  29. odibi/connections/local.py +119 -0
  30. odibi/connections/local_dbfs.py +61 -0
  31. odibi/constants.py +17 -0
  32. odibi/context.py +528 -0
  33. odibi/diagnostics/__init__.py +12 -0
  34. odibi/diagnostics/delta.py +520 -0
  35. odibi/diagnostics/diff.py +169 -0
  36. odibi/diagnostics/manager.py +171 -0
  37. odibi/engine/__init__.py +20 -0
  38. odibi/engine/base.py +334 -0
  39. odibi/engine/pandas_engine.py +2178 -0
  40. odibi/engine/polars_engine.py +1114 -0
  41. odibi/engine/registry.py +54 -0
  42. odibi/engine/spark_engine.py +2362 -0
  43. odibi/enums.py +7 -0
  44. odibi/exceptions.py +297 -0
  45. odibi/graph.py +426 -0
  46. odibi/introspect.py +1214 -0
  47. odibi/lineage.py +511 -0
  48. odibi/node.py +3341 -0
  49. odibi/orchestration/__init__.py +0 -0
  50. odibi/orchestration/airflow.py +90 -0
  51. odibi/orchestration/dagster.py +77 -0
  52. odibi/patterns/__init__.py +24 -0
  53. odibi/patterns/aggregation.py +599 -0
  54. odibi/patterns/base.py +94 -0
  55. odibi/patterns/date_dimension.py +423 -0
  56. odibi/patterns/dimension.py +696 -0
  57. odibi/patterns/fact.py +748 -0
  58. odibi/patterns/merge.py +128 -0
  59. odibi/patterns/scd2.py +148 -0
  60. odibi/pipeline.py +2382 -0
  61. odibi/plugins.py +80 -0
  62. odibi/project.py +581 -0
  63. odibi/references.py +151 -0
  64. odibi/registry.py +246 -0
  65. odibi/semantics/__init__.py +71 -0
  66. odibi/semantics/materialize.py +392 -0
  67. odibi/semantics/metrics.py +361 -0
  68. odibi/semantics/query.py +743 -0
  69. odibi/semantics/runner.py +430 -0
  70. odibi/semantics/story.py +507 -0
  71. odibi/semantics/views.py +432 -0
  72. odibi/state/__init__.py +1203 -0
  73. odibi/story/__init__.py +55 -0
  74. odibi/story/doc_story.py +554 -0
  75. odibi/story/generator.py +1431 -0
  76. odibi/story/lineage.py +1043 -0
  77. odibi/story/lineage_utils.py +324 -0
  78. odibi/story/metadata.py +608 -0
  79. odibi/story/renderers.py +453 -0
  80. odibi/story/templates/run_story.html +2520 -0
  81. odibi/story/themes.py +216 -0
  82. odibi/testing/__init__.py +13 -0
  83. odibi/testing/assertions.py +75 -0
  84. odibi/testing/fixtures.py +85 -0
  85. odibi/testing/source_pool.py +277 -0
  86. odibi/transformers/__init__.py +122 -0
  87. odibi/transformers/advanced.py +1472 -0
  88. odibi/transformers/delete_detection.py +610 -0
  89. odibi/transformers/manufacturing.py +1029 -0
  90. odibi/transformers/merge_transformer.py +778 -0
  91. odibi/transformers/relational.py +675 -0
  92. odibi/transformers/scd.py +579 -0
  93. odibi/transformers/sql_core.py +1356 -0
  94. odibi/transformers/validation.py +165 -0
  95. odibi/ui/__init__.py +0 -0
  96. odibi/ui/app.py +195 -0
  97. odibi/utils/__init__.py +66 -0
  98. odibi/utils/alerting.py +667 -0
  99. odibi/utils/config_loader.py +343 -0
  100. odibi/utils/console.py +231 -0
  101. odibi/utils/content_hash.py +202 -0
  102. odibi/utils/duration.py +43 -0
  103. odibi/utils/encoding.py +102 -0
  104. odibi/utils/extensions.py +28 -0
  105. odibi/utils/hashing.py +61 -0
  106. odibi/utils/logging.py +203 -0
  107. odibi/utils/logging_context.py +740 -0
  108. odibi/utils/progress.py +429 -0
  109. odibi/utils/setup_helpers.py +302 -0
  110. odibi/utils/telemetry.py +140 -0
  111. odibi/validation/__init__.py +62 -0
  112. odibi/validation/engine.py +765 -0
  113. odibi/validation/explanation_linter.py +155 -0
  114. odibi/validation/fk.py +547 -0
  115. odibi/validation/gate.py +252 -0
  116. odibi/validation/quarantine.py +605 -0
  117. odibi/writers/__init__.py +15 -0
  118. odibi/writers/sql_server_writer.py +2081 -0
  119. odibi-2.5.0.dist-info/METADATA +255 -0
  120. odibi-2.5.0.dist-info/RECORD +124 -0
  121. odibi-2.5.0.dist-info/WHEEL +5 -0
  122. odibi-2.5.0.dist-info/entry_points.txt +2 -0
  123. odibi-2.5.0.dist-info/licenses/LICENSE +190 -0
  124. odibi-2.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,507 @@
1
+ """
2
+ Semantic Story Generation
3
+ =========================
4
+
5
+ Generate execution stories for semantic layer view creation.
6
+
7
+ Stories capture:
8
+ - View execution metadata (success/failure, duration, SQL)
9
+ - Graph data for lineage visualization
10
+ - HTML and JSON outputs for documentation
11
+ """
12
+
13
+ import json
14
+ from dataclasses import asdict, dataclass, field
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ from typing import Any, Callable, Dict, List, Optional
18
+
19
+ from odibi.semantics.metrics import SemanticLayerConfig
20
+ from odibi.semantics.views import ViewExecutionResult, ViewGenerator
21
+ from odibi.utils.logging_context import get_logging_context
22
+
23
+
24
+ @dataclass
25
+ class ViewExecutionMetadata:
26
+ """
27
+ Metadata for a single view execution.
28
+
29
+ Captures execution details for documentation and debugging.
30
+ """
31
+
32
+ view_name: str
33
+ source_table: str
34
+ status: str # "success", "failed"
35
+ duration: float
36
+ sql_generated: str
37
+ sql_file_path: Optional[str] = None
38
+ error_message: Optional[str] = None
39
+ row_count: Optional[int] = None
40
+ metrics_included: List[str] = field(default_factory=list)
41
+ dimensions_included: List[str] = field(default_factory=list)
42
+
43
+
44
+ @dataclass
45
+ class GraphNode:
46
+ """Node in the lineage graph."""
47
+
48
+ id: str
49
+ type: str # "table", "view"
50
+ layer: str # "gold", "semantic"
51
+
52
+
53
+ @dataclass
54
+ class GraphEdge:
55
+ """Edge in the lineage graph."""
56
+
57
+ from_node: str
58
+ to_node: str
59
+
60
+
61
+ @dataclass
62
+ class SemanticStoryMetadata:
63
+ """
64
+ Complete metadata for a semantic layer execution story.
65
+
66
+ Contains all information needed to generate story outputs.
67
+ """
68
+
69
+ name: str
70
+ started_at: str
71
+ completed_at: str
72
+ duration: float
73
+ views: List[ViewExecutionMetadata]
74
+ views_created: int
75
+ views_failed: int
76
+ sql_files_saved: List[str]
77
+ graph_data: Dict[str, Any]
78
+ pipeline_layer: str = "semantic"
79
+
80
+ def to_dict(self) -> Dict[str, Any]:
81
+ """Convert to dictionary for JSON serialization."""
82
+ return {
83
+ "name": self.name,
84
+ "pipeline_layer": self.pipeline_layer,
85
+ "started_at": self.started_at,
86
+ "completed_at": self.completed_at,
87
+ "duration": self.duration,
88
+ "views": [asdict(v) for v in self.views],
89
+ "views_created": self.views_created,
90
+ "views_failed": self.views_failed,
91
+ "sql_files_saved": self.sql_files_saved,
92
+ "graph_data": self.graph_data,
93
+ }
94
+
95
+
96
+ class SemanticStoryGenerator:
97
+ """
98
+ Generate execution stories for semantic layer operations.
99
+
100
+ Creates both JSON and HTML outputs documenting:
101
+ - View creation status and timing
102
+ - Generated SQL for each view
103
+ - Lineage graph connecting source tables to views
104
+ """
105
+
106
+ def __init__(
107
+ self,
108
+ config: SemanticLayerConfig,
109
+ name: str = "semantic_layer",
110
+ output_path: str = "stories/",
111
+ storage_options: Optional[Dict[str, Any]] = None,
112
+ ):
113
+ """
114
+ Initialize story generator.
115
+
116
+ Args:
117
+ config: Semantic layer configuration
118
+ name: Name for this semantic layer execution
119
+ output_path: Directory for story output
120
+ storage_options: Credentials for remote storage
121
+ """
122
+ self.config = config
123
+ self.name = name
124
+ self.output_path_str = output_path
125
+ self.is_remote = "://" in output_path
126
+ self.storage_options = storage_options or {}
127
+
128
+ self._view_generator = ViewGenerator(config)
129
+ self._metadata: Optional[SemanticStoryMetadata] = None
130
+
131
+ if not self.is_remote:
132
+ self.output_path = Path(output_path)
133
+ else:
134
+ self.output_path = None
135
+
136
+ def execute_with_story(
137
+ self,
138
+ execute_sql: Callable[[str], None],
139
+ save_sql_to: Optional[str] = None,
140
+ write_file: Optional[Callable[[str, str], None]] = None,
141
+ ) -> SemanticStoryMetadata:
142
+ """
143
+ Execute all views and generate a story.
144
+
145
+ Args:
146
+ execute_sql: Callable that executes SQL against the database
147
+ save_sql_to: Optional path to save SQL files
148
+ write_file: Optional callable to write files
149
+
150
+ Returns:
151
+ SemanticStoryMetadata with execution details
152
+ """
153
+ ctx = get_logging_context()
154
+ started_at = datetime.now()
155
+ ctx.info("Starting semantic layer execution with story", name=self.name)
156
+
157
+ execution_result = self._view_generator.execute_all_views(
158
+ execute_sql=execute_sql,
159
+ save_sql_to=save_sql_to,
160
+ write_file=write_file,
161
+ )
162
+
163
+ completed_at = datetime.now()
164
+ duration = (completed_at - started_at).total_seconds()
165
+
166
+ view_metadata = self._build_view_metadata(execution_result)
167
+ graph_data = self._build_graph_data(execution_result)
168
+
169
+ self._metadata = SemanticStoryMetadata(
170
+ name=self.name,
171
+ started_at=started_at.isoformat(),
172
+ completed_at=completed_at.isoformat(),
173
+ duration=duration,
174
+ views=view_metadata,
175
+ views_created=len(execution_result.views_created),
176
+ views_failed=len(execution_result.errors),
177
+ sql_files_saved=execution_result.sql_files_saved,
178
+ graph_data=graph_data,
179
+ )
180
+
181
+ ctx.info(
182
+ "Semantic layer execution complete",
183
+ views_created=self._metadata.views_created,
184
+ views_failed=self._metadata.views_failed,
185
+ duration=duration,
186
+ )
187
+
188
+ return self._metadata
189
+
190
+ def _build_view_metadata(
191
+ self, execution_result: ViewExecutionResult
192
+ ) -> List[ViewExecutionMetadata]:
193
+ """Build metadata for each view execution."""
194
+ view_metadata = []
195
+
196
+ for result in execution_result.results:
197
+ view_config = self._view_generator.get_view(result.name)
198
+ source_table = ""
199
+ if view_config:
200
+ try:
201
+ source_table = self._view_generator._get_source_table(view_config)
202
+ except ValueError:
203
+ source_table = "unknown"
204
+
205
+ metadata = ViewExecutionMetadata(
206
+ view_name=result.name,
207
+ source_table=source_table,
208
+ status="success" if result.success else "failed",
209
+ duration=0.0,
210
+ sql_generated=result.sql,
211
+ sql_file_path=result.sql_file_path,
212
+ error_message=result.error,
213
+ metrics_included=view_config.metrics if view_config else [],
214
+ dimensions_included=view_config.dimensions if view_config else [],
215
+ )
216
+ view_metadata.append(metadata)
217
+
218
+ return view_metadata
219
+
220
+ def _build_graph_data(self, execution_result: ViewExecutionResult) -> Dict[str, Any]:
221
+ """Build lineage graph data."""
222
+ nodes = []
223
+ edges = []
224
+ seen_sources = set()
225
+
226
+ for result in execution_result.results:
227
+ view_config = self._view_generator.get_view(result.name)
228
+ if not view_config:
229
+ continue
230
+
231
+ nodes.append(
232
+ {
233
+ "id": result.name,
234
+ "type": "view",
235
+ "layer": "semantic",
236
+ }
237
+ )
238
+
239
+ try:
240
+ source_table = self._view_generator._get_source_table(view_config)
241
+ if source_table not in seen_sources:
242
+ # Source tables are inputs - mark as type "source"
243
+ # Layer will be inferred by lineage stitcher from matching nodes
244
+ nodes.append(
245
+ {
246
+ "id": source_table,
247
+ "type": "source",
248
+ }
249
+ )
250
+ seen_sources.add(source_table)
251
+
252
+ edges.append(
253
+ {
254
+ "from": source_table,
255
+ "to": result.name,
256
+ }
257
+ )
258
+ except ValueError:
259
+ pass
260
+
261
+ return {"nodes": nodes, "edges": edges}
262
+
263
+ def save_story(
264
+ self,
265
+ write_file: Optional[Callable[[str, str], None]] = None,
266
+ ) -> Dict[str, str]:
267
+ """
268
+ Save story as JSON and HTML files.
269
+
270
+ Args:
271
+ write_file: Optional callable to write files (for remote storage)
272
+
273
+ Returns:
274
+ Dict with paths to saved files
275
+ """
276
+ if not self._metadata:
277
+ raise ValueError("No story metadata. Call execute_with_story first.")
278
+
279
+ ctx = get_logging_context()
280
+ now = datetime.now()
281
+ date_str = now.strftime("%Y-%m-%d")
282
+ time_str = now.strftime("run_%H-%M-%S")
283
+
284
+ if self.is_remote:
285
+ base_path = f"{self.output_path_str.rstrip('/')}/{self.name}/{date_str}"
286
+ else:
287
+ base_path = self.output_path / self.name / date_str
288
+ base_path = str(base_path)
289
+
290
+ json_path = f"{base_path}/{time_str}.json"
291
+ html_path = f"{base_path}/{time_str}.html"
292
+
293
+ json_content = self.render_json()
294
+ html_content = self.render_html()
295
+
296
+ if write_file:
297
+ write_file(json_path, json_content)
298
+ write_file(html_path, html_content)
299
+ elif not self.is_remote:
300
+ Path(base_path).mkdir(parents=True, exist_ok=True)
301
+ Path(json_path).write_text(json_content, encoding="utf-8")
302
+ Path(html_path).write_text(html_content, encoding="utf-8")
303
+
304
+ ctx.info("Story saved", json_path=json_path, html_path=html_path)
305
+
306
+ return {"json": json_path, "html": html_path}
307
+
308
+ def render_json(self) -> str:
309
+ """Render story as JSON string."""
310
+ if not self._metadata:
311
+ raise ValueError("No story metadata. Call execute_with_story first.")
312
+ return json.dumps(self._metadata.to_dict(), indent=2)
313
+
314
+ def render_html(self) -> str:
315
+ """Render story as HTML string."""
316
+ if not self._metadata:
317
+ raise ValueError("No story metadata. Call execute_with_story first.")
318
+
319
+ meta = self._metadata
320
+
321
+ status_class = "success" if meta.views_failed == 0 else "warning"
322
+ status_text = "Success" if meta.views_failed == 0 else "Partial Failure"
323
+
324
+ views_html = []
325
+ for view in meta.views:
326
+ view_status = "✅" if view.status == "success" else "❌"
327
+ error_html = ""
328
+ if view.error_message:
329
+ error_html = f'<div class="error">{view.error_message}</div>'
330
+
331
+ sql_html = f"<pre><code>{self._escape_html(view.sql_generated)}</code></pre>"
332
+
333
+ views_html.append(
334
+ f"""
335
+ <div class="view-card {view.status}">
336
+ <h3>{view_status} {view.view_name}</h3>
337
+ <div class="view-details">
338
+ <p><strong>Source:</strong> {view.source_table}</p>
339
+ <p><strong>Metrics:</strong> {", ".join(view.metrics_included)}</p>
340
+ <p><strong>Dimensions:</strong> {", ".join(view.dimensions_included)}</p>
341
+ {error_html}
342
+ </div>
343
+ <details>
344
+ <summary>Generated SQL</summary>
345
+ {sql_html}
346
+ </details>
347
+ </div>
348
+ """
349
+ )
350
+
351
+ mermaid_code = self._generate_mermaid_diagram()
352
+
353
+ html = f"""<!DOCTYPE html>
354
+ <html lang="en">
355
+ <head>
356
+ <meta charset="UTF-8">
357
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
358
+ <title>Semantic Layer Story: {meta.name}</title>
359
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
360
+ <style>
361
+ :root {{
362
+ --primary: #2563eb;
363
+ --success: #16a34a;
364
+ --warning: #dc2626;
365
+ --bg: #f8fafc;
366
+ --card-bg: #ffffff;
367
+ --text: #1e293b;
368
+ --border: #e2e8f0;
369
+ }}
370
+ body {{
371
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
372
+ background: var(--bg);
373
+ color: var(--text);
374
+ margin: 0;
375
+ padding: 20px;
376
+ line-height: 1.6;
377
+ }}
378
+ .container {{ max-width: 1200px; margin: 0 auto; }}
379
+ h1 {{ color: var(--primary); margin-bottom: 0; }}
380
+ .subtitle {{ color: #64748b; margin-top: 5px; }}
381
+ .summary {{
382
+ display: grid;
383
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
384
+ gap: 15px;
385
+ margin: 20px 0;
386
+ }}
387
+ .stat {{
388
+ background: var(--card-bg);
389
+ padding: 15px;
390
+ border-radius: 8px;
391
+ border: 1px solid var(--border);
392
+ text-align: center;
393
+ }}
394
+ .stat-value {{ font-size: 24px; font-weight: bold; color: var(--primary); }}
395
+ .stat-label {{ font-size: 12px; color: #64748b; text-transform: uppercase; }}
396
+ .status-badge {{
397
+ display: inline-block;
398
+ padding: 4px 12px;
399
+ border-radius: 20px;
400
+ font-size: 14px;
401
+ font-weight: 500;
402
+ }}
403
+ .status-badge.success {{ background: #dcfce7; color: var(--success); }}
404
+ .status-badge.warning {{ background: #fee2e2; color: var(--warning); }}
405
+ .view-card {{
406
+ background: var(--card-bg);
407
+ border: 1px solid var(--border);
408
+ border-radius: 8px;
409
+ padding: 15px;
410
+ margin: 15px 0;
411
+ }}
412
+ .view-card.success {{ border-left: 4px solid var(--success); }}
413
+ .view-card.failed {{ border-left: 4px solid var(--warning); }}
414
+ .view-card h3 {{ margin: 0 0 10px 0; }}
415
+ .view-details p {{ margin: 5px 0; }}
416
+ .error {{ background: #fee2e2; color: var(--warning); padding: 10px; border-radius: 4px; margin-top: 10px; }}
417
+ details {{ margin-top: 10px; }}
418
+ summary {{ cursor: pointer; font-weight: 500; color: var(--primary); }}
419
+ pre {{
420
+ background: #1e293b;
421
+ color: #e2e8f0;
422
+ padding: 15px;
423
+ border-radius: 8px;
424
+ overflow-x: auto;
425
+ font-size: 13px;
426
+ }}
427
+ .lineage {{ background: var(--card-bg); padding: 20px; border-radius: 8px; border: 1px solid var(--border); margin: 20px 0; }}
428
+ .mermaid {{ text-align: center; }}
429
+ </style>
430
+ </head>
431
+ <body>
432
+ <div class="container">
433
+ <h1>🔷 {meta.name}</h1>
434
+ <p class="subtitle">Semantic Layer Execution Story</p>
435
+
436
+ <div class="summary">
437
+ <div class="stat">
438
+ <div class="stat-value">{meta.views_created}</div>
439
+ <div class="stat-label">Views Created</div>
440
+ </div>
441
+ <div class="stat">
442
+ <div class="stat-value">{meta.views_failed}</div>
443
+ <div class="stat-label">Failed</div>
444
+ </div>
445
+ <div class="stat">
446
+ <div class="stat-value">{meta.duration:.2f}s</div>
447
+ <div class="stat-label">Duration</div>
448
+ </div>
449
+ <div class="stat">
450
+ <span class="status-badge {status_class}">{status_text}</span>
451
+ </div>
452
+ </div>
453
+
454
+ <h2>📊 Lineage</h2>
455
+ <div class="lineage">
456
+ <div class="mermaid">
457
+ {mermaid_code}
458
+ </div>
459
+ </div>
460
+
461
+ <h2>📋 Views</h2>
462
+ {"".join(views_html)}
463
+
464
+ <footer style="text-align: center; color: #94a3b8; margin-top: 40px; font-size: 12px;">
465
+ Generated: {meta.completed_at} | Duration: {meta.duration:.2f}s
466
+ </footer>
467
+ </div>
468
+ <script>mermaid.initialize({{startOnLoad: true, theme: 'neutral'}});</script>
469
+ </body>
470
+ </html>"""
471
+
472
+ return html
473
+
474
+ def _generate_mermaid_diagram(self) -> str:
475
+ """Generate Mermaid flowchart from graph data."""
476
+ if not self._metadata:
477
+ return "graph LR\n A[No Data]"
478
+
479
+ lines = ["graph LR"]
480
+
481
+ for node in self._metadata.graph_data.get("nodes", []):
482
+ node_id = node["id"].replace(".", "_").replace("-", "_")
483
+ if node["type"] == "table":
484
+ lines.append(f' {node_id}[("{node["id"]}")]')
485
+ else:
486
+ lines.append(f' {node_id}["{node["id"]}"]')
487
+
488
+ for edge in self._metadata.graph_data.get("edges", []):
489
+ from_id = edge["from"].replace(".", "_").replace("-", "_")
490
+ to_id = edge["to"].replace(".", "_").replace("-", "_")
491
+ lines.append(f" {from_id} --> {to_id}")
492
+
493
+ return "\n".join(lines)
494
+
495
+ def _escape_html(self, text: str) -> str:
496
+ """Escape HTML special characters."""
497
+ return (
498
+ text.replace("&", "&amp;")
499
+ .replace("<", "&lt;")
500
+ .replace(">", "&gt;")
501
+ .replace('"', "&quot;")
502
+ )
503
+
504
+ @property
505
+ def metadata(self) -> Optional[SemanticStoryMetadata]:
506
+ """Get the last generated metadata."""
507
+ return self._metadata