graflo 1.3.7__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.

Potentially problematic release.


This version of graflo might be problematic. Click here for more details.

Files changed (70) hide show
  1. graflo/README.md +18 -0
  2. graflo/__init__.py +70 -0
  3. graflo/architecture/__init__.py +38 -0
  4. graflo/architecture/actor.py +1276 -0
  5. graflo/architecture/actor_util.py +450 -0
  6. graflo/architecture/edge.py +418 -0
  7. graflo/architecture/onto.py +376 -0
  8. graflo/architecture/onto_sql.py +54 -0
  9. graflo/architecture/resource.py +163 -0
  10. graflo/architecture/schema.py +135 -0
  11. graflo/architecture/transform.py +292 -0
  12. graflo/architecture/util.py +89 -0
  13. graflo/architecture/vertex.py +562 -0
  14. graflo/caster.py +736 -0
  15. graflo/cli/__init__.py +14 -0
  16. graflo/cli/ingest.py +203 -0
  17. graflo/cli/manage_dbs.py +197 -0
  18. graflo/cli/plot_schema.py +132 -0
  19. graflo/cli/xml2json.py +93 -0
  20. graflo/data_source/__init__.py +48 -0
  21. graflo/data_source/api.py +339 -0
  22. graflo/data_source/base.py +95 -0
  23. graflo/data_source/factory.py +304 -0
  24. graflo/data_source/file.py +148 -0
  25. graflo/data_source/memory.py +70 -0
  26. graflo/data_source/registry.py +82 -0
  27. graflo/data_source/sql.py +183 -0
  28. graflo/db/__init__.py +44 -0
  29. graflo/db/arango/__init__.py +22 -0
  30. graflo/db/arango/conn.py +1025 -0
  31. graflo/db/arango/query.py +180 -0
  32. graflo/db/arango/util.py +88 -0
  33. graflo/db/conn.py +377 -0
  34. graflo/db/connection/__init__.py +6 -0
  35. graflo/db/connection/config_mapping.py +18 -0
  36. graflo/db/connection/onto.py +717 -0
  37. graflo/db/connection/wsgi.py +29 -0
  38. graflo/db/manager.py +119 -0
  39. graflo/db/neo4j/__init__.py +16 -0
  40. graflo/db/neo4j/conn.py +639 -0
  41. graflo/db/postgres/__init__.py +37 -0
  42. graflo/db/postgres/conn.py +948 -0
  43. graflo/db/postgres/fuzzy_matcher.py +281 -0
  44. graflo/db/postgres/heuristics.py +133 -0
  45. graflo/db/postgres/inference_utils.py +428 -0
  46. graflo/db/postgres/resource_mapping.py +273 -0
  47. graflo/db/postgres/schema_inference.py +372 -0
  48. graflo/db/postgres/types.py +148 -0
  49. graflo/db/postgres/util.py +87 -0
  50. graflo/db/tigergraph/__init__.py +9 -0
  51. graflo/db/tigergraph/conn.py +2365 -0
  52. graflo/db/tigergraph/onto.py +26 -0
  53. graflo/db/util.py +49 -0
  54. graflo/filter/__init__.py +21 -0
  55. graflo/filter/onto.py +525 -0
  56. graflo/logging.conf +22 -0
  57. graflo/onto.py +312 -0
  58. graflo/plot/__init__.py +17 -0
  59. graflo/plot/plotter.py +616 -0
  60. graflo/util/__init__.py +23 -0
  61. graflo/util/chunker.py +807 -0
  62. graflo/util/merge.py +150 -0
  63. graflo/util/misc.py +37 -0
  64. graflo/util/onto.py +422 -0
  65. graflo/util/transform.py +454 -0
  66. graflo-1.3.7.dist-info/METADATA +243 -0
  67. graflo-1.3.7.dist-info/RECORD +70 -0
  68. graflo-1.3.7.dist-info/WHEEL +4 -0
  69. graflo-1.3.7.dist-info/entry_points.txt +5 -0
  70. graflo-1.3.7.dist-info/licenses/LICENSE +126 -0
