• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DHARPA-Project / kiara_plugin.network_analysis / 15680097829

16 Jun 2025 11:55AM UTC coverage: 60.865% (+0.05%) from 60.815%
15680097829

push

github

makkus
chore: fix linting & mypy issues

63 of 121 branches covered (52.07%)

Branch coverage included in aggregate %.

5 of 18 new or added lines in 3 files covered. (27.78%)

3 existing lines in 2 files now uncovered.

472 of 758 relevant lines covered (62.27%)

3.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

60.9
/src/kiara_plugin/network_analysis/models/__init__.py
1
# -*- coding: utf-8 -*-
2

3
"""This module contains the metadata (and other) models that are used in the ``kiara_plugin.network_analysis`` package.
4

5
Those models are convenience wrappers that make it easier for *kiara* to find, create, manage and version metadata -- but also
6
other type of models -- that is attached to data, as well as *kiara* modules.
7

8
Metadata models must be a sub-class of [kiara.metadata.MetadataModel][kiara.metadata.MetadataModel]. Other models usually
9
sub-class a pydantic BaseModel or implement custom base classes.
10
"""
11

12
from typing import (
5✔
13
    TYPE_CHECKING,
14
    ClassVar,
15
    Dict,
16
    Iterable,
17
    List,
18
    Literal,
19
    Protocol,
20
    Type,
21
    TypeVar,
22
    Union,
23
)
24

25
from pydantic import BaseModel, Field
5✔
26

27
from kiara.exceptions import KiaraException
5✔
28
from kiara.models import KiaraModel
5✔
29
from kiara.models.values.value import Value
5✔
30
from kiara.models.values.value_metadata import ValueMetadata
5✔
31
from kiara_plugin.network_analysis.defaults import (
5✔
32
    ATTRIBUTE_PROPERTY_KEY,
33
    CONNECTIONS_COLUMN_NAME,
34
    CONNECTIONS_MULTI_COLUMN_NAME,
35
    COUNT_DIRECTED_COLUMN_NAME,
36
    COUNT_IDX_DIRECTED_COLUMN_NAME,
37
    COUNT_IDX_UNDIRECTED_COLUMN_NAME,
38
    COUNT_UNDIRECTED_COLUMN_NAME,
39
    EDGE_ID_COLUMN_NAME,
40
    EDGES_TABLE_NAME,
41
    IN_DIRECTED_COLUMN_NAME,
42
    IN_DIRECTED_MULTI_COLUMN_NAME,
43
    LABEL_COLUMN_NAME,
44
    NODE_ID_COLUMN_NAME,
45
    NODES_TABLE_NAME,
46
    OUT_DIRECTED_COLUMN_NAME,
47
    OUT_DIRECTED_MULTI_COLUMN_NAME,
48
    SOURCE_COLUMN_NAME,
49
    TARGET_COLUMN_NAME,
50
    UNWEIGHTED_DEGREE_CENTRALITY_COLUMN_NAME,
51
    UNWEIGHTED_DEGREE_CENTRALITY_MULTI_COLUMN_NAME,
52
    GraphType,
53
)
54
from kiara_plugin.network_analysis.utils import (
5✔
55
    augment_edges_table_with_id_and_weights,
56
    augment_nodes_table_with_connection_counts,
57
    extract_networkx_edges_as_table,
58
    extract_networkx_nodes_as_table,
59
)
60
from kiara_plugin.tabular.models.tables import KiaraTables
5✔
61

62
if TYPE_CHECKING:
5✔
63
    import networkx as nx
×
64
    import pyarrow as pa
×
65
    import rustworkx as rx
×
66

NEW
67
    from kiara_plugin.network_analysis.models.metadata import (
×
68
        NetworkNodeAttributeMetadata,
69
    )
UNCOV
70
    from kiara_plugin.tabular.models.table import KiaraTable
×
71

72
NETWORKX_GRAPH_TYPE = TypeVar("NETWORKX_GRAPH_TYPE", bound="nx.Graph")
5✔
73
RUSTWORKX_GRAPH_TYPE = TypeVar("RUSTWORKX_GRAPH_TYPE", "rx.PyGraph", "rx.PyDiGraph")
5✔
74

75

76
class NodesCallback(Protocol):
5✔
77
    def __call__(self, _node_id: int, **kwargs) -> None: ...
5✔
78

79

80
class EdgesCallback(Protocol):
5✔
81
    def __call__(self, _source: int, _target: int, **kwargs) -> None: ...
5✔
82

83

84
class NetworkData(KiaraTables):
5✔
85
    """A flexible, graph-type agnostic wrapper class for network datasets.
86

87
    This class provides a unified interface for working with network data that can represent
88
    any type of graph structure: directed, undirected, simple, or multi-graphs. The design
89
    philosophy emphasizes flexibility and performance while maintaining a clean, intuitive API.
90

91
    **Design Philosophy:**
92
    - **Graph Type Agnostic**: Supports all graph types (directed/undirected, simple/multi)
93
      within the same data structure without requiring type-specific conversions
94
    - **Efficient Storage**: Uses Apache Arrow tables for high-performance columnar storage
95
    - **Flexible Querying**: Provides SQL-based querying capabilities alongside programmatic access
96
    - **Seamless Export**: Easy conversion to NetworkX and RustWorkX graph objects, other representations possible in the future
97
    - **Metadata Rich**: Automatically computes and stores graph statistics and properties
98

99
    **Internal Structure:**
100
    The network data is stored as two Arrow tables:
101
    - **nodes table**: Contains node information with required columns '_node_id' (int) and '_label' (str)
102
    - **edges table**: Contains edge information with required columns '_source' (int) and '_target' (int)
103

104
    Additional computed columns (prefixed with '_') provide graph statistics for different interpretations:
105
    - Degree counts for directed/undirected graphs
106
    - Multi-edge counts and indices
107
    - Centrality measures
108

109
    **Graph Type Support:**
110
    - **Simple Graphs**: Single edges between node pairs
111
    - **Multi-graphs**: Multiple edges between the same node pairs
112
    - **Directed Graphs**: One-way edges with source → target semantics
113
    - **Undirected Graphs**: Bidirectional edges
114
    - **Mixed Types**: The same data can be interpreted as different graph types
115

116
    **Note:** Column names prefixed with '_' have internal meaning and are automatically
117
    computed. Original attributes from source data are stored without the prefix.
118
    """
119

120
    _kiara_model_id: ClassVar = "instance.network_data"
5✔
121

122
    @classmethod
