Skip to content

Commit

Permalink
Merge pull request #10 from meaningfy-ws/feature/MSSDK1-17
Browse files Browse the repository at this point in the history
Feature/mssdk1 17
  • Loading branch information
duprijil authored Feb 28, 2025
2 parents f98d903 + e102cf8 commit 5ac3dde
Show file tree
Hide file tree
Showing 10 changed files with 411 additions and 19 deletions.
12 changes: 6 additions & 6 deletions mapping_suite_sdk/adapters/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from pathlib import Path
from typing import Generator, Protocol

from mapping_suite_sdk.adapters.tracer import traced_class


class Extractor(Protocol):
"""Protocol defining the interface for file extract operations.
Expand All @@ -13,9 +15,8 @@ class Extractor(Protocol):
a context manager that handles the extraction process and cleanup.
"""

@staticmethod
@contextmanager
def extract_temporary(source_path: Path) -> Generator[Path, None, None]:
def extract_temporary(self, source_path: Path) -> Generator[Path, None, None]:
"""Extract content to a temporary directory and yield its path.
This context manager should handle the extraction of files to a temporary
Expand All @@ -33,6 +34,7 @@ def extract_temporary(source_path: Path) -> Generator[Path, None, None]:
raise NotImplementedError


@traced_class
class ArchiveExtractor(Extractor):
"""Implementation of Extractor protocol for ZIP file operations.
Expand All @@ -41,9 +43,8 @@ class ArchiveExtractor(Extractor):
- Pack directories into ZIP files without including the root directory name
"""

@staticmethod
@contextmanager
def extract_temporary(archive_path: Path) -> Generator[Path, None, None]:
def extract_temporary(self, archive_path: Path) -> Generator[Path, None, None]:
"""Extract a ZIP archive to a temporary directory and yield its path.
This context manager handles the extraction of ZIP files to a temporary
Expand Down Expand Up @@ -84,8 +85,7 @@ def extract_temporary(archive_path: Path) -> Generator[Path, None, None]:
except Exception as e:
raise ValueError(f"Failed to extract ZIP file: {e}")

@staticmethod
def pack_directory(source_dir: Path, output_path: Path) -> Path:
def pack_directory(self, source_dir: Path, output_path: Path) -> Path:
"""Pack a directory's contents into a ZIP file without including the root directory name.
Creates a ZIP file containing the contents of the specified directory.
Expand Down
9 changes: 6 additions & 3 deletions mapping_suite_sdk/adapters/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from pydantic import TypeAdapter

from mapping_suite_sdk.adapters.tracer import traced_class
from mapping_suite_sdk.models.asset import TechnicalMappingSuite, VocabularyMappingSuite, TestDataSuite, \
SAPRQLTestSuite, SHACLTestSuite, TestResultSuite, RMLMappingAsset, \
ConceptualMappingPackageAsset, VocabularyMappingAsset, TestDataAsset, SPARQLQueryAsset, SHACLShapesAsset
Expand Down Expand Up @@ -109,9 +110,10 @@ def load(self, package_folder_path: Path) -> List[TestDataSuite]:
for ts_suite in (package_folder_path / RELATIVE_TEST_DATA_PATH).iterdir():
if ts_suite.is_dir():
test_data_suites.append(TestDataSuite(path=ts_suite.relative_to(package_folder_path),
files=[TestDataAsset(path=ts_file.relative_to(package_folder_path),
content=ts_file.read_text()) for ts_file in
ts_suite.iterdir() if ts_file.is_file()]))
files=[
TestDataAsset(path=ts_file.relative_to(package_folder_path),
content=ts_file.read_text()) for ts_file in
ts_suite.iterdir() if ts_file.is_file()]))
return test_data_suites


Expand Down Expand Up @@ -253,6 +255,7 @@ def load(self, package_folder_path: Path) -> ConceptualMappingPackageAsset:
)


@traced_class
class MappingPackageLoader(MappingPackageAssetLoader):
"""Main loader for complete mapping packages.
Expand Down
2 changes: 2 additions & 0 deletions mapping_suite_sdk/adapters/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from pymongo import MongoClient

from mapping_suite_sdk.adapters.tracer import traced_class
from mapping_suite_sdk.models.core import CoreModel

T = TypeVar('T', bound=CoreModel)
Expand Down Expand Up @@ -38,6 +39,7 @@ def delete(self, model_id: str) -> None:
raise NotImplementedError


