0
# Metadata & Schema
1
2
Cluster metadata access, schema introspection, and topology information with complete keyspace, table, and column metadata. The metadata system provides comprehensive access to cluster topology and schema information.
3
4
## Capabilities
5
6
### Cluster Metadata
7
8
Primary metadata container providing access to cluster topology and schema information.
9
10
```python { .api }
11
class Metadata:
12
def __init__(self):
13
"""Container for cluster metadata including keyspaces, tables, and hosts."""
14
15
def get_keyspace(self, keyspace):
16
"""
17
Get metadata for a specific keyspace.
18
19
Parameters:
20
- keyspace (str): Name of the keyspace
21
22
Returns:
23
KeyspaceMetadata: Keyspace metadata or None if not found
24
"""
25
26
def get_table(self, keyspace, table):
27
"""
28
Get metadata for a specific table.
29
30
Parameters:
31
- keyspace (str): Name of the keyspace
32
- table (str): Name of the table
33
34
Returns:
35
TableMetadata: Table metadata or None if not found
36
"""
37
38
def get_user_type(self, keyspace, user_type):
39
"""
40
Get metadata for a user-defined type.
41
42
Parameters:
43
- keyspace (str): Name of the keyspace
44
- user_type (str): Name of the user-defined type
45
46
Returns:
47
UserType: User-defined type metadata or None if not found
48
"""
49
50
def get_function(self, keyspace, function, signature):
51
"""
52
Get metadata for a user-defined function.
53
54
Parameters:
55
- keyspace (str): Name of the keyspace
56
- function (str): Name of the function
57
- signature (list): Function signature (argument types)
58
59
Returns:
60
Function: Function metadata or None if not found
61
"""
62
63
def get_aggregate(self, keyspace, aggregate, signature):
64
"""
65
Get metadata for a user-defined aggregate.
66
67
Parameters:
68
- keyspace (str): Name of the keyspace
69
- aggregate (str): Name of the aggregate
70
- signature (list): Aggregate signature (argument types)
71
72
Returns:
73
Aggregate: Aggregate metadata or None if not found
74
"""
75
76
def get_host(self, address):
77
"""
78
Get host metadata by address.
79
80
Parameters:
81
- address (str): Host IP address
82
83
Returns:
84
Host: Host metadata or None if not found
85
"""
86
87
def all_hosts(self):
88
"""
89
Get all known hosts in the cluster.
90
91
Returns:
92
list: List of Host objects
93
"""
94
95
def rebuild_schema(self, keyspace=None):
96
"""
97
Rebuild schema metadata from the cluster.
98
99
Parameters:
100
- keyspace (str): Specific keyspace to rebuild, or None for all
101
"""
102
103
@property
104
def keyspaces(self):
105
"""dict: Dictionary mapping keyspace names to KeyspaceMetadata objects"""
106
107
@property
108
def cluster_name(self):
109
"""str: Name of the cluster"""
110
111
@property
112
def partitioner(self):
113
"""str: Partitioner used by the cluster"""
114
115
@property
116
def token_map(self):
117
"""TokenMap: Token mapping for the cluster"""
118
119
@property
120
def hosts(self):
121
"""dict: Dictionary mapping host addresses to Host objects"""
122
```
123
124
### Keyspace Metadata
125
126
Metadata for Cassandra keyspaces including replication configuration and contained objects.
127
128
```python { .api }
129
class KeyspaceMetadata:
130
def __init__(self, name, durable_writes, strategy_class, strategy_options):
131
"""
132
Metadata for a Cassandra keyspace.
133
134
Parameters:
135
- name (str): Keyspace name
136
- durable_writes (bool): Whether durable writes are enabled
137
- strategy_class (str): Replication strategy class
138
- strategy_options (dict): Replication strategy options
139
"""
140
141
@property
142
def name(self):
143
"""str: Name of the keyspace"""
144
145
@property
146
def durable_writes(self):
147
"""bool: Whether durable writes are enabled"""
148
149
@property
150
def replication_strategy(self):
151
"""ReplicationStrategy: Replication strategy instance"""
152
153
@property
154
def tables(self):
155
"""dict: Dictionary mapping table names to TableMetadata objects"""
156
157
@property
158
def user_types(self):
159
"""dict: Dictionary mapping type names to UserType objects"""
160
161
@property
162
def functions(self):
163
"""dict: Dictionary mapping function signatures to Function objects"""
164
165
@property
166
def aggregates(self):
167
"""dict: Dictionary mapping aggregate signatures to Aggregate objects"""
168
169
def export_as_string(self):
170
"""
171
Export keyspace definition as CQL string.
172
173
Returns:
174
str: CQL CREATE KEYSPACE statement
175
"""
176
```
177
178
### Table Metadata
179
180
Comprehensive metadata for Cassandra tables including columns, indexes, and options.
181
182
```python { .api }
183
class TableMetadata:
184
def __init__(self, keyspace, name, columns, partition_key, clustering_key, options, triggers, indexes):
185
"""
186
Metadata for a Cassandra table.
187
188
Parameters:
189
- keyspace (str): Keyspace name
190
- name (str): Table name
191
- columns (list): List of ColumnMetadata objects
192
- partition_key (list): Partition key columns
193
- clustering_key (list): Clustering key columns
194
- options (dict): Table options
195
- triggers (dict): Table triggers
196
- indexes (dict): Secondary indexes
197
"""
198
199
@property
200
def keyspace_name(self):
201
"""str: Name of the keyspace containing this table"""
202
203
@property
204
def name(self):
205
"""str: Name of the table"""
206
207
@property
208
def columns(self):
209
"""dict: Dictionary mapping column names to ColumnMetadata objects"""
210
211
@property
212
def partition_key(self):
213
"""list: List of ColumnMetadata objects forming the partition key"""
214
215
@property
216
def clustering_key(self):
217
"""list: List of ColumnMetadata objects forming the clustering key"""
218
219
@property
220
def primary_key(self):
221
"""list: Complete primary key (partition key + clustering key)"""
222
223
@property
224
def options(self):
225
"""dict: Table options (compaction, compression, etc.)"""
226
227
@property
228
def triggers(self):
229
"""dict: Dictionary mapping trigger names to TriggerMetadata objects"""
230
231
@property
232
def indexes(self):
233
"""dict: Dictionary mapping index names to IndexMetadata objects"""
234
235
def export_as_string(self):
236
"""
237
Export table definition as CQL string.
238
239
Returns:
240
str: CQL CREATE TABLE statement
241
"""
242
243
def is_cql_compatible(self):
244
"""
245
Check if table is compatible with CQL.
246
247
Returns:
248
bool: True if table can be used with CQL
249
"""
250
```
251
252
### Column Metadata
253
254
Metadata for individual table columns including type and constraints.
255
256
```python { .api }
257
class ColumnMetadata:
258
def __init__(self, table, name, cql_type, is_static=False, is_reversed=False):
259
"""
260
Metadata for a table column.
261
262
Parameters:
263
- table (TableMetadata): Parent table
264
- name (str): Column name
265
- cql_type: CQL type of the column
266
- is_static (bool): Whether column is static
267
- is_reversed (bool): Whether column has reversed order
268
"""
269
270
@property
271
def table(self):
272
"""TableMetadata: Parent table metadata"""
273
274
@property
275
def name(self):
276
"""str: Name of the column"""
277
278
@property
279
def cql_type(self):
280
"""_CassandraType: CQL type of the column"""
281
282
@property
283
def is_static(self):
284
"""bool: Whether this is a static column"""
285
286
@property
287
def is_reversed(self):
288
"""bool: Whether this column has reversed clustering order"""
289
290
@property
291
def is_partition_key(self):
292
"""bool: Whether this column is part of the partition key"""
293
294
@property
295
def is_clustering_key(self):
296
"""bool: Whether this column is part of the clustering key"""
297
298
@property
299
def is_primary_key(self):
300
"""bool: Whether this column is part of the primary key"""
301
```
302
303
### Index Metadata
304
305
Metadata for secondary indexes on tables.
306
307
```python { .api }
308
class IndexMetadata:
309
def __init__(self, table, name, kind, options):
310
"""
311
Metadata for a secondary index.
312
313
Parameters:
314
- table (TableMetadata): Parent table
315
- name (str): Index name
316
- kind (str): Index type/kind
317
- options (dict): Index options
318
"""
319
320
@property
321
def table(self):
322
"""TableMetadata: Parent table metadata"""
323
324
@property
325
def name(self):
326
"""str: Name of the index"""
327
328
@property
329
def kind(self):
330
"""str: Type of index (COMPOSITES, KEYS, CUSTOM)"""
331
332
@property
333
def options(self):
334
"""dict: Index configuration options"""
335
336
def export_as_string(self):
337
"""
338
Export index definition as CQL string.
339
340
Returns:
341
str: CQL CREATE INDEX statement
342
"""
343
```
344
345
### User-Defined Types
346
347
Metadata for user-defined composite types.
348
349
```python { .api }
350
class UserType:
351
def __init__(self, keyspace, name, field_names, field_types):
352
"""
353
Metadata for a user-defined type.
354
355
Parameters:
356
- keyspace (str): Keyspace name
357
- name (str): Type name
358
- field_names (list): Field names
359
- field_types (list): Field types
360
"""
361
362
@property
363
def keyspace(self):
364
"""str: Keyspace containing this type"""
365
366
@property
367
def name(self):
368
"""str: Name of the type"""
369
370
@property
371
def field_names(self):
372
"""list: Names of fields in this type"""
373
374
@property
375
def field_types(self):
376
"""list: Types of fields in this type"""
377
378
def export_as_string(self):
379
"""
380
Export type definition as CQL string.
381
382
Returns:
383
str: CQL CREATE TYPE statement
384
"""
385
```
386
387
### User-Defined Functions
388
389
Metadata for user-defined functions and aggregates.
390
391
```python { .api }
392
class Function:
393
def __init__(self, keyspace, name, argument_names, argument_types, body, called_on_null_input, language, return_type):
394
"""
395
Metadata for a user-defined function.
396
397
Parameters:
398
- keyspace (str): Keyspace name
399
- name (str): Function name
400
- argument_names (list): Parameter names
401
- argument_types (list): Parameter types
402
- body (str): Function body code
403
- called_on_null_input (bool): Whether function is called on null input
404
- language (str): Implementation language
405
- return_type: Return type
406
"""
407
408
@property
409
def keyspace_name(self):
410
"""str: Keyspace containing this function"""
411
412
@property
413
def name(self):
414
"""str: Name of the function"""
415
416
@property
417
def argument_names(self):
418
"""list: Names of function parameters"""
419
420
@property
421
def argument_types(self):
422
"""list: Types of function parameters"""
423
424
@property
425
def signature(self):
426
"""str: Function signature string"""
427
428
@property
429
def body(self):
430
"""str: Function implementation code"""
431
432
@property
433
def called_on_null_input(self):
434
"""bool: Whether function is called when input is null"""
435
436
@property
437
def language(self):
438
"""str: Implementation language (java, javascript, etc.)"""
439
440
@property
441
def return_type(self):
442
"""_CassandraType: Return type of the function"""
443
444
class Aggregate:
445
def __init__(self, keyspace, name, argument_types, state_func, state_type, final_func, initial_condition, return_type):
446
"""
447
Metadata for a user-defined aggregate.
448
449
Parameters:
450
- keyspace (str): Keyspace name
451
- name (str): Aggregate name
452
- argument_types (list): Input types
453
- state_func (str): State function name
454
- state_type: State type
455
- final_func (str): Final function name
456
- initial_condition: Initial state value
457
- return_type: Return type
458
"""
459
460
@property
461
def keyspace_name(self):
462
"""str: Keyspace containing this aggregate"""
463
464
@property
465
def name(self):
466
"""str: Name of the aggregate"""
467
468
@property
469
def argument_types(self):
470
"""list: Types of aggregate input"""
471
472
@property
473
def signature(self):
474
"""str: Aggregate signature string"""
475
476
@property
477
def state_func(self):
478
"""str: Name of the state function"""
479
480
@property
481
def state_type(self):
482
"""_CassandraType: Type of the state value"""
483
484
@property
485
def final_func(self):
486
"""str: Name of the final function"""
487
488
@property
489
def initial_condition(self):
490
"""Initial state value"""
491
492
@property
493
def return_type(self):
494
"""_CassandraType: Return type of the aggregate"""
495
```
496
497
### Replication Strategies
498
499
Replication strategy implementations for keyspaces.
500
501
```python { .api }
502
class ReplicationStrategy:
503
"""Base class for replication strategies."""
504
505
@property
506
def name(self):
507
"""str: Name of the replication strategy"""
508
509
@property
510
def options(self):
511
"""dict: Strategy configuration options"""
512
513
class SimpleStrategy(ReplicationStrategy):
514
def __init__(self, replication_factor):
515
"""
516
Simple replication strategy for single-datacenter clusters.
517
518
Parameters:
519
- replication_factor (int): Number of replicas
520
"""
521
522
@property
523
def replication_factor(self):
524
"""int: Number of replicas"""
525
526
class NetworkTopologyStrategy(ReplicationStrategy):
527
def __init__(self, dc_replication_factors):
528
"""
529
Network topology replication strategy for multi-datacenter clusters.
530
531
Parameters:
532
- dc_replication_factors (dict): Replication factors by datacenter
533
"""
534
535
@property
536
def dc_replication_factors(self):
537
"""dict: Replication factors by datacenter name"""
538
539
class LocalStrategy(ReplicationStrategy):
540
def __init__(self):
541
"""Local replication strategy (for system keyspaces)."""
542
```
543
544
### Token Management
545
546
Token ring and routing information for the cluster.
547
548
```python { .api }
549
class TokenMap:
550
def __init__(self, token_to_host_owner, tokens_to_host_owners, ring):
551
"""
552
Token mapping for cluster routing.
553
554
Parameters:
555
- token_to_host_owner (dict): Mapping of tokens to primary hosts
556
- tokens_to_host_owners (dict): Mapping of tokens to replica sets
557
- ring (list): Ordered list of tokens in the ring
558
"""
559
560
def get_replicas(self, keyspace, token):
561
"""
562
Get replica hosts for a token in a keyspace.
563
564
Parameters:
565
- keyspace (str): Keyspace name
566
- token: Token to look up
567
568
Returns:
569
set: Set of Host objects that are replicas for the token
570
"""
571
572
@property
573
def ring(self):
574
"""list: Ordered list of tokens in the cluster ring"""
575
576
class Token:
577
"""Base class for partition tokens."""
578
579
@property
580
def value(self):
581
"""Token value"""
582
583
class Murmur3Token(Token):
584
def __init__(self, value):
585
"""
586
Murmur3 hash token (default partitioner).
587
588
Parameters:
589
- value (int): Token value
590
"""
591
592
class MD5Token(Token):
593
def __init__(self, value):
594
"""
595
MD5 hash token (legacy partitioner).
596
597
Parameters:
598
- value (int): Token value
599
"""
600
601
class BytesToken(Token):
602
def __init__(self, value):
603
"""
604
Bytes-based token (byte order partitioner).
605
606
Parameters:
607
- value (bytes): Token value
608
"""
609
```
610
611
### Metadata Utilities
612
613
Utility functions for working with CQL identifiers and values.
614
615
```python { .api }
616
def protect_names(names):
617
"""
618
Quote CQL identifiers that need protection.
619
620
Parameters:
621
- names (list): List of CQL identifiers
622
623
Returns:
624
list: List of quoted identifiers
625
"""
626
627
def protect_name(name):
628
"""
629
Quote a CQL identifier if it needs protection.
630
631
Parameters:
632
- name (str): CQL identifier
633
634
Returns:
635
str: Quoted identifier if needed, otherwise original name
636
"""
637
638
def protect_value(value):
639
"""
640
Quote a CQL value for safe inclusion in queries.
641
642
Parameters:
643
- value: Value to quote
644
645
Returns:
646
str: Quoted value suitable for CQL
647
"""
648
649
def is_valid_name(name):
650
"""
651
Check if a name is a valid unquoted CQL identifier.
652
653
Parameters:
654
- name (str): Identifier to check
655
656
Returns:
657
bool: True if the name is valid unquoted
658
"""
659
660
def escape_name(name):
661
"""
662
Escape a CQL identifier for use in quoted form.
663
664
Parameters:
665
- name (str): Identifier to escape
666
667
Returns:
668
str: Escaped identifier
669
"""
670
```
671
672
## Usage Examples
673
674
### Exploring Cluster Schema
675
676
```python
677
# Get cluster metadata
678
metadata = cluster.metadata
679
680
print(f"Cluster name: {metadata.cluster_name}")
681
print(f"Partitioner: {metadata.partitioner}")
682
print(f"Total hosts: {len(metadata.all_hosts())}")
683
684
# List all keyspaces
685
print("\nKeyspaces:")
686
for keyspace_name in metadata.keyspaces:
687
keyspace = metadata.keyspaces[keyspace_name]
688
print(f" {keyspace_name}: {keyspace.replication_strategy}")
689
690
# Explore a specific keyspace
691
keyspace = metadata.get_keyspace('my_app')
692
if keyspace:
693
print(f"\nKeyspace '{keyspace.name}':")
694
print(f" Durable writes: {keyspace.durable_writes}")
695
print(f" Tables: {list(keyspace.tables.keys())}")
696
print(f" User types: {list(keyspace.user_types.keys())}")
697
print(f" Functions: {len(keyspace.functions)}")
698
```
699
700
### Examining Table Structure
701
702
```python
703
# Get table metadata
704
table = metadata.get_table('my_app', 'users')
705
if table:
706
print(f"Table: {table.keyspace_name}.{table.name}")
707
708
# Show partition key
709
print(f"Partition key: {[col.name for col in table.partition_key]}")
710
711
# Show clustering key
712
if table.clustering_key:
713
print(f"Clustering key: {[col.name for col in table.clustering_key]}")
714
715
# Show all columns
716
print("\nColumns:")
717
for col_name, column in table.columns.items():
718
key_type = ""
719
if column.is_partition_key:
720
key_type = " (partition key)"
721
elif column.is_clustering_key:
722
key_type = " (clustering key)"
723
elif column.is_static:
724
key_type = " (static)"
725
726
print(f" {col_name}: {column.cql_type}{key_type}")
727
728
# Show indexes
729
if table.indexes:
730
print("\nIndexes:")
731
for index_name, index in table.indexes.items():
732
print(f" {index_name}: {index.kind}")
733
734
# Show table options
735
print(f"\nTable options: {table.options}")
736
737
# Export as CQL
738
print(f"\nCQL Definition:\n{table.export_as_string()}")
739
```
740
741
### Working with User-Defined Types
742
743
```python
744
# Get UDT metadata
745
address_type = metadata.get_user_type('my_app', 'address')
746
if address_type:
747
print(f"User type: {address_type.keyspace}.{address_type.name}")
748
print("Fields:")
749
for field_name, field_type in zip(address_type.field_names, address_type.field_types):
750
print(f" {field_name}: {field_type}")
751
752
print(f"\nCQL Definition:\n{address_type.export_as_string()}")
753
754
# Find tables using this UDT
755
print(f"\nTables using {address_type.name}:")
756
keyspace = metadata.get_keyspace('my_app')
757
for table_name, table in keyspace.tables.items():
758
for col_name, column in table.columns.items():
759
if hasattr(column.cql_type, 'typename') and column.cql_type.typename == 'address':
760
print(f" {table_name}.{col_name}")
761
```
762
763
### Analyzing Cluster Topology
764
765
```python
766
# Examine hosts and datacenters
767
print("Cluster topology:")
768
hosts_by_dc = {}
769
for host in metadata.all_hosts():
770
dc = host.datacenter or 'unknown'
771
if dc not in hosts_by_dc:
772
hosts_by_dc[dc] = []
773
hosts_by_dc[dc].append(host)
774
775
for dc, hosts in hosts_by_dc.items():
776
print(f"\nDatacenter: {dc}")
777
for host in hosts:
778
status = "UP" if host.is_up else "DOWN"
779
print(f" {host.address} ({host.rack}): {status} - {host.release_version}")
780
781
# Examine token distribution
782
token_map = metadata.token_map
783
if token_map:
784
print(f"\nToken ring has {len(token_map.ring)} tokens")
785
786
# Show token ownership for a keyspace
787
keyspace_name = 'my_app'
788
if keyspace_name in metadata.keyspaces:
789
print(f"\nReplica distribution for keyspace '{keyspace_name}':")
790
sample_tokens = token_map.ring[:5] # Sample first 5 tokens
791
for token in sample_tokens:
792
replicas = token_map.get_replicas(keyspace_name, token)
793
replica_addresses = [host.address for host in replicas]
794
print(f" Token {token.value}: {replica_addresses}")
795
```
796
797
### Schema Evolution Tracking
798
799
```python
800
def compare_schemas(old_metadata, new_metadata, keyspace_name):
801
"""Compare two metadata snapshots to detect schema changes."""
802
803
old_ks = old_metadata.get_keyspace(keyspace_name)
804
new_ks = new_metadata.get_keyspace(keyspace_name)
805
806
if not old_ks or not new_ks:
807
print("Keyspace not found in one of the metadata snapshots")
808
return
809
810
# Compare tables
811
old_tables = set(old_ks.tables.keys())
812
new_tables = set(new_ks.tables.keys())
813
814
added_tables = new_tables - old_tables
815
removed_tables = old_tables - new_tables
816
common_tables = old_tables & new_tables
817
818
if added_tables:
819
print(f"Added tables: {added_tables}")
820
if removed_tables:
821
print(f"Removed tables: {removed_tables}")
822
823
# Compare columns in common tables
824
for table_name in common_tables:
825
old_table = old_ks.tables[table_name]
826
new_table = new_ks.tables[table_name]
827
828
old_columns = set(old_table.columns.keys())
829
new_columns = set(new_table.columns.keys())
830
831
added_columns = new_columns - old_columns
832
removed_columns = old_columns - new_columns
833
834
if added_columns:
835
print(f"Table {table_name} - Added columns: {added_columns}")
836
if removed_columns:
837
print(f"Table {table_name} - Removed columns: {removed_columns}")
838
839
# Usage
840
old_metadata = cluster.metadata
841
# ... time passes, schema changes occur ...
842
cluster.metadata.rebuild_schema()
843
new_metadata = cluster.metadata
844
845
compare_schemas(old_metadata, new_metadata, 'my_app')
846
```
847
848
### Dynamic Query Generation
849
850
```python
851
from cassandra.metadata import protect_name, protect_value
852
853
def generate_insert_query(table_metadata, data):
854
"""Generate INSERT query from table metadata and data."""
855
856
table_name = f"{table_metadata.keyspace_name}.{protect_name(table_metadata.name)}"
857
858
# Filter data to only include existing columns
859
valid_columns = []
860
valid_values = []
861
placeholders = []
862
863
for col_name, value in data.items():
864
if col_name in table_metadata.columns:
865
valid_columns.append(protect_name(col_name))
866
valid_values.append(value)
867
placeholders.append('?')
868
869
if not valid_columns:
870
raise ValueError("No valid columns found in data")
871
872
query = f"""
873
INSERT INTO {table_name} ({', '.join(valid_columns)})
874
VALUES ({', '.join(placeholders)})
875
"""
876
877
return query.strip(), valid_values
878
879
# Usage
880
table = metadata.get_table('my_app', 'users')
881
data = {
882
'id': uuid.uuid4(),
883
'name': 'Alice Smith',
884
'email': 'alice@example.com',
885
'invalid_column': 'ignored' # This will be filtered out
886
}
887
888
query, values = generate_insert_query(table, data)
889
print(f"Generated query: {query}")
890
print(f"Values: {values}")
891
892
session.execute(query, values)
893
```
894
895
### Custom Metadata Introspection
896
897
```python
898
def analyze_keyspace_complexity(keyspace_metadata):
899
"""Analyze complexity metrics for a keyspace."""
900
901
metrics = {
902
'total_tables': len(keyspace_metadata.tables),
903
'total_columns': 0,
904
'total_indexes': 0,
905
'tables_with_clustering': 0,
906
'tables_with_static_columns': 0,
907
'user_types': len(keyspace_metadata.user_types),
908
'functions': len(keyspace_metadata.functions),
909
'column_types': set()
910
}
911
912
for table in keyspace_metadata.tables.values():
913
metrics['total_columns'] += len(table.columns)
914
metrics['total_indexes'] += len(table.indexes)
915
916
if table.clustering_key:
917
metrics['tables_with_clustering'] += 1
918
919
has_static = any(col.is_static for col in table.columns.values())
920
if has_static:
921
metrics['tables_with_static_columns'] += 1
922
923
for column in table.columns.values():
924
metrics['column_types'].add(type(column.cql_type).__name__)
925
926
return metrics
927
928
# Usage
929
keyspace = metadata.get_keyspace('my_app')
930
if keyspace:
931
complexity = analyze_keyspace_complexity(keyspace)
932
print(f"Keyspace complexity analysis:")
933
for metric, value in complexity.items():
934
if metric == 'column_types':
935
print(f" {metric}: {sorted(value)}")
936
else:
937
print(f" {metric}: {value}")
938
```