5✔
123
    def create_augmented(
5✔
124
        cls,
125
        network_data: "NetworkData",
126
        additional_edges_columns: Union[None, Dict[str, "pa.Array"]] = None,
127
        additional_nodes_columns: Union[None, Dict[str, "pa.Array"]] = None,
128
        nodes_column_metadata: Union[Dict[str, Dict[str, KiaraModel]], None] = None,
129
        edges_column_metadata: Union[Dict[str, Dict[str, KiaraModel]], None] = None,
130
    ) -> "NetworkData":
131
        """Create a new NetworkData instance with additional columns.
132

133
        This method creates a new NetworkData instance by adding extra columns to an existing
134
        instance without recomputing the automatically generated internal columns (those
135
        prefixed with '_'). This is useful for adding derived attributes or analysis results.
136

137
        Args:
138
            network_data: The source NetworkData instance to augment
139
            additional_edges_columns: Dictionary mapping column names to PyArrow Arrays
140
                for new edge attributes
141
            additional_nodes_columns: Dictionary mapping column names to PyArrow Arrays
142
                for new node attributes
143
            nodes_column_metadata: Additional metadata to attach to nodes table columns
144
            edges_column_metadata: Additional metadata to attach to edges table columns
145

146
        Returns:
147
            NetworkData: A new NetworkData instance with the additional columns
148

149
        Example:
150
            ```python
151
            import pyarrow as pa
152

153
            # Add a weight column to edges
154
            weights = pa.array([1.0, 2.5, 0.8] * (network_data.num_edges // 3))
155
            augmented = NetworkData.create_augmented(
156
                network_data,
157
                additional_edges_columns={"weight": weights}
158
            )
159
            ```
160
        """
161

162
        nodes_table = network_data.nodes.arrow_table
×
163
        edges_table = network_data.edges.arrow_table
×
164

165
        # nodes_table = pa.Table.from_arrays(orig_nodes_table.columns, schema=orig_nodes_table.schema)
166
        # edges_table = pa.Table.from_arrays(orig_edges_table.columns, schema=orig_edges_table.schema)
167

168
        if additional_edges_columns is not None:
×
169
            for col_name, col_data in additional_edges_columns.items():
×
170
                edges_table = edges_table.append_column(col_name, col_data)
×
171

172
        if additional_nodes_columns is not None:
×
173
            for col_name, col_data in additional_nodes_columns.items():
×
174
                nodes_table = nodes_table.append_column(col_name, col_data)
×
175

176
        new_network_data = NetworkData.create_network_data(
×
177
            nodes_table=nodes_table,
178
            edges_table=edges_table,
179
            augment_tables=False,
180
            nodes_column_metadata=nodes_column_metadata,
181
            edges_column_metadata=edges_column_metadata,
182
        )
183

184
        return new_network_data
×
185

186
    @classmethod
5✔
187
    def create_network_data(
5✔
188
        cls,
189
        nodes_table: "pa.Table",
190
        edges_table: "pa.Table",
191
        augment_tables: bool = True,
192
        nodes_column_metadata: Union[Dict[str, Dict[str, KiaraModel]], None] = None,
193
        edges_column_metadata: Union[Dict[str, Dict[str, KiaraModel]], None] = None,
194
    ) -> "NetworkData":
195
        """Create a NetworkData instance from PyArrow tables.
196

197
        This is the primary factory method for creating NetworkData instances from raw tabular data.
198
        It supports all graph types and automatically computes necessary metadata for efficient
199
        graph operations.
200

201
        **Required Table Structure:**
202

203
        Nodes table must contain:
204
        - '_node_id' (int): Unique integer identifier for each node
205
        - '_label' (str): Human-readable label for the node
206

207
        Edges table must contain:
208
        - '_source' (int): Source node ID (must exist in nodes table)
209
        - '_target' (int): Target node ID (must exist in nodes table)
210

211
        **Automatic Augmentation:**
212
        When `augment_tables=True` (default), the method automatically adds computed columns:
213

214
        For edges:
215
        - '_edge_id': Unique edge identifier
216
        - '_count_dup_directed': Count of parallel edges (directed interpretation)
217
        - '_idx_dup_directed': Index within parallel edge group (directed)
218
        - '_count_dup_undirected': Count of parallel edges (undirected interpretation)
219
        - '_idx_dup_undirected': Index within parallel edge group (undirected)
220

221
        For nodes:
222
        - '_count_edges': Total edge count (simple graph interpretation)
223
        - '_count_edges_multi': Total edge count (multi-graph interpretation)
224
        - '_in_edges': Incoming edge count (directed, simple)
225
        - '_out_edges': Outgoing edge count (directed, simple)
226
        - '_in_edges_multi': Incoming edge count (directed, multi)
227
        - '_out_edges_multi': Outgoing edge count (directed, multi)
228
        - '_degree_centrality': Normalized degree centrality
229
        - '_degree_centrality_multi': Normalized degree centrality (multi-graph)
230

231
        Args:
232
            nodes_table: PyArrow table containing node data
233
            edges_table: PyArrow table containing edge data
234
            augment_tables: Whether to compute and add internal metadata columns.
235
                Set to False only if you know the metadata is already present and correct.
236
            nodes_column_metadata: Additional metadata to attach to nodes table columns.
237
                Format: {column_name: {property_name: property_value}}
238
            edges_column_metadata: Additional metadata to attach to edges table columns.
239
                Format: {column_name: {property_name: property_value}}
240

241
        Returns:
242
            NetworkData: A new NetworkData instance
243

244
        Raises:
245
            KiaraException: If required columns are missing or contain null values
246

247
        Example:
248
            ```python
249
            import pyarrow as pa
250

251
            # Create simple graph data
252
            nodes = pa.table({
253
                '_node_id': [0, 1, 2],
254
                '_label': ['A', 'B', 'C'],
255
                'type': ['person', 'person', 'organization']
256
            })
257

258
            edges = pa.table({
259
                '_source': [0, 1, 2],
260
                '_target': [1, 2, 0],
261
                'relationship': ['friend', 'works_for', 'sponsors']
262
            })
263

264
            network_data = NetworkData.create_network_data(nodes, edges)
265
            ```
266
        """
267