@traced_class
class MongoDBRepository(RepositoryABC[T]):
def __init__(
self,
Expand Down
5 changes: 4 additions & 1 deletion mapping_suite_sdk/adapters/serialiser.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
from pathlib import Path
from typing import Any, List, Protocol

from mapping_suite_sdk.adapters.loader import RELATIVE_TECHNICAL_MAPPING_SUITE_PATH, RELATIVE_VOCABULARY_MAPPING_SUITE_PATH, \
from mapping_suite_sdk.adapters.loader import RELATIVE_TECHNICAL_MAPPING_SUITE_PATH, \
RELATIVE_VOCABULARY_MAPPING_SUITE_PATH, \
RELATIVE_SUITE_METADATA_PATH, RELATIVE_CONCEPTUAL_MAPPING_PATH
from mapping_suite_sdk.adapters.tracer import traced_class
from mapping_suite_sdk.models.asset import (
TechnicalMappingSuite, VocabularyMappingSuite, TestDataSuite,
SAPRQLTestSuite, SHACLTestSuite, ConceptualMappingPackageAsset
Expand Down Expand Up @@ -116,6 +118,7 @@ def serialize(self, package_folder_path: Path, asset: ConceptualMappingPackageAs
file_path.write_bytes(asset.content)


@traced_class
class MappingPackageSerialiser(MappingPackageAssetSerialiser):
"""Main serialiser for complete mapping packages."""

Expand Down
155 changes: 155 additions & 0 deletions mapping_suite_sdk/adapters/tracer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
"""
Tracing module for the Mapping Suite SDK.
This module provides functionality to add OpenTelemetry tracing to the SDK,
allowing users to monitor and debug performance and execution flow.
"""

import functools
import os
from enum import Enum

from opentelemetry import trace
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.sdk.trace import TracerProvider

# Environment variable to control tracing state
MSSDK_TRACE_VAR_NAME = "MSSDK_TRACE"
# Setup the OpenTelemetry tracer provider
MSSDK_TRACER_PROVIDER = TracerProvider(resource=Resource(attributes={SERVICE_NAME: "mapping-suite-sdk"}))
trace.set_tracer_provider(MSSDK_TRACER_PROVIDER)


class MSSDKTraceState(Enum):
"""Enumeration for tracing states: ON or OFF."""
OFF = "false"
ON = "true"

def __bool__(self):
"""Convert trace state to boolean: True if tracing is ON, False otherwise."""
return self == MSSDKTraceState.ON


def set_mssdk_tracing(state: MSSDKTraceState) -> MSSDKTraceState:
"""
Set the tracing state for the SDK.
Args:
state: The desired tracing state (ON or OFF)
Returns:
The new tracing state
"""
os.environ[MSSDK_TRACE_VAR_NAME] = state.value
return state


def get_mssdk_tracing() -> MSSDKTraceState:
"""
Get the current tracing state.
Returns:
The current tracing state (defaults to ON if not set)
"""
env_value = os.environ.get(MSSDK_TRACE_VAR_NAME, MSSDKTraceState.ON.value)
return MSSDKTraceState.ON if env_value == "true" else MSSDKTraceState.OFF


def is_mssdk_tracing_enabled() -> bool:
"""
Check if tracing is currently enabled.
Returns:
True if tracing is enabled, False otherwise
"""
return bool(get_mssdk_tracing())


def traced_routine(func):
"""
Decorator to add tracing to a function.
Creates a span for the decorated function, capturing function details,
arguments, and any errors that occur during execution.
Args:
func: The function to trace
Returns:
The wrapped function with tracing capabilities
"""

@functools.wraps(func)
def wrapper(*args, **kwargs):
if is_mssdk_tracing_enabled():
with trace.get_tracer(__name__).start_as_current_span(f"{func.__module__}.{func.__name__}") as span:

span.set_attribute("function.name", func.__name__)
span.set_attribute("function.module", func.__module__)
span.set_attribute("function.args", args)
span.set_attribute("function.args_count", len(args))

try:
func_result = func(*args, **kwargs)

span.set_attribute("function.status", "success")
return func_result

except Exception as e:
span.set_attribute("function.status", "error")
span.set_attribute("error.type", e.__class__.__name__)
span.set_attribute("error.message", str(e))
span.record_exception(e)

raise
else:
return func(*args, **kwargs)

return wrapper


def traced_class(cls):
"""
Class decorator that applies tracing to all methods of a class.
Similar to traced_routine but works on all methods of a class.
Note: Currently does not work with static methods.
Args:
cls: The class to apply tracing to
Returns:
The class with tracing applied to its methods
"""
class_name = cls.__name__

def class_traced_routine(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if is_mssdk_tracing_enabled():
with trace.get_tracer(__name__).start_as_current_span(f"{class_name}.{func.__name__}") as span:
span.set_attribute("function.name", func.__name__)
span.set_attribute("class.name", class_name)
span.set_attribute("function.args_count", len(args))

try:
func_result = func(*args, **kwargs)
span.set_attribute("function.status", "success")
return func_result
except Exception as e:
span.set_attribute("function.status", "error")
span.set_attribute("error.type", e.__class__.__name__)
span.set_attribute("error.message", str(e))
span.record_exception(e)
raise
else:
return func(*args, **kwargs)

return wrapper

for attr_name, attr_value in cls.__dict__.items():
# Skip special methods and non-callable attributes
if callable(attr_value) and not attr_name.startswith('__'):
setattr(cls, attr_name, class_traced_routine(attr_value))

return cls
1 change: 0 additions & 1 deletion mapping_suite_sdk/models/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ class Config:
extra = "forbid"
frozen = False
arbitrary_types_allowed = False
smart_union = True
use_enum_values = True
str_strip_whitespace = False
validate_default = True
Expand Down
8 changes: 6 additions & 2 deletions mapping_suite_sdk/services/load_mapping_package.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from pathlib import Path
from typing import Optional

from mapping_suite_sdk.adapters.loader import MappingPackageAssetLoader, MappingPackageLoader
from mapping_suite_sdk.adapters.extractor import ArchiveExtractor
from mapping_suite_sdk.adapters.loader import MappingPackageAssetLoader, MappingPackageLoader
from mapping_suite_sdk.adapters.repository import MongoDBRepository
from mapping_suite_sdk.adapters.tracer import traced_routine
from mapping_suite_sdk.models.mapping_package import MappingPackage


@traced_routine
def load_mapping_package_from_folder(
mapping_package_folder_path: Path,
mapping_package_loader: Optional[MappingPackageAssetLoader] = None
Expand Down Expand Up @@ -44,6 +46,7 @@ def load_mapping_package_from_folder(
return mapping_package_loader.load(mapping_package_folder_path)


@traced_routine
def load_mapping_package_from_archive(
mapping_package_archive_path: Path,
mapping_package_loader: Optional[MappingPackageAssetLoader] = None,
Expand Down Expand Up @@ -86,6 +89,7 @@ def load_mapping_package_from_archive(
mapping_package_loader=mapping_package_loader)


@traced_routine
def load_mapping_package_from_mongo_db(
mapping_package_id: str,
mapping_package_repository: MongoDBRepository[MappingPackage]
Expand Down Expand Up @@ -120,4 +124,4 @@ def load_mapping_package_from_mongo_db(
if not mapping_package_repository:
raise ValueError("MongoDB repository must be provided")

return mapping_package_repository.read(mapping_package_id)
return mapping_package_repository.read(mapping_package_id)
4 changes: 3 additions & 1 deletion mapping_suite_sdk/services/serialise_mapping_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
from pathlib import Path
from typing import Optional

from mapping_suite_sdk.adapters.serialiser import MappingPackageSerialiser
from mapping_suite_sdk.adapters.extractor import ArchiveExtractor
from mapping_suite_sdk.adapters.serialiser import MappingPackageSerialiser
from mapping_suite_sdk.adapters.tracer import traced_routine
from mapping_suite_sdk.models.mapping_package import MappingPackage


@traced_routine
def serialise_mapping_package(mapping_package: MappingPackage,
serialisation_folder_path: Path,
archive_unpacker: Optional[ArchiveExtractor] = None) -> None:
Expand Down
10 changes: 5 additions & 5 deletions tests/unit/adapters/test_archive_extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@


def test_archive_unpack_successful(dummy_mapping_package_path: Path) -> None:
with ArchiveExtractor.extract_temporary(dummy_mapping_package_path) as extracted_path:
with ArchiveExtractor().extract_temporary(dummy_mapping_package_path) as extracted_path:
assert extracted_path.exists()
assert extracted_path.is_dir()


def test_archive_cleanup_after_context(dummy_mapping_package_path: Path) -> None:
with ArchiveExtractor.extract_temporary(dummy_mapping_package_path) as path:
with ArchiveExtractor().extract_temporary(dummy_mapping_package_path) as path:
extracted_path = path
assert extracted_path.exists()

Expand All @@ -24,21 +24,21 @@ def test_archive_cleanup_after_context(dummy_mapping_package_path: Path) -> None

def test_nonexistent_archive() -> None:
with pytest.raises(FileNotFoundError) as exc_info:
with ArchiveExtractor.extract_temporary(Path("nonexistent.zip")):
with ArchiveExtractor().extract_temporary(Path("nonexistent.zip")):
pass
assert "Archive file not found" in str(exc_info.value)


def test_invalid_archive_path() -> None:
with pytest.raises(ValueError) as exc_info:
with ArchiveExtractor.extract_temporary(Path(__file__)):
with ArchiveExtractor().extract_temporary(Path(__file__)):
pass
assert "Specified path is not a file" in str(exc_info.value)


def test_corrupted_archive(dummy_corrupted_mapping_package_path: Path) -> None:
with pytest.raises(ValueError) as exc_info:
with ArchiveExtractor.extract_temporary(dummy_corrupted_mapping_package_path):
with ArchiveExtractor().extract_temporary(dummy_corrupted_mapping_package_path):
pass
assert "Failed to extract" in str(exc_info.value)

Expand Down
Loading

0 comments on commit 5ac3dde

Please sign in to comment.