@@ -0,0 +1,376 @@
1
+ """Core ontology and data structures for graph database operations.
2
+
3
+ This module defines the fundamental data structures and types used throughout the graflo
4
+ package for working with graph databases. It provides:
5
+
6
+ - Core data types for vertices and edges
7
+ - Database index configurations
8
+ - Graph container implementations
9
+ - Edge mapping and casting utilities
10
+ - Action context for graph transformations
11
+
12
+ The module is designed to be database-agnostic, supporting both ArangoDB and Neo4j through
13
+ the DBFlavor enum. It provides a unified interface for working with graph data structures
14
+ while allowing for database-specific optimizations and features.
15
+
16
+ Key Components:
17
+ - EdgeMapping: Defines how edges are mapped between vertices
18
+ - IndexType: Supported database index types
19
+ - EdgeType: Types of edge handling in the graph database
20
+ - GraphContainer: Main container for graph data
21
+ - ActionContext: Context for graph transformation operations
22
+
23
+ Example:
24
+ >>> container = GraphContainer(vertices={}, edges={}, linear=[])
25
+ >>> index = Index(fields=["name", "age"], type=IndexType.PERSISTENT)
26
+ >>> context = ActionContext()
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import dataclasses
32
+ import logging
33
+ from abc import ABCMeta
34
+ from collections import defaultdict
35
+ from typing import Any, TypeAlias
36
+
37
+ from dataclass_wizard import JSONWizard, YAMLWizard
38
+
39
+ from graflo.onto import BaseDataclass, BaseEnum, DBFlavor
40
+ from graflo.util.transform import pick_unique_dict
41
+
42
+ # type for vertex or edge name (index)
43
+ EdgeId: TypeAlias = tuple[str, str, str | None]
44
+ GraphEntity: TypeAlias = str | EdgeId
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ class EdgeMapping(BaseEnum):
50
+ """Defines how edges are mapped between vertices.
51
+
52
+ ALL: Maps all vertices to all vertices
53
+ ONE_N: Maps one vertex to many vertices
54
+ """
55
+
56
+ ALL = "all"
57
+ ONE_N = "1-n"
58
+
59
+
60
+ class EncodingType(BaseEnum):
61
+ """Supported character encodings for data input/output."""
62
+
63
+ ISO_8859 = "ISO-8859-1"
64
+ UTF_8 = "utf-8"
65
+
66
+
67
+ class IndexType(BaseEnum):
68
+ """Types of database indexes supported.
69
+
70
+ PERSISTENT: Standard persistent index
71
+ HASH: Hash-based index for fast lookups
72
+ SKIPLIST: Sorted index using skip list data structure
73
+ FULLTEXT: Index optimized for text search
74
+ """
75
+
76
+ PERSISTENT = "persistent"
77
+ HASH = "hash"
78
+ SKIPLIST = "skiplist"
79
+ FULLTEXT = "fulltext"
80
+
81
+
82
+ class EdgeType(BaseEnum):
83
+ """Defines how edges are handled in the graph database.
84
+
85
+ INDIRECT: Defined as a collection with indexes, may be used after data ingestion
86
+ DIRECT: In addition to indexes, these edges are generated during ingestion
87
+ """
88
+
89
+ INDIRECT = "indirect"
90
+ DIRECT = "direct"
91
+
92
+
93
+ @dataclasses.dataclass
94
+ class ABCFields(BaseDataclass, metaclass=ABCMeta):
95
+ """Abstract base class for entities that have fields.
96
+
97
+ Attributes:
98
+ name: Optional name of the entity
99
+ fields: List of field names
100
+ """
101
+
102
+ name: str | None = None
103
+ fields: list[str] = dataclasses.field(default_factory=list)
104
+ keep_vertex_name: bool = True
105
+
106
+ def cfield(self, x: str) -> str:
107
+ """Creates a composite field name by combining the entity name with a field name.
108
+
109
+ Args:
110
+ x: Field name to combine with entity name
111
+
112
+ Returns:
113
+ Composite field name in format "entity@field"
114
+ """
115
+ return f"{self.name}@{x}" if self.keep_vertex_name else x
116
+
117
+
118
+ @dataclasses.dataclass
119
+ class Weight(ABCFields):
120
+ """Defines weight configuration for edges.
121
+
122
+ Attributes:
123
+ map: Dictionary mapping field values to weights
124
+ filter: Dictionary of filter conditions for weights
125
+ """
126
+
127
+ map: dict = dataclasses.field(default_factory=dict)
128
+ filter: dict = dataclasses.field(default_factory=dict)
129
+
130
+
131
+ @dataclasses.dataclass
132
+ class Index(BaseDataclass):
133
+ """Configuration for database indexes.
134
+
135
+ Attributes:
136
+ name: Optional name of the index
137
+ fields: List of fields to index
138
+ unique: Whether the index enforces uniqueness
139
+ type: Type of index to create
140
+ deduplicate: Whether to deduplicate index entries
141
+ sparse: Whether to create a sparse index
142
+ exclude_edge_endpoints: Whether to exclude edge endpoints from index
143
+ """
144
+
145
+ name: str | None = None
146
+ fields: list[str] = dataclasses.field(default_factory=list)
147
+ unique: bool = True
148
+ type: IndexType = IndexType.PERSISTENT
149
+ deduplicate: bool = True
150
+ sparse: bool = False
151
+ exclude_edge_endpoints: bool = False
152
+
153
+ def __iter__(self):
154
+ """Iterate over the indexed fields."""
155
+ return iter(self.fields)
156
+
157
+ def db_form(self, db_type: DBFlavor) -> dict:
158
+ """Convert index configuration to database-specific format.
159
+
160
+ Args:
161
+ db_type: Type of database (ARANGO or NEO4J)
162
+
163
+ Returns:
164
+ Dictionary of index configuration in database-specific format
165
+
166
+ Raises:
167
+ ValueError: If db_type is not supported
168
+ """
169
+ r = self.to_dict()
170
+ if db_type == DBFlavor.ARANGO:
171
+ _ = r.pop("name")
172
+ _ = r.pop("exclude_edge_endpoints")
173
+ elif db_type == DBFlavor.NEO4J:
174
+ pass
175
+ else:
176
+ raise ValueError(f"Unknown db_type {db_type}")
177
+
178
+ return r
179
+
180
+
181
+ class ItemsView:
182
+ """View class for iterating over vertices and edges in a GraphContainer."""
183
+
184
+ def __init__(self, gc: GraphContainer):
185
+ self._dictlike = gc
186
+
187
+ def __iter__(self):
188
+ """Iterate over vertices and edges in the container."""
189
+ for key in self._dictlike.vertices:
190
+ yield key, self._dictlike.vertices[key]
191
+ for key in self._dictlike.edges:
192
+ yield key, self._dictlike.edges[key]
193
+
194
+
195
+ @dataclasses.dataclass
196
+ class GraphContainer(BaseDataclass):
197
+ """Container for graph data including vertices and edges.
198
+
199
+ Attributes:
200
+ vertices: Dictionary mapping vertex names to lists of vertex data
201
+ edges: Dictionary mapping edge IDs to lists of edge data
202
+ linear: List of default dictionaries containing linear data
203
+ """
204
+
205
+ vertices: dict[str, list]
206
+ edges: dict[tuple[str, str, str | None], list]
207
+ linear: list[defaultdict[str | tuple[str, str, str | None], list[Any]]]
208
+
209
+ def __post_init__(self):
210
+ pass
211
+
212
+ def items(self):
213
+ """Get an ItemsView of the container's contents."""
214
+ return ItemsView(self)
215
+
216
+ def pick_unique(self):
217
+ """Remove duplicate entries from vertices and edges."""
218
+ for k, v in self.vertices.items():
219
+ self.vertices[k] = pick_unique_dict(v)
220
+ for k, v in self.edges.items():
221
+ self.edges[k] = pick_unique_dict(v)
222
+
223
+ def loop_over_relations(self, edge_def: tuple[str, str, str | None]):
224
+ """Iterate over edges matching the given edge definition.
225
+
226
+ Args:
227
+ edge_def: Tuple of (source, target, optional_purpose)
228
+
229
+ Returns:
230
+ Generator yielding matching edge IDs
231
+ """
232
+ source, target, _ = edge_def
233
+ return (ed for ed in self.edges if source == ed[0] and target == ed[1])
234
+
235
+ @classmethod
236
+ def from_docs_list(
237
+ cls, list_default_dicts: list[defaultdict[GraphEntity, list]]
238
+ ) -> GraphContainer:
239
+ """Create a GraphContainer from a list of default dictionaries.
240
+
241
+ Args:
242
+ list_default_dicts: List of default dictionaries containing vertex and edge data
243
+
244
+ Returns:
245
+ New GraphContainer instance
246
+
247
+ Raises:
248
+ AssertionError: If edge IDs are not properly formatted
249
+ """
250
+ vdict: defaultdict[str, list] = defaultdict(list)
251
+ edict: defaultdict[tuple[str, str, str | None], list] = defaultdict(list)
252
+
253
+ for d in list_default_dicts:
254
+ for k, v in d.items():
255
+ if isinstance(k, str):
256
+ vdict[k].extend(v)
257
+ elif isinstance(k, tuple):
258
+ assert (
259
+ len(k) == 3
260
+ and all(isinstance(item, str) for item in k[:-1])
261
+ and isinstance(k[-1], (str, type(None)))
262
+ )
263
+ edict[k].extend(v)
264
+ return GraphContainer(
265
+ vertices=dict(vdict.items()),
266
+ edges=dict(edict.items()),
267
+ linear=list_default_dicts,
268
+ )
269
+
270
+
271
+ class EdgeCastingType(BaseEnum):
272
+ """Types of edge casting supported.
273
+
274
+ PAIR: Edges are cast as pairs of vertices
275
+ PRODUCT: Edges are cast as combinations of vertex sets
276
+ """
277
+
278
+ PAIR = "pair"
279
+ PRODUCT = "product"
280
+ COMBINATIONS = "combinations"
281
+
282
+
283
+ def inner_factory_vertex() -> defaultdict[LocationIndex, list]:
284
+ """Create a default dictionary for vertex data."""
285
+ return defaultdict(list)
286
+
287
+
288
+ def outer_factory() -> defaultdict[str, defaultdict[LocationIndex, list]]:
289
+ """Create a nested default dictionary for vertex data."""
290
+ return defaultdict(inner_factory_vertex)
291
+
292
+
293
+ def dd_factory() -> defaultdict[GraphEntity, list]:
294
+ """Create a default dictionary for graph entity data."""
295
+ return defaultdict(list)
296
+
297
+
298
+ @dataclasses.dataclass(kw_only=True)
299
+ class VertexRep(BaseDataclass):
300
+ """Context for graph transformation actions.
301
+
302
+ Attributes:
303
+ vertex: doc representing a vertex
304
+ ctx: context (for edge definition upstream
305
+ """
306
+
307
+ vertex: dict
308
+ ctx: dict
309
+
310
+
311
+ @dataclasses.dataclass(frozen=True, eq=True)
312
+ class LocationIndex(JSONWizard, YAMLWizard):
313
+ path: tuple[str | int | None, ...] = dataclasses.field(default_factory=tuple)
314
+
315
+ def extend(self, extension: tuple[str | int | None, ...]) -> LocationIndex:
316
+ return LocationIndex((*self.path, *extension))
317
+
318
+ def depth(self):
319
+ return len(self.path)
320
+
321
+ def congruence_measure(self, other: LocationIndex):
322
+ neq_position = 0
323
+ for step_a, step_b in zip(self.path, other.path):
324
+ if step_a != step_b:
325
+ break
326
+ neq_position += 1
327
+ return neq_position
328
+
329
+ def filter(self, lindex_list: list[LocationIndex]) -> list[LocationIndex]:
330
+ return [
331
+ t
332
+ for t in lindex_list
333
+ if t.depth() >= self.depth() and t.path[: self.depth()] == self.path
334
+ ]
335
+
336
+ def __lt__(self, other: LocationIndex):
337
+ return len(self.path) < len(other.path)
338
+
339
+ def __contains__(self, item):
340
+ return item in self.path
341
+
342
+ def __len__(self):
343
+ return len(self.path)
344
+
345
+ def __iter__(self):
346
+ return iter(self.path)
347
+
348
+ def __getitem__(self, item):
349
+ return self.path[item]
350
+
351
+
352
+ @dataclasses.dataclass(kw_only=True)
353
+ class ActionContext(BaseDataclass):
354
+ """Context for graph transformation actions.
355
+
356
+ Attributes:
357
+ acc_vertex: Local accumulation of vertices
358
+ acc_global: Global accumulation of graph entities
359
+ buffer_vertex: Buffer for vertex data
360
+ buffer_transforms: Buffer for transforms data
361
+ target_vertices: Set of target vertex names indicating user intention
362
+ """
363
+
364
+ acc_vertex: defaultdict[str, defaultdict[LocationIndex, list]] = dataclasses.field(
365
+ default_factory=outer_factory
366
+ )
367
+ acc_global: defaultdict[GraphEntity, list] = dataclasses.field(
368
+ default_factory=dd_factory
369
+ )
370
+ buffer_vertex: defaultdict[GraphEntity, list] = dataclasses.field(
371
+ default_factory=lambda: defaultdict(list)
372
+ )
373
+ buffer_transforms: defaultdict[LocationIndex, list[dict]] = dataclasses.field(
374
+ default_factory=lambda: defaultdict(list)
375
+ )
376
+ target_vertices: set[str] = dataclasses.field(default_factory=set)
@@ -0,0 +1,54 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class ColumnInfo(BaseModel):
5
+ """Column information from PostgreSQL table."""
6
+
7
+ name: str
8
+ type: str
9
+ description: str = ""
10
+ is_nullable: str = "YES"
11
+ column_default: str | None = None
12
+ is_pk: bool = False
13
+
14
+
15
+ class ForeignKeyInfo(BaseModel):
16
+ """Foreign key relationship information."""
17
+
18
+ column: str
19
+ references_table: str
20
+ references_column: str | None = None
21
+ constraint_name: str | None = None
22
+
23
+
24
+ class VertexTableInfo(BaseModel):
25
+ """Vertex table information from schema introspection."""
26
+
27
+ name: str
28
+ schema_name: str
29
+ columns: list[ColumnInfo]
30
+ primary_key: list[str]
31
+ foreign_keys: list[ForeignKeyInfo]
32
+
33
+
34
+ class EdgeTableInfo(BaseModel):
35
+ """Edge table information from schema introspection."""
36
+
37
+ name: str
38
+ schema_name: str
39
+ columns: list[ColumnInfo]
40
+ primary_key: list[str]
41
+ foreign_keys: list[ForeignKeyInfo]
42
+ source_table: str
43
+ target_table: str
44
+ source_column: str
45
+ target_column: str
46
+ relation: str | None = None
47
+
48
+
49
+ class SchemaIntrospectionResult(BaseModel):
50
+ """Result of PostgreSQL schema introspection."""
51
+
52
+ vertex_tables: list[VertexTableInfo]
53
+ edge_tables: list[EdgeTableInfo]
54
+ schema_name: str
@@ -0,0 +1,163 @@
1
+ """Resource management and processing for graph databases.
2
+
3
+ This module provides the core resource handling functionality for graph databases.
4
+ It defines how data resources are processed, transformed, and mapped to graph
5
+ structures through a system of actors and transformations.
6
+
7
+ Key Components:
8
+ - Resource: Main class for resource processing and transformation
9
+ - ActorWrapper: Wrapper for processing actors
10
+ - ActionContext: Context for processing actions
11
+
12
+ The resource system allows for:
13
+ - Data encoding and transformation
14
+ - Vertex and edge creation
15
+ - Weight management
16
+ - Collection merging
17
+ - Type casting and validation
18
+
19
+ Example:
20
+ >>> resource = Resource(
21
+ ... resource_name="users",
22
+ ... apply=[VertexActor("user"), EdgeActor("follows")],
23
+ ... encoding=EncodingType.UTF_8
24
+ ... )
25
+ >>> result = resource(doc)
26
+ """
27
+
28
+ import dataclasses
29
+ import logging
30
+ from collections import defaultdict
31
+ from typing import Callable
32
+
33
+ from dataclass_wizard import JSONWizard
34
+
35
+ from graflo.architecture.actor import (
36
+ ActorWrapper,
37
+ )
38
+ from graflo.architecture.edge import Edge, EdgeConfig
39
+ from graflo.architecture.onto import (
40
+ ActionContext,
41
+ EncodingType,
42
+ GraphEntity,
43
+ )
44
+ from graflo.architecture.transform import ProtoTransform
45
+ from graflo.architecture.vertex import (
46
+ VertexConfig,
47
+ )
48
+ from graflo.onto import BaseDataclass
49
+
50
+ logger = logging.getLogger(__name__)
51
+
52
+
53
+ @dataclasses.dataclass(kw_only=True)
54
+ class Resource(BaseDataclass, JSONWizard):
55
+ """Resource configuration and processing.
56
+
57
+ This class represents a data resource that can be processed and transformed
58
+ into graph structures. It manages the processing pipeline through actors
59
+ and handles data encoding, transformation, and mapping.
60
+
61
+ Attributes:
62
+ resource_name: Name of the resource
63
+ apply: List of actors to apply in sequence
64
+ encoding: Data encoding type (default: UTF_8)
65
+ merge_collections: List of collections to merge
66
+ extra_weights: List of additional edge weights
67
+ types: Dictionary of field type mappings
68
+ root: Root actor wrapper for processing
69
+ vertex_config: Configuration for vertices
70
+ edge_config: Configuration for edges
71
+ """
72
+
73
+ resource_name: str
74
+ apply: list
75
+ encoding: EncodingType = EncodingType.UTF_8
76
+ merge_collections: list[str] = dataclasses.field(default_factory=list)
77
+ extra_weights: list[Edge] = dataclasses.field(default_factory=list)
78
+ types: dict[str, str] = dataclasses.field(default_factory=dict)
79
+ edge_greedy: bool = True
80
+
81
+ def __post_init__(self):
82
+ """Initialize the resource after dataclass initialization.
83
+
84
+ Sets up the actor wrapper and type mappings. Evaluates type expressions
85
+ for field type casting.
86
+
87
+ Raises:
88
+ Exception: If type evaluation fails for any field
89
+ """
90
+ self.root = ActorWrapper(*self.apply)
91
+ self._types: dict[str, Callable] = dict()
92
+ self.vertex_config: VertexConfig
93
+ self.edge_config: EdgeConfig
94
+ for k, v in self.types.items():
95
+ try:
96
+ self._types[k] = eval(v)
97
+ except Exception as ex:
98
+ logger.error(
99
+ f"For resource {self.name} for field {k} failed to cast type {v} : {ex}"
100
+ )
101
+
102
+ @property
103
+ def name(self):
104
+ """Get the resource name.
105
+
106
+ Returns:
107
+ str: Name of the resource
108
+ """
109
+ return self.resource_name
110
+
111
+ def finish_init(
112
+ self,
113
+ vertex_config: VertexConfig,
114
+ edge_config: EdgeConfig,
115
+ transforms: dict[str, ProtoTransform],
116
+ ):
117
+ """Complete resource initialization.
118
+
119
+ Initializes the resource with vertex and edge configurations,
120
+ and sets up the processing pipeline.
121
+
122
+ Args:
123
+ vertex_config: Configuration for vertices
124
+ edge_config: Configuration for edges
125
+ transforms: Dictionary of available transforms
126
+ """
127
+ self.vertex_config = vertex_config
128
+ self.edge_config = edge_config
129
+
130
+ logger.debug(f"total resource actor count : {self.root.count()}")
131
+ self.root.finish_init(
132
+ vertex_config=vertex_config,
133
+ transforms=transforms,
134
+ edge_config=edge_config,
135
+ edge_greedy=self.edge_greedy,
136
+ )
137
+
138
+ logger.debug(f"total resource actor count (after 2 finit): {self.root.count()}")
139
+
140
+ for e in self.extra_weights:
141
+ e.finish_init(vertex_config)
142
+
143
+ def __call__(self, doc: dict) -> defaultdict[GraphEntity, list]:
144
+ """Process a document through the resource pipeline.
145
+
146
+ Args:
147
+ doc: Document to process
148
+
149
+ Returns:
150
+ defaultdict[GraphEntity, list]: Processed graph entities
151
+ """
152
+ ctx = ActionContext()
153
+ ctx = self.root(ctx, doc=doc)
154
+ acc = self.root.normalize_ctx(ctx)
155
+ return acc
156
+
157
+ def count(self):
158
+ """Get the total number of actors in the resource.
159
+
160
+ Returns:
161
+ int: Number of actors
162
+ """
163
+ return self.root.count()