268
        from kiara_plugin.network_analysis.models.metadata import (
5✔
269
            EDGE_COUNT_DUP_DIRECTED_COLUMN_METADATA,
270
            EDGE_COUNT_DUP_UNDIRECTED_COLUMN_METADATA,
271
            EDGE_ID_COLUMN_METADATA,
272
            EDGE_IDX_DUP_DIRECTED_COLUMN_METADATA,
273
            EDGE_IDX_DUP_UNDIRECTED_COLUMN_METADATA,
274
            EDGE_SOURCE_COLUMN_METADATA,
275
            EDGE_TARGET_COLUMN_METADATA,
276
            NODE_COUND_EDGES_MULTI_COLUMN_METADATA,
277
            NODE_COUNT_EDGES_COLUMN_METADATA,
278
            NODE_COUNT_IN_EDGES_COLUMN_METADATA,
279
            NODE_COUNT_IN_EDGES_MULTI_COLUMN_METADATA,
280
            NODE_COUNT_OUT_EDGES_COLUMN_METADATA,
281
            NODE_COUNT_OUT_EDGES_MULTI_COLUMN_METADATA,
282
            NODE_DEGREE_COLUMN_METADATA,
283
            NODE_DEGREE_MULTI_COLUMN_METADATA,
284
            NODE_ID_COLUMN_METADATA,
285
            NODE_LABEL_COLUMN_METADATA,
286
        )
287

288
        if augment_tables:
5✔
289
            edges_table = augment_edges_table_with_id_and_weights(edges_table)
5✔
290
            nodes_table = augment_nodes_table_with_connection_counts(
5✔
291
                nodes_table, edges_table
292
            )
293

294
        if edges_table.column(SOURCE_COLUMN_NAME).null_count > 0:
5✔
295
            raise KiaraException(
×
296
                msg="Can't assemble network data.",
297
                details="Source column in edges table contains null values.",
298
            )
299
        if edges_table.column(TARGET_COLUMN_NAME).null_count > 0:
5✔
300
            raise KiaraException(
×
301
                msg="Can't assemble network data.",
302
                details="Target column in edges table contains null values.",
303
            )
304

305
        network_data: NetworkData = cls.create_tables(
5✔
306
            {NODES_TABLE_NAME: nodes_table, EDGES_TABLE_NAME: edges_table}
307
        )
308

309
        # set default column metadata
310
        network_data.edges.set_column_metadata(
5✔
311
            EDGE_ID_COLUMN_NAME,
312
            ATTRIBUTE_PROPERTY_KEY,
313
            EDGE_ID_COLUMN_METADATA,
314
            overwrite_existing=False,
315
        )
316
        network_data.edges.set_column_metadata(
5✔
317
            SOURCE_COLUMN_NAME,
318
            ATTRIBUTE_PROPERTY_KEY,
319
            EDGE_SOURCE_COLUMN_METADATA,
320
            overwrite_existing=False,
321
        )
322
        network_data.edges.set_column_metadata(
5✔
323
            TARGET_COLUMN_NAME,
324
            ATTRIBUTE_PROPERTY_KEY,
325
            EDGE_TARGET_COLUMN_METADATA,
326
            overwrite_existing=False,
327
        )
328
        network_data.edges.set_column_metadata(
5✔
329
            COUNT_DIRECTED_COLUMN_NAME,
330
            ATTRIBUTE_PROPERTY_KEY,
331
            EDGE_COUNT_DUP_DIRECTED_COLUMN_METADATA,
332
            overwrite_existing=False,
333
        )
334
        network_data.edges.set_column_metadata(
5✔
335
            COUNT_IDX_DIRECTED_COLUMN_NAME,
336
            ATTRIBUTE_PROPERTY_KEY,
337
            EDGE_IDX_DUP_DIRECTED_COLUMN_METADATA,
338
            overwrite_existing=False,
339
        )
340
        network_data.edges.set_column_metadata(
5✔
341
            COUNT_UNDIRECTED_COLUMN_NAME,
342
            ATTRIBUTE_PROPERTY_KEY,
343
            EDGE_COUNT_DUP_UNDIRECTED_COLUMN_METADATA,
344
            overwrite_existing=False,
345
        )
346
        network_data.edges.set_column_metadata(
5✔
347
            COUNT_IDX_UNDIRECTED_COLUMN_NAME,
348
            ATTRIBUTE_PROPERTY_KEY,
349
            EDGE_IDX_DUP_UNDIRECTED_COLUMN_METADATA,
350
            overwrite_existing=False,
351
        )
352

353
        network_data.nodes.set_column_metadata(
5✔
354
            NODE_ID_COLUMN_NAME,
355
            ATTRIBUTE_PROPERTY_KEY,
356
            NODE_ID_COLUMN_METADATA,
357
            overwrite_existing=False,
358
        )
359
        network_data.nodes.set_column_metadata(
5✔
360
            LABEL_COLUMN_NAME,
361
            ATTRIBUTE_PROPERTY_KEY,
362
            NODE_LABEL_COLUMN_METADATA,
363
            overwrite_existing=False,
364
        )
365
        network_data.nodes.set_column_metadata(
5✔
366
            CONNECTIONS_COLUMN_NAME,
367
            ATTRIBUTE_PROPERTY_KEY,
368
            NODE_COUNT_EDGES_COLUMN_METADATA,
369
            overwrite_existing=False,
370
        )
371
        network_data.nodes.set_column_metadata(
5✔
372
            UNWEIGHTED_DEGREE_CENTRALITY_COLUMN_NAME,
373
            ATTRIBUTE_PROPERTY_KEY,
374
            NODE_DEGREE_COLUMN_METADATA,
375
            overwrite_existing=False,
376
        )
377
        network_data.nodes.set_column_metadata(
5✔
378
            CONNECTIONS_MULTI_COLUMN_NAME,
379
            ATTRIBUTE_PROPERTY_KEY,
380
            NODE_COUND_EDGES_MULTI_COLUMN_METADATA,
381
            overwrite_existing=False,
382
        )
383
        network_data.nodes.set_column_metadata(
5✔
384
            UNWEIGHTED_DEGREE_CENTRALITY_MULTI_COLUMN_NAME,
385
            ATTRIBUTE_PROPERTY_KEY,
386
            NODE_DEGREE_MULTI_COLUMN_METADATA,
387
            overwrite_existing=False,
388
        )
389
        network_data.nodes.set_column_metadata(
5✔
390
            IN_DIRECTED_COLUMN_NAME,
391
            ATTRIBUTE_PROPERTY_KEY,
392
            NODE_COUNT_IN_EDGES_COLUMN_METADATA,
393
            overwrite_existing=False,
394
        )
