Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 36 additions & 17 deletions TM1py/Services/HierarchyService.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@

import json
import math
from collections import defaultdict
from typing import Dict, Iterable, List, Optional, Tuple

try:
import networkx as nx

_has_networkx = True
except ImportError:
_has_networkx = False

from requests import Response

Expand All @@ -32,7 +27,6 @@
case_and_space_insensitive_equals,
format_url,
require_data_admin,
require_networkx,
require_ops_admin,
require_pandas,
verify_version,
Expand Down Expand Up @@ -61,22 +55,47 @@ def __init__(self, rest: RestService):
self.elements = ElementService(rest)

@staticmethod
@require_networkx
def _validate_edges(df: "pd.DataFrame"):
graph = nx.DiGraph()
for _, *record in df.itertuples():
child = record[0]
for parent in record[1:]:
graph = defaultdict(list) # Build adjacency list (child -> list of parents)
for row in df.itertuples(index=False, name=None):
child, *parents = row
for parent in parents:
if not parent:
continue
if isinstance(parent, float) and math.isnan(parent):
continue
graph.add_edge(child, parent)
graph[child].append(parent)
child = parent

cycles = list(nx.simple_cycles(graph))
visited = set() # nodes already fully explored
active_path = set() # nodes currently being explored (for cycle detection)
cycles = [] # stores detected cycles

def explore_relationships(node, path):
if node in active_path:
# Found a cycle: extract the part of the path that loops
loop_start = path.index(node)
cycles.append(path[loop_start:])
return
if node in visited:
return

visited.add(node)
active_path.add(node)

for parent in graph.get(node, []):
explore_relationships(parent, path + [parent])

active_path.remove(node)

for node in graph:
if node not in visited:
explore_relationships(node, [node])

if cycles:
raise ValueError(f"Circular reference{'s' if len(cycles) > 1 else ''} found in edges: {cycles}")
raise ValueError(
f"Circular reference{'s' if len(cycles) > 1 else ''} found in edges: {cycles}"
)

@staticmethod
def _validate_alias_uniqueness(df: "pd.DataFrame"):
Expand Down Expand Up @@ -458,7 +477,7 @@ def update_or_create_hierarchy_from_dataframe(
df: "pd.DataFrame",
element_column: str = None,
verify_unique_elements: bool = False,
verify_edges: bool = True,
verify_edges: bool = False,
element_type_column: str = "ElementType",
unwind_all: bool = False,
unwind_consolidations: Iterable = None,
Expand Down Expand Up @@ -492,7 +511,7 @@ def update_or_create_hierarchy_from_dataframe(
:param verify_unique_elements:
Abort early if element names are not unique
:param verify_edges:
Abort early if edges contain a circular reference
Abort early if edges contain a circular reference.
:param unwind_all: bool
Unwind hierarch before creating new edges
:param unwind_consolidations: list
Expand Down
14 changes: 0 additions & 14 deletions TM1py/Utils/Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,6 @@ def wrapper(self, *args, **kwargs):
return wrapper


@decohints
def require_networkx(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
try:
import networkx # noqa: F401

return func(*args, **kwargs)
except ImportError:
raise ImportError(f"Function '{func.__name__}' requires networkx")

return wrapper


def get_all_servers_from_adminhost(adminhost="localhost", port=None, use_ssl=False) -> List:
from TM1py.Objects import Server

Expand Down
2 changes: 2 additions & 0 deletions Tests/HierarchyService_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,7 @@ def test_update_or_create_hierarchy_from_dataframe_circular_reference(self):
element_column=self.region_dimension_name,
element_type_column="ElementType",
unwind_all=True,
verify_edges=True
)

def test_update_or_create_hierarchy_from_dataframe_circular_references(self):
Expand Down Expand Up @@ -1336,6 +1337,7 @@ def test_update_or_create_hierarchy_from_dataframe_circular_references(self):
element_column=self.region_dimension_name,
element_type_column="ElementType",
unwind_all=True,
verify_edges=True
)

def test_update_or_create_hierarchy_from_dataframe_no_weight_columns(self):
Expand Down
3 changes: 1 addition & 2 deletions Tests/_gh_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,7 @@ install_requires=[
'requests',
'pytz',
'requests_negotiate_sspi;platform_system=="Windows"',
'mdxpy>=1.3.1',
'networkx'],
'mdxpy>=1.3.1'],
extras_require={
"pandas": ["pandas"],
"dev": [
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
name="TM1py",
packages=["TM1py", "TM1py/Exceptions", "TM1py/Objects", "TM1py/Services", "TM1py/Utils"],
version=SCHEDULE_VERSION,
description="A python module for TM1.",
description="The python module for TM1.",
long_description=long_description,
long_description_content_type="text/markdown",
license="MIT",
Expand All @@ -29,6 +29,8 @@
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Natural Language :: English",
],
install_requires=[
Expand All @@ -37,7 +39,6 @@
"pytz",
'requests_negotiate_sspi;platform_system=="Windows"',
"mdxpy>=1.3.1",
"networkx",
],
extras_require={
"pandas": ["pandas"],
Expand Down