lss4py 0.1.0__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.
- lss4py/__init__.py +53 -0
- lss4py/adapters/__init__.py +4 -0
- lss4py/adapters/event_log.py +8 -0
- lss4py/adapters/tabular.py +7 -0
- lss4py/analytics/__init__.py +3 -0
- lss4py/analytics/capability.py +6 -0
- lss4py/analytics/descriptive.py +8 -0
- lss4py/analytics/spc.py +6 -0
- lss4py/cli.py +16 -0
- lss4py/domain/__init__.py +37 -0
- lss4py/domain/models.py +126 -0
- lss4py/domain/validation.py +30 -0
- lss4py/errors.py +14 -0
- lss4py/observability/__init__.py +4 -0
- lss4py/observability/logging.py +35 -0
- lss4py/observability/telemetry.py +19 -0
- lss4py/reporting/__init__.py +3 -0
- lss4py/reporting/bundles.py +14 -0
- lss4py/workflows/__init__.py +3 -0
- lss4py/workflows/bottleneck.py +6 -0
- lss4py/workflows/comparison.py +6 -0
- lss4py/workflows/process_map.py +49 -0
- lss4py/workflows/simulation.py +6 -0
- lss4py/yellow_belt.py +686 -0
- lss4py-0.1.0.dist-info/METADATA +303 -0
- lss4py-0.1.0.dist-info/RECORD +28 -0
- lss4py-0.1.0.dist-info/WHEEL +4 -0
- lss4py-0.1.0.dist-info/entry_points.txt +3 -0
lss4py/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from lss4py.domain import (
|
|
2
|
+
EventLog,
|
|
3
|
+
EventLogBindingsIR,
|
|
4
|
+
LeadTimeBindingIR,
|
|
5
|
+
MetricBindingsIR,
|
|
6
|
+
MetricSemanticsIR,
|
|
7
|
+
ProcessEdgeIR,
|
|
8
|
+
ProcessModelIR,
|
|
9
|
+
ProcessStepIR,
|
|
10
|
+
StageBindingsIR,
|
|
11
|
+
validate_event_log,
|
|
12
|
+
)
|
|
13
|
+
from lss4py.errors import AdapterError, LSS4PyError, ValidationError, WorkflowError
|
|
14
|
+
from lss4py.workflows import map_process
|
|
15
|
+
from lss4py.yellow_belt import (
|
|
16
|
+
booked_to_completed_yield,
|
|
17
|
+
created_to_booked_yield,
|
|
18
|
+
lead_time,
|
|
19
|
+
process_cycle_efficiency,
|
|
20
|
+
rolled_yield,
|
|
21
|
+
takt_time,
|
|
22
|
+
wait_time,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AdapterError",
|
|
27
|
+
"EventLogBindingsIR",
|
|
28
|
+
"EventLog",
|
|
29
|
+
"LSS4PyError",
|
|
30
|
+
"ValidationError",
|
|
31
|
+
"WorkflowError",
|
|
32
|
+
"booked_to_completed_yield",
|
|
33
|
+
"created_to_booked_yield",
|
|
34
|
+
"lead_time",
|
|
35
|
+
"LeadTimeBindingIR",
|
|
36
|
+
"main",
|
|
37
|
+
"map_process",
|
|
38
|
+
"MetricBindingsIR",
|
|
39
|
+
"MetricSemanticsIR",
|
|
40
|
+
"ProcessEdgeIR",
|
|
41
|
+
"ProcessModelIR",
|
|
42
|
+
"ProcessStepIR",
|
|
43
|
+
"process_cycle_efficiency",
|
|
44
|
+
"rolled_yield",
|
|
45
|
+
"StageBindingsIR",
|
|
46
|
+
"takt_time",
|
|
47
|
+
"validate_event_log",
|
|
48
|
+
"wait_time",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def main() -> None:
|
|
53
|
+
print("lss4py scaffold ready")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from lss4py.domain.models import EventLog, PercentileProfile
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def summarize_cycle_time(
|
|
5
|
+
event_log: EventLog, percentiles: tuple[int, ...] = (50, 90, 95, 99)
|
|
6
|
+
) -> PercentileProfile:
|
|
7
|
+
_ = (event_log, percentiles)
|
|
8
|
+
raise NotImplementedError("Cycle-time percentile summarization is not implemented yet.")
|
lss4py/analytics/spc.py
ADDED
lss4py/cli.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from lss4py.observability import configure_logging
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
@click.option("--debug", is_flag=True, default=False, help="Enable debug logging.")
|
|
9
|
+
def main(debug: bool) -> None:
|
|
10
|
+
configure_logging(debug=debug)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@main.command("doctor")
|
|
14
|
+
def doctor() -> None:
|
|
15
|
+
console = Console()
|
|
16
|
+
console.print("[green]lss4py[/green] scaffold is ready.")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .models import (
|
|
2
|
+
AssumptionNote,
|
|
3
|
+
AssumptionReport,
|
|
4
|
+
ComparisonSlice,
|
|
5
|
+
EventLog,
|
|
6
|
+
EventLogBindingsIR,
|
|
7
|
+
LeadTimeBindingIR,
|
|
8
|
+
MetricBindingsIR,
|
|
9
|
+
MetricResult,
|
|
10
|
+
MetricSemanticsIR,
|
|
11
|
+
PercentileProfile,
|
|
12
|
+
ProcessEdgeIR,
|
|
13
|
+
ProcessModelIR,
|
|
14
|
+
ProcessStepIR,
|
|
15
|
+
ProcessWindow,
|
|
16
|
+
StageBindingsIR,
|
|
17
|
+
)
|
|
18
|
+
from .validation import validate_event_log
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AssumptionNote",
|
|
22
|
+
"AssumptionReport",
|
|
23
|
+
"ComparisonSlice",
|
|
24
|
+
"EventLogBindingsIR",
|
|
25
|
+
"EventLog",
|
|
26
|
+
"LeadTimeBindingIR",
|
|
27
|
+
"MetricBindingsIR",
|
|
28
|
+
"MetricSemanticsIR",
|
|
29
|
+
"MetricResult",
|
|
30
|
+
"PercentileProfile",
|
|
31
|
+
"ProcessEdgeIR",
|
|
32
|
+
"ProcessModelIR",
|
|
33
|
+
"ProcessStepIR",
|
|
34
|
+
"ProcessWindow",
|
|
35
|
+
"StageBindingsIR",
|
|
36
|
+
"validate_event_log",
|
|
37
|
+
]
|
lss4py/domain/models.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(slots=True)
|
|
7
|
+
class AssumptionNote:
|
|
8
|
+
code: str
|
|
9
|
+
message: str
|
|
10
|
+
severity: str = "info"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class AssumptionReport:
|
|
15
|
+
assumptions: list[AssumptionNote] = field(default_factory=list)
|
|
16
|
+
warnings: list[AssumptionNote] = field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(slots=True)
|
|
20
|
+
class ProcessWindow:
|
|
21
|
+
name: str
|
|
22
|
+
start: datetime | None = None
|
|
23
|
+
end: datetime | None = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ValueClassification = Literal["value_add", "non_value_add", "required_non_value_add"]
|
|
27
|
+
EdgeKind = Literal["sequence", "parallel_split", "parallel_join", "rework"]
|
|
28
|
+
ParallelTiming = Literal["critical_path", "sum_all_branches"]
|
|
29
|
+
ObservedDataShape = Literal["event_log_rows"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class LeadTimeBindingIR:
|
|
34
|
+
start_column: str | None = None
|
|
35
|
+
end_column: str | None = None
|
|
36
|
+
unit: Literal["seconds", "minutes", "hours", "days"] = "hours"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(slots=True)
|
|
40
|
+
class StageBindingsIR:
|
|
41
|
+
created: str | None = None
|
|
42
|
+
booked: str | None = None
|
|
43
|
+
completed: str | None = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class EventLogBindingsIR:
|
|
48
|
+
case_id_column: str | None = None
|
|
49
|
+
step_id_column: str | None = None
|
|
50
|
+
started_at_column: str | None = None
|
|
51
|
+
completed_at_column: str | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(slots=True)
|
|
55
|
+
class MetricBindingsIR:
|
|
56
|
+
lead_time: LeadTimeBindingIR = field(default_factory=LeadTimeBindingIR)
|
|
57
|
+
stages: StageBindingsIR = field(default_factory=StageBindingsIR)
|
|
58
|
+
event_log: EventLogBindingsIR = field(default_factory=EventLogBindingsIR)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(slots=True)
|
|
62
|
+
class MetricSemanticsIR:
|
|
63
|
+
parallel_timing: ParallelTiming = "critical_path"
|
|
64
|
+
observed_data_shape: ObservedDataShape = "event_log_rows"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(slots=True)
|
|
68
|
+
class ProcessStepIR:
|
|
69
|
+
id: str
|
|
70
|
+
name: str
|
|
71
|
+
cycle_time_column: str | None = None
|
|
72
|
+
value_classification: ValueClassification = "value_add"
|
|
73
|
+
role_label: str | None = None
|
|
74
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(slots=True)
|
|
78
|
+
class ProcessEdgeIR:
|
|
79
|
+
source_step_id: str
|
|
80
|
+
target_step_id: str
|
|
81
|
+
kind: EdgeKind = "sequence"
|
|
82
|
+
condition: str | None = None
|
|
83
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(slots=True)
|
|
87
|
+
class ProcessModelIR:
|
|
88
|
+
name: str
|
|
89
|
+
schema_version: str = "1.0"
|
|
90
|
+
steps: list[ProcessStepIR] = field(default_factory=list)
|
|
91
|
+
edges: list[ProcessEdgeIR] = field(default_factory=list)
|
|
92
|
+
bindings: MetricBindingsIR = field(default_factory=MetricBindingsIR)
|
|
93
|
+
metric_semantics: MetricSemanticsIR = field(default_factory=MetricSemanticsIR)
|
|
94
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(slots=True)
|
|
98
|
+
class EventLog:
|
|
99
|
+
events: list[dict[str, Any]]
|
|
100
|
+
required_fields: tuple[str, ...] = ("case_id", "activity", "timestamp")
|
|
101
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(slots=True)
|
|
105
|
+
class ComparisonSlice:
|
|
106
|
+
before: EventLog
|
|
107
|
+
after: EventLog
|
|
108
|
+
label: str = "before_after"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(slots=True)
|
|
112
|
+
class MetricResult:
|
|
113
|
+
name: str
|
|
114
|
+
value: float | None
|
|
115
|
+
unit: str | None = None
|
|
116
|
+
notes: list[AssumptionNote] = field(default_factory=list)
|
|
117
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(slots=True)
|
|
121
|
+
class PercentileProfile:
|
|
122
|
+
p50: float | None
|
|
123
|
+
p90: float | None
|
|
124
|
+
p95: float | None
|
|
125
|
+
p99: float | None
|
|
126
|
+
count: int
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from .models import AssumptionNote, AssumptionReport, EventLog
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def validate_event_log(event_log: EventLog) -> AssumptionReport:
|
|
5
|
+
if not event_log.events:
|
|
6
|
+
return AssumptionReport(
|
|
7
|
+
warnings=[
|
|
8
|
+
AssumptionNote(
|
|
9
|
+
code="empty_event_log",
|
|
10
|
+
message="Event log contains zero rows.",
|
|
11
|
+
severity="warning",
|
|
12
|
+
)
|
|
13
|
+
]
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
sample_event = event_log.events[0]
|
|
17
|
+
missing = [field for field in event_log.required_fields if field not in sample_event]
|
|
18
|
+
|
|
19
|
+
if missing:
|
|
20
|
+
return AssumptionReport(
|
|
21
|
+
warnings=[
|
|
22
|
+
AssumptionNote(
|
|
23
|
+
code="missing_required_fields",
|
|
24
|
+
message=f"Missing required fields: {', '.join(missing)}",
|
|
25
|
+
severity="warning",
|
|
26
|
+
)
|
|
27
|
+
]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return AssumptionReport()
|
lss4py/errors.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class LSS4PyError(Exception):
|
|
2
|
+
"""Base exception for lss4py."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ValidationError(LSS4PyError):
|
|
6
|
+
"""Raised when input data fails validation checks."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AdapterError(LSS4PyError):
|
|
10
|
+
"""Raised when adapter-level IO or parsing fails."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WorkflowError(LSS4PyError):
|
|
14
|
+
"""Raised when workflow orchestration fails."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import structlog
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def configure_logging(debug: bool = False) -> None:
|
|
9
|
+
log_level = logging.DEBUG if debug else logging.INFO
|
|
10
|
+
|
|
11
|
+
logging.basicConfig(
|
|
12
|
+
format="%(message)s",
|
|
13
|
+
stream=sys.stdout,
|
|
14
|
+
level=log_level,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
processors: list[Any] = [
|
|
18
|
+
structlog.contextvars.merge_contextvars,
|
|
19
|
+
structlog.processors.add_log_level,
|
|
20
|
+
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
|
21
|
+
structlog.processors.StackInfoRenderer(),
|
|
22
|
+
structlog.processors.format_exc_info,
|
|
23
|
+
structlog.processors.JSONRenderer(),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
structlog.configure(
|
|
27
|
+
processors=processors,
|
|
28
|
+
wrapper_class=structlog.make_filtering_bound_logger(log_level),
|
|
29
|
+
logger_factory=structlog.PrintLoggerFactory(file=sys.stdout),
|
|
30
|
+
cache_logger_on_first_use=True,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_logger(name: str | None = None) -> structlog.stdlib.BoundLogger:
|
|
35
|
+
return structlog.get_logger(name)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from typing import Final
|
|
2
|
+
|
|
3
|
+
from opentelemetry import trace
|
|
4
|
+
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
|
|
5
|
+
from opentelemetry.sdk.trace import TracerProvider
|
|
6
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
|
|
7
|
+
|
|
8
|
+
DEFAULT_SERVICE_NAME: Final[str] = "lss4py"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def configure_telemetry(service_name: str = DEFAULT_SERVICE_NAME) -> None:
|
|
12
|
+
resource = Resource.create({SERVICE_NAME: service_name})
|
|
13
|
+
provider = TracerProvider(resource=resource)
|
|
14
|
+
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
|
|
15
|
+
trace.set_tracer_provider(provider)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_tracer(name: str = DEFAULT_SERVICE_NAME):
|
|
19
|
+
return trace.get_tracer(name)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(slots=True)
|
|
6
|
+
class AnalysisBundle:
|
|
7
|
+
title: str
|
|
8
|
+
sections: list[dict[str, Any]] = field(default_factory=list)
|
|
9
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def to_markdown(bundle: AnalysisBundle) -> str:
|
|
13
|
+
_ = bundle
|
|
14
|
+
raise NotImplementedError("Markdown bundle rendering is not implemented yet.")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, TypedDict
|
|
4
|
+
|
|
5
|
+
from lss4py.domain.models import EventLog
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProcessMapResult(TypedDict):
|
|
9
|
+
nodes: list[str]
|
|
10
|
+
edges: dict[str, dict[str, int]]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_timestamp(value: Any) -> datetime:
|
|
14
|
+
if isinstance(value, datetime):
|
|
15
|
+
return value
|
|
16
|
+
|
|
17
|
+
if isinstance(value, str):
|
|
18
|
+
return datetime.fromisoformat(value)
|
|
19
|
+
|
|
20
|
+
raise TypeError("Event timestamps must be datetime objects or ISO-8601 strings.")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def map_process(event_log: EventLog) -> ProcessMapResult:
|
|
24
|
+
grouped_events: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
25
|
+
|
|
26
|
+
for event in event_log.events:
|
|
27
|
+
case_id = str(event["case_id"])
|
|
28
|
+
grouped_events[case_id].append(event)
|
|
29
|
+
|
|
30
|
+
nodes: set[str] = set()
|
|
31
|
+
edges: dict[str, dict[str, int]] = {}
|
|
32
|
+
|
|
33
|
+
for case_events in grouped_events.values():
|
|
34
|
+
ordered_events = sorted(case_events, key=lambda event: _parse_timestamp(event["timestamp"]))
|
|
35
|
+
|
|
36
|
+
for event in ordered_events:
|
|
37
|
+
nodes.add(str(event["activity"]))
|
|
38
|
+
|
|
39
|
+
for current_event, next_event in zip(ordered_events, ordered_events[1:], strict=False):
|
|
40
|
+
current_activity = str(current_event["activity"])
|
|
41
|
+
next_activity = str(next_event["activity"])
|
|
42
|
+
edges.setdefault(current_activity, {})
|
|
43
|
+
transition_count = edges[current_activity].get(next_activity, 0) + 1
|
|
44
|
+
edges[current_activity][next_activity] = transition_count
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"nodes": sorted(nodes),
|
|
48
|
+
"edges": edges,
|
|
49
|
+
}
|