395
        network_data.nodes.set_column_metadata(
5✔
396
            IN_DIRECTED_MULTI_COLUMN_NAME,
397
            ATTRIBUTE_PROPERTY_KEY,
398
            NODE_COUNT_IN_EDGES_MULTI_COLUMN_METADATA,
399
            overwrite_existing=False,
400
        )
401
        network_data.nodes.set_column_metadata(
5✔
402
            OUT_DIRECTED_COLUMN_NAME,
403
            ATTRIBUTE_PROPERTY_KEY,
404
            NODE_COUNT_OUT_EDGES_COLUMN_METADATA,
405
            overwrite_existing=False,
406
        )
407
        network_data.nodes.set_column_metadata(
5✔
408
            OUT_DIRECTED_MULTI_COLUMN_NAME,
409
            ATTRIBUTE_PROPERTY_KEY,
410
            NODE_COUNT_OUT_EDGES_MULTI_COLUMN_METADATA,
411
            overwrite_existing=False,
412
        )
413

414
        if nodes_column_metadata is not None:
5✔
415
            for col_name, col_meta in nodes_column_metadata.items():
×
416
                for prop_name, prop_value in col_meta.items():
×
417
                    network_data.nodes.set_column_metadata(
×
418
                        col_name, prop_name, prop_value, overwrite_existing=True
419
                    )
420
        if edges_column_metadata is not None:
5✔
421
            for col_name, col_meta in edges_column_metadata.items():
×
422
                for prop_name, prop_value in col_meta.items():
×
423
                    network_data.edges.set_column_metadata(
×
424
                        col_name, prop_name, prop_value, overwrite_existing=True
425
                    )
426

427
        return network_data
5✔
428

429
    @classmethod
5✔
430
    def from_filtered_nodes(
5✔
431
        cls, network_data: "NetworkData", nodes_list: List[int]
432
    ) -> "NetworkData":
433
        """Create a new, filtered instance of this class using a source network, and a list of node ids to include.
434

435
        Nodes/edges containing a node id not in the list will be removed from the resulting network data.
436

437
        Arguments:
438
            network_data: the source network data
439
            nodes_list: the list of node ids to include in the filtered network data
440
        """
441

442
        import duckdb
×
443
        import polars as pl
×
444

445
        node_columns = [NODE_ID_COLUMN_NAME, LABEL_COLUMN_NAME]
×
446
        for column_name, metadata in network_data.nodes.column_metadata.items():
×
447
            attr_prop: Union[None, NetworkNodeAttributeMetadata] = metadata.get(  # type: ignore
×
448
                ATTRIBUTE_PROPERTY_KEY, None
449
            )
450
            if attr_prop is None or not attr_prop.computed_attribute:
×
451
                node_columns.append(column_name)
×
452

453
        node_list_str = ", ".join([str(n) for n in nodes_list])
×
454

455
        nodes_table = network_data.nodes.arrow_table  # noqa
×
456
        nodes_query = f"SELECT {', '.join(node_columns)} FROM nodes_table n WHERE n.{NODE_ID_COLUMN_NAME} IN ({node_list_str})"
×
457

458
        nodes_result = duckdb.sql(nodes_query).pl()
×
459

460
        edges_table = network_data.edges.arrow_table  # noqa
×
461
        edge_columns = [SOURCE_COLUMN_NAME, TARGET_COLUMN_NAME]
×
462
        for column_name, metadata in network_data.edges.column_metadata.items():
×
463
            attr_prop = metadata.get(ATTRIBUTE_PROPERTY_KEY, None)  # type: ignore
×
464
            if attr_prop is None or not attr_prop.computed_attribute:
×
465
                edge_columns.append(column_name)
×
466

467
        edges_query = f"SELECT {', '.join(edge_columns)} FROM edges_table WHERE {SOURCE_COLUMN_NAME} IN ({node_list_str}) OR {TARGET_COLUMN_NAME} IN ({node_list_str})"
×
468

469
        edges_result = duckdb.sql(edges_query).pl()
×
470

471
        nodes_idx_colum = range(len(nodes_result))
×
472
        old_idx_column = nodes_result[NODE_ID_COLUMN_NAME]
×
473

474
        repl_map = dict(zip(old_idx_column.to_list(), nodes_idx_colum))
×
475
        nodes_result = nodes_result.with_columns(
×
476
            pl.col(NODE_ID_COLUMN_NAME).replace_strict(repl_map, default=None)
477
        )
478

479
        edges_result = edges_result.with_columns(
×
480
            pl.col(SOURCE_COLUMN_NAME).replace_strict(repl_map, default=None),
481
            pl.col(TARGET_COLUMN_NAME).replace_strict(repl_map, default=None),
482
        )
483

484
        filtered = NetworkData.create_network_data(
×
485
            nodes_table=nodes_result, edges_table=edges_result
486
        )
487
        return filtered
×
488

489
    @classmethod
5✔
490
    def create_from_networkx_graph(
5✔
491
        cls,
492
        graph: "nx.Graph",
493
        label_attr_name: Union[str, None] = None,
494
        ignore_node_attributes: Union[Iterable[str], None] = None,
495
    ) -> "NetworkData":
496
        """Create a NetworkData instance from any NetworkX graph type.
497

498
        This method provides seamless conversion from NetworkX graphs to NetworkData,
499
        preserving all node and edge attributes while automatically handling different
500
        graph types (Graph, DiGraph, MultiGraph, MultiDiGraph).
501

502
        **Graph Type Support:**
503
        - **nx.Graph**: Converted to undirected simple graph representation
504
        - **nx.DiGraph**: Converted to directed simple graph representation
505
        - **nx.MultiGraph**: Converted with multi-edge support (undirected)
506
        - **nx.MultiDiGraph**: Converted with multi-edge support (directed)
507

508
        **Attribute Handling:**
509
        All NetworkX node and edge attributes are preserved as columns in the resulting
510
        tables, except those starting with '_' (reserved for internal use).
511

512
        Args:
513
            graph: Any NetworkX graph instance (Graph, DiGraph, MultiGraph, MultiDiGraph)
514
            label_attr_name: Name of the node attribute to use as the node label.
515
                If None, the node ID is converted to string and used as label.
516
                Can also be an iterable of attribute names to try in order.
517
            ignore_node_attributes: List of node attribute names to exclude from
518
                the resulting nodes table
519

520
        Returns:
521
            NetworkData: A new NetworkData instance representing the graph
522

523
        Raises:
524
            KiaraException: If node/edge attributes contain names starting with '_'
525

526
        Note:
527
            Node IDs in the original NetworkX graph are mapped to sequential integers
528
            starting from 0 in the NetworkData representation. The original node IDs
529
            are preserved as the '_label' if no label_attr_name is specified.
530
        """
531

532
        # TODO: should we also index nodes/edges attributes?
533

534
        nodes_table, node_id_map = extract_networkx_nodes_as_table(
×
535
            graph=graph,
536
            label_attr_name=label_attr_name,
537
            ignore_attributes=ignore_node_attributes,
538
        )
539

540
        edges_table = extract_networkx_edges_as_table(graph, node_id_map)
×
541

542
        network_data = NetworkData.create_network_data(
×
543
            nodes_table=nodes_table, edges_table=edges_table
544
        )
545

546
        return network_data
×
547

548
    @property
5✔
549
    def edges(self) -> "KiaraTable":
5✔
550
        """Access the edges table containing all edge data and computed statistics.
551

552
        The edges table contains both original edge attributes and computed columns:
553
        - '_edge_id': Unique edge identifier
554
        - '_source', '_target': Node IDs for edge endpoints
555
        - '_count_dup_*': Parallel edge counts for different graph interpretations
556
        - '_idx_dup_*': Indices within parallel edge groups
557
        - Original edge attributes (without '_' prefix)
558

559
        Returns:
560
            KiaraTable: The edges table with full schema and data access methods
561
        """
562
        return self.tables[EDGES_TABLE_NAME]
5✔
563

564
    @property
5✔
565
    def nodes(self) -> "KiaraTable":
5✔
566
        """Access the nodes table containing all node data and computed statistics.
567

568
        The nodes table contains both original node attributes and computed columns:
569
        - '_node_id': Unique node identifier (sequential integers from 0)
570
        - '_label': Human-readable node label
571
        - '_count_edges*': Edge counts for different graph interpretations
572
        - '_in_edges*', '_out_edges*': Directional edge counts
573
        - '_degree_centrality*': Normalized degree centrality measures
574
        - Original node attributes (without '_' prefix)
575

576
        Returns:
577
            KiaraTable: The nodes table with full schema and data access methods
578
        """
579
        return self.tables[NODES_TABLE_NAME]
5✔
580

581
    @property
5✔
582
    def num_nodes(self) -> int:
5✔
583
        """Get the total number of nodes in the network.
584

585
        Returns:
586
            int: Number of nodes in the network
587
        """
588
        return self.nodes.num_rows  # type: ignore
5✔
589

590
    @property
5✔
591
    def num_edges(self) -> int:
5✔
592
        """Get the total number of edges in the network.
593

594
        Note: This returns the total number of edge records, which includes
595
        all parallel edges in multi-graph interpretations.
596

597
        Returns:
598
            int: Total number of edges (including parallel edges)
599
        """
600
        return self.edges.num_rows  # type: ignore
5✔
601

602
    def query_edges(
5✔
603
        self, sql_query: str, relation_name: str = EDGES_TABLE_NAME
604
    ) -> "pa.Table":
605
        """Execute SQL queries on the edges table for flexible data analysis.
606

607
        This method provides direct SQL access to the edges table, enabling complex
608
        queries and aggregations. All computed edge columns are available for querying.
609

610
        **Available Columns:**
611
        - '_edge_id': Unique edge identifier
612
        - '_source', '_target': Node IDs for edge endpoints
613
        - '_count_dup_directed': Number of parallel edges (directed interpretation)
614
        - '_idx_dup_directed': Index within parallel edge group (directed)
615
        - '_count_dup_undirected': Number of parallel edges (undirected interpretation)
616
        - '_idx_dup_undirected': Index within parallel edge group (undirected)
617
        - Original edge attributes (names without '_' prefix)
618

619
        Args:
620
            sql_query: SQL query string. Use 'edges' as the table name in your query.
621
            relation_name: Alternative table name to use in the query (default: 'edges').
622
                If specified, all occurrences of this name in the query will be replaced
623
                with 'edges'.
624

625
        Returns:
626
            pa.Table: Query results as a PyArrow table
627

628
        Example:
629
            ```python
630
            # Find edges with high multiplicity
631
            parallel_edges = network_data.query_edges(
632
                "SELECT _source, _target, _count_dup_directed FROM edges WHERE _count_dup_directed > 1"
633
            )
634

635
            # Get edge statistics
636
            stats = network_data.query_edges(
637
                "SELECT COUNT(*) as total_edges, AVG(_count_dup_directed) as avg_multiplicity FROM edges"
638
            )
639
            ```
640
        """
641
        import duckdb
5✔
642

643
        con = duckdb.connect()
5✔
644
        edges = self.edges.arrow_table  # noqa: F841
5✔
645
        if relation_name != EDGES_TABLE_NAME:
5✔
646
            sql_query = sql_query.replace(relation_name, EDGES_TABLE_NAME)
×
647

648
        result = con.execute(sql_query)
5✔
649
        return result.arrow()
5✔
650

651
    def query_nodes(
5✔
652
        self, sql_query: str, relation_name: str = NODES_TABLE_NAME
653
    ) -> "pa.Table":
654
        """Execute SQL queries on the nodes table for flexible data analysis.
655

656
        This method provides direct SQL access to the nodes table, enabling complex
657
        queries and aggregations. All computed node statistics are available for querying.
658

659
        **Available Columns:**
660
        - '_node_id': Unique node identifier
661
        - '_label': Human-readable node label
662
        - '_count_edges': Total edge count (simple graph interpretation)
663
        - '_count_edges_multi': Total edge count (multi-graph interpretation)
664
        - '_in_edges': Incoming edge count (directed, simple)
665
        - '_out_edges': Outgoing edge count (directed, simple)
666
        - '_in_edges_multi': Incoming edge count (directed, multi)
667
        - '_out_edges_multi': Outgoing edge count (directed, multi)
668
        - '_degree_centrality': Normalized degree centrality (simple)
669
        - '_degree_centrality_multi': Normalized degree centrality (multi)
670
        - Original node attributes (names without '_' prefix)
671

672
        Args:
673
            sql_query: SQL query string. Use 'nodes' as the table name in your query.
674
            relation_name: Alternative table name to use in the query (default: 'nodes').
675
                If specified, all occurrences of this name in the query will be replaced
676
                with 'nodes'.
677

678
        Returns:
679
            pa.Table: Query results as a PyArrow table
680

681
        Example:
682
            ```python
683
            # Find high-degree nodes
684
            hubs = network_data.query_nodes(
685
                "SELECT _node_id, _label, _count_edges FROM nodes WHERE _count_edges > 10 ORDER BY _count_edges DESC"
686
            )
687

688
            # Get centrality statistics
689
            centrality_stats = network_data.query_nodes(
690
                "SELECT AVG(_degree_centrality) as avg_centrality, MAX(_degree_centrality) as max_centrality FROM nodes"
691
            )
692
            ```
693
        """
694
        import duckdb
×
695

696
        con = duckdb.connect()
×
697
        nodes = self.nodes.arrow_table  # noqa
×
698
        if relation_name != NODES_TABLE_NAME:
×
699
            sql_query = sql_query.replace(relation_name, NODES_TABLE_NAME)
×
700

701
        result = con.execute(sql_query)
×
702
        return result.arrow()
×
703

704
    def _calculate_node_attributes(
5✔
705
        self, incl_node_attributes: Union[bool, str, Iterable[str]]
706
    ) -> List[str]:
707
        """Calculate the node attributes that should be included in the output."""
708

709
        if incl_node_attributes is False:
5✔
710
            node_attr_names: List[str] = [NODE_ID_COLUMN_NAME, LABEL_COLUMN_NAME]
5✔
711
        else:
712
            all_node_attr_names: List[str] = self.nodes.column_names  # type: ignore
×
713
            if incl_node_attributes is True:
×
714
                node_attr_names = [NODE_ID_COLUMN_NAME]
×
NEW
715
                node_attr_names.extend(
×
716
                    (x for x in all_node_attr_names if x != NODE_ID_COLUMN_NAME)
717
                )  # type: ignore
718
            elif isinstance(incl_node_attributes, str):
×
719
                if incl_node_attributes not in all_node_attr_names:
×
720
                    raise KiaraException(
×
721
                        f"Can't include node attribute {incl_node_attributes}: not part of the available attributes ({', '.join(all_node_attr_names)})."
722
                    )
723
                node_attr_names = [NODE_ID_COLUMN_NAME, incl_node_attributes]
×
724
            else:
725
                node_attr_names = [NODE_ID_COLUMN_NAME]
×
726
                for attr_name in incl_node_attributes:
×
727
                    if incl_node_attributes not in all_node_attr_names:
×
728
                        raise KiaraException(
×
729
                            f"Can't include node attribute {incl_node_attributes}: not part of the available attributes ({', '.join(all_node_attr_names)})."
730
                        )
731
                    node_attr_names.append(attr_name)  # type: ignore
×
732

733
        return node_attr_names
5✔
734

735
    def _calculate_edge_attributes(
5✔
736
        self, incl_edge_attributes: Union[bool, str, Iterable[str]]
737
    ) -> List[str]:
738
        """Calculate the edge attributes that should be included in the output."""
739

740
        if incl_edge_attributes is False:
5✔
741
            edge_attr_names: List[str] = [SOURCE_COLUMN_NAME, TARGET_COLUMN_NAME]
5✔
742
        else:
743
            all_edge_attr_names: List[str] = self.edges.column_names  # type: ignore
×
744
            if incl_edge_attributes is True:
×
745
                edge_attr_names = [SOURCE_COLUMN_NAME, TARGET_COLUMN_NAME]
×
746
                edge_attr_names.extend(
×
747
                    (
748
                        x
749
                        for x in all_edge_attr_names
750
                        if x not in (SOURCE_COLUMN_NAME, TARGET_COLUMN_NAME)
751
                    )
752
                )  # type: ignore
753
            elif isinstance(incl_edge_attributes, str):
×
754
                if incl_edge_attributes not in all_edge_attr_names:
×
755
                    raise KiaraException(
×
756
                        f"Can't include edge attribute {incl_edge_attributes}: not part of the available attributes ({', '.join(all_edge_attr_names)})."
757
                    )
758
                edge_attr_names = [
×
759
                    SOURCE_COLUMN_NAME,
760
                    TARGET_COLUMN_NAME,
761
                    incl_edge_attributes,
762
                ]
763
            else:
764
                edge_attr_names = [SOURCE_COLUMN_NAME, TARGET_COLUMN_NAME]
×
765
                for attr_name in incl_edge_attributes:
×
766
                    if incl_edge_attributes not in all_edge_attr_names:
×
767
                        raise KiaraException(
×
768
                            f"Can't include edge attribute {incl_edge_attributes}: not part of the available attributes ({', '.join(all_edge_attr_names)})."
769
                        )
770
                    edge_attr_names.append(attr_name)  # type: ignore
×
771

772
        return edge_attr_names
5✔
773

774
    def retrieve_graph_data(
5✔
775
        self,
776
        nodes_callback: Union[NodesCallback, None] = None,
777
        edges_callback: Union[EdgesCallback, None] = None,
778
        incl_node_attributes: Union[bool, str, Iterable[str]] = False,
779
        incl_edge_attributes: Union[bool, str, Iterable[str]] = False,
780
        omit_self_loops: bool = False,
781
    ):
782
        """Retrieve graph data from the sqlite database, and call the specified callbacks for each node and edge.
783

784
        First the nodes will be processed, then the edges, if that does not suit your needs you can just use this method twice, and set the callback you don't need to None.
785

786
        The nodes_callback will be called with the following arguments:
787
            - node_id: the id of the node (int)
788
            - if False: nothing else
789
            - if True: all node attributes, in the order they are defined in the table schema
790
            - if str: the value of the specified node attribute
791
            - if Iterable[str]: the values of the specified node attributes, in the order they are specified
792

793
        The edges_callback will be called with the following aruments:
794
            - source_id: the id of the source node (int)
795
            - target_id: the id of the target node (int)
796
            - if False: nothing else
797
            - if True: all edge attributes, in the order they are defined in the table schema
798
            - if str: the value of the specified edge attribute
799
            - if Iterable[str]: the values of the specified edge attributes, in the order they are specified
800

801
        """
802

803
        if nodes_callback is not None:
5✔
804
            node_attr_names = self._calculate_node_attributes(incl_node_attributes)
5✔
805

806
            nodes_df = self.nodes.to_polars_dataframe()
5✔
807
            for row in nodes_df.select(*node_attr_names).rows(named=True):
5✔
808
                nodes_callback(**row)  # type: ignore
5✔
809

810
        if edges_callback is not None:
5✔
811
            edge_attr_names = self._calculate_edge_attributes(incl_edge_attributes)
5✔
812

813
            edges_df = self.edges.to_polars_dataframe()
5✔
814
            for row in edges_df.select(*edge_attr_names).rows(named=True):
5✔
815
                if (
5✔
816
                    omit_self_loops
817
                    and row[SOURCE_COLUMN_NAME] == row[TARGET_COLUMN_NAME]
818
                ):
819
                    continue
×
820
                edges_callback(**row)  # type: ignore
5✔
821

822
    def as_networkx_graph(
5✔
823
        self,
824
        graph_type: Type[NETWORKX_GRAPH_TYPE],
825
        incl_node_attributes: Union[bool, str, Iterable[str]] = False,
826
        incl_edge_attributes: Union[bool, str, Iterable[str]] = False,
827
        omit_self_loops: bool = False,
828
    ) -> NETWORKX_GRAPH_TYPE:
829
        """Export the network data as a NetworkX graph object.
830

831
        This method converts the NetworkData to any NetworkX graph type, providing
832
        flexibility to work with the data using NetworkX's extensive algorithm library.
833
        The conversion preserves node and edge attributes as specified.
834

835
        **Supported Graph Types:**
836
        - **nx.Graph**: Undirected simple graph (parallel edges are merged)
837
        - **nx.DiGraph**: Directed simple graph (parallel edges are merged)
838
        - **nx.MultiGraph**: Undirected multigraph (parallel edges preserved)
839
        - **nx.MultiDiGraph**: Directed multigraph (parallel edges preserved)
840

841
        **Attribute Handling:**
842
        Node and edge attributes can be selectively included in the exported graph.
843
        Internal columns (prefixed with '_') are available but typically excluded
844
        from exports to maintain clean NetworkX compatibility.
845

846
        Args:
847
            graph_type: NetworkX graph class to instantiate (nx.Graph, nx.DiGraph, etc.)
848
            incl_node_attributes: Controls which node attributes to include:
849
                - False: No attributes (only node IDs)
850
                - True: All attributes (including computed columns)
851
                - str: Single attribute name to include
852
                - Iterable[str]: List of specific attributes to include
853
            incl_edge_attributes: Controls which edge attributes to include:
854
                - False: No attributes
855
                - True: All attributes (including computed columns)
856
                - str: Single attribute name to include
857
                - Iterable[str]: List of specific attributes to include
858
            omit_self_loops: If True, edges where source equals target are excluded
859

860
        Returns:
861
            NETWORKX_GRAPH_TYPE: NetworkX graph instance of the specified type
862

863
        Note:
864
            When exporting to simple graph types (Graph, DiGraph), parallel edges
865
            are automatically merged. Use MultiGraph or MultiDiGraph to preserve
866
            all edge instances.
867
        """
868

869
        graph: NETWORKX_GRAPH_TYPE = graph_type()
5✔
870

871
        def add_node(_node_id: int, **attrs):
5✔
872
            graph.add_node(_node_id, **attrs)
5✔
873

874
        def add_edge(_source: int, _target: int, **attrs):
5✔
875
            graph.add_edge(_source, _target, **attrs)
5✔
876

877
        self.retrieve_graph_data(
5✔
878
            nodes_callback=add_node,
879
            edges_callback=add_edge,
880
            incl_node_attributes=incl_node_attributes,
881
            incl_edge_attributes=incl_edge_attributes,
882
            omit_self_loops=omit_self_loops,
883
        )
884

885
        return graph
5✔
886

887
    def as_rustworkx_graph(
5✔
888
        self,
889
        graph_type: Type[RUSTWORKX_GRAPH_TYPE],
890
        multigraph: bool = False,
891
        incl_node_attributes: Union[bool, str, Iterable[str]] = False,
892
        incl_edge_attributes: Union[bool, str, Iterable[str]] = False,
893
        omit_self_loops: bool = False,
894
        attach_node_id_map: bool = False,
895
    ) -> RUSTWORKX_GRAPH_TYPE:
896
        """Export the network data as a RustWorkX graph object.
897

898
        RustWorkX provides high-performance graph algorithms implemented in Rust with
899
        Python bindings. This method converts NetworkData to RustWorkX format while
900
        handling the differences in node ID management between the two systems.
901

902
        **Supported Graph Types:**
903
        - **rx.PyGraph**: Undirected graph (with optional multigraph support)
904
        - **rx.PyDiGraph**: Directed graph (with optional multigraph support)
905

906
        **Node ID Mapping:**
907
        RustWorkX uses sequential integer node IDs starting from 0, which may differ
908
        from the original NetworkData node IDs. The original '_node_id' values are
909
        preserved as node attributes, and an optional mapping can be attached to
910
        the graph for reference.
911

912
        **Performance Benefits:**
913
        RustWorkX graphs offer significant performance advantages for:
914
        - Large-scale graph algorithms
915
        - Parallel processing
916
        - Memory-efficient operations
917
        - High-performance centrality calculations
918

919
        Args:
920
            graph_type: RustWorkX graph class (rx.PyGraph or rx.PyDiGraph)
921
            multigraph: If True, parallel edges are preserved; if False, they are merged
922
            incl_node_attributes: Controls which node attributes to include:
923
                - False: No attributes (only node data structure)
924
                - True: All attributes (including computed columns)
925
                - str: Single attribute name to include
926
                - Iterable[str]: List of specific attributes to include
927
            incl_edge_attributes: Controls which edge attributes to include:
928
                - False: No attributes
929
                - True: All attributes (including computed columns)
930
                - str: Single attribute name to include
931
                - Iterable[str]: List of specific attributes to include
932
            omit_self_loops: If True, self-loops (edges where source == target) are excluded
933
            attach_node_id_map: If True, adds a 'node_id_map' attribute to the graph
934
                containing the mapping from RustWorkX node IDs to original NetworkData node IDs
935

936
        Returns:
937
            RUSTWORKX_GRAPH_TYPE: RustWorkX graph instance of the specified type
938

939
        Note:
940
            The original NetworkData '_node_id' values are always included in the
941
            node data dictionary, regardless of the incl_node_attributes setting.
942
        """
943

944
        from bidict import bidict
5✔
945

946
        graph = graph_type(multigraph=multigraph)
5✔
947

948
        # rustworkx uses 0-based integer indexes, so we don't neeed to look up the node ids (unless we want to
949
        # include node attributes)
950

951
        self._calculate_node_attributes(incl_node_attributes)[1:]
5✔
952
        self._calculate_edge_attributes(incl_edge_attributes)[2:]
5✔
953

954
        # we can use a 'global' dict here because we know the nodes are processed before the edges
955
        node_map: bidict = bidict()
5✔
956

957
        def add_node(_node_id: int, **attrs):
5✔
958
            data = {NODE_ID_COLUMN_NAME: _node_id}
5✔
959
            data.update(attrs)
5✔
960

961
            graph_node_id = graph.add_node(data)
5✔
962

963
            node_map[graph_node_id] = _node_id
5✔
964
            # if not _node_id == graph_node_id:
965
            #     raise Exception("Internal error: node ids don't match")
966

967
        def add_edge(_source: int, _target: int, **attrs):
5✔
968
            source = node_map[_source]
5✔
969
            target = node_map[_target]
5✔
970
            if not attrs:
5✔
971
                graph.add_edge(source, target, None)
5✔
972
            else:
973
                graph.add_edge(source, target, attrs)
×
974

975
        self.retrieve_graph_data(
5✔
976
            nodes_callback=add_node,
977
            edges_callback=add_edge,
978
            incl_node_attributes=incl_node_attributes,
979
            incl_edge_attributes=incl_edge_attributes,
980
            omit_self_loops=omit_self_loops,
981
        )
982

983
        if attach_node_id_map:
5✔
984
            graph.attrs = {"node_id_map": node_map}  # type: ignore
×
985

986
        return graph
5✔
987

988

989
class GraphProperties(BaseModel):
5✔
990
    """Properties of graph data, if interpreted as a specific graph type."""
991

992
    number_of_edges: int = Field(description="The number of edges.")
5✔
993
    parallel_edges: int = Field(
5✔
994
        description="The number of parallel edges (if 'multi' graph type).", default=0
995
    )
996

997

998
class NetworkGraphProperties(ValueMetadata):
5✔
999
    """Network data stats."""
1000

1001
    _metadata_key: ClassVar[str] = "network_data"
5✔
1002

1003
    number_of_nodes: int = Field(description="Number of nodes in the network graph.")
5✔
1004
    properties_by_graph_type: Dict[  # type: ignore
5✔
1005
        Literal[
1006
            GraphType.DIRECTED.value,
1007
            GraphType.UNDIRECTED.value,
1008
            GraphType.UNDIRECTED_MULTI.value,
1009
            GraphType.DIRECTED_MULTI.value,
1010
        ],
1011
        GraphProperties,
1012
    ] = Field(description="Properties of the network data, by graph type.")
1013
    number_of_self_loops: int = Field(
5✔
1014
        description="Number of edges where source and target point to the same node."
1015
    )
1016

1017
    @classmethod
5✔
1018
    def retrieve_supported_data_types(cls) -> Iterable[str]:
5✔
1019
        return ["network_data"]
5✔
1020

1021
    @classmethod
5✔
1022
    def create_value_metadata(cls, value: Value) -> "NetworkGraphProperties":
5✔
1023
        network_data: NetworkData = value.data
5✔
1024

1025
        num_rows = network_data.num_nodes
5✔
1026
        num_edges = network_data.num_edges
5✔
1027

1028
        # query_num_edges_directed = f"SELECT COUNT(*) FROM (SELECT DISTINCT {SOURCE_COLUMN_NAME}, {TARGET_COLUMN_NAME} FROM {EDGES_TABLE_NAME})"
1029
        query_num_edges_directed = f"SELECT COUNT(*) FROM {EDGES_TABLE_NAME} WHERE {COUNT_IDX_DIRECTED_COLUMN_NAME} = 1"
5✔
1030

1031
        num_edges_directed_result = network_data.query_edges(query_num_edges_directed)
5✔
1032
        num_edges_directed = num_edges_directed_result.columns[0][0].as_py()
5✔
1033

1034
        query_num_edges_undirected = f"SELECT COUNT(*) FROM {EDGES_TABLE_NAME} WHERE {COUNT_IDX_UNDIRECTED_COLUMN_NAME} = 1"
5✔
1035
        num_edges_undirected_result = network_data.query_edges(
5✔
1036
            query_num_edges_undirected
1037
        )
1038
        num_edges_undirected = num_edges_undirected_result.columns[0][0].as_py()
5✔
1039

1040
        self_loop_query = f"SELECT count(*) FROM {EDGES_TABLE_NAME} WHERE {SOURCE_COLUMN_NAME} = {TARGET_COLUMN_NAME}"
5✔
1041
        self_loop_result = network_data.query_edges(self_loop_query)
5✔
1042
        num_self_loops = self_loop_result.columns[0][0].as_py()
5✔
1043

1044
        num_parallel_edges_directed_query = f"SELECT COUNT(*) FROM {EDGES_TABLE_NAME} WHERE {COUNT_IDX_DIRECTED_COLUMN_NAME} = 2"
5✔
1045
        num_parallel_edges_directed_result = network_data.query_edges(
5✔
1046
            num_parallel_edges_directed_query
1047
        )
1048
        num_parallel_edges_directed = num_parallel_edges_directed_result.columns[0][
5✔
1049
            0
1050
        ].as_py()
1051

1052
        num_parallel_edges_undirected_query = f"SELECT COUNT(*) FROM {EDGES_TABLE_NAME} WHERE {COUNT_IDX_UNDIRECTED_COLUMN_NAME} = 2"
5✔
1053
        num_parallel_edges_undirected_result = network_data.query_edges(
5✔
1054
            num_parallel_edges_undirected_query
1055
        )
1056
        num_parallel_edges_undirected = num_parallel_edges_undirected_result.columns[0][
5✔
1057
            0
1058
        ].as_py()
1059

1060
        directed_props = GraphProperties(number_of_edges=num_edges_directed)
5✔
1061
        undirected_props = GraphProperties(number_of_edges=num_edges_undirected)
5✔
1062
        directed_multi_props = GraphProperties(
5✔
1063
            number_of_edges=num_edges, parallel_edges=num_parallel_edges_directed
1064
        )
1065
        undirected_multi_props = GraphProperties(
5✔
1066
            number_of_edges=num_edges, parallel_edges=num_parallel_edges_undirected
1067
        )
1068

1069
        props = {
5✔
1070
            GraphType.DIRECTED.value: directed_props,
1071
            GraphType.DIRECTED_MULTI.value: directed_multi_props,
1072
            GraphType.UNDIRECTED.value: undirected_props,
1073
            GraphType.UNDIRECTED_MULTI.value: undirected_multi_props,
1074
        }
1075

1076
        result = cls(
5✔
1077
            number_of_nodes=num_rows,
1078
            properties_by_graph_type=props,
1079
            number_of_self_loops=num_self_loops,
1080
        )
1081
        return result
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc