ocelescope-module-ocel 0.1.0__tar.gz
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.
- ocelescope_module_ocel-0.1.0/PKG-INFO +17 -0
- ocelescope_module_ocel-0.1.0/README.md +6 -0
- ocelescope_module_ocel-0.1.0/pyproject.toml +27 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/__init__.py +0 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/models/__init__.py +30 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/models/attributes.py +32 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/models/metadata.py +31 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/models/quantities.py +22 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/models/relations.py +42 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/models/time.py +13 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/models/variants.py +33 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/module.py +19 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/routers/__init__.py +28 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/routers/attributes.py +80 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/routers/events.py +104 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/routers/export.py +108 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/routers/management.py +117 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/routers/objects.py +56 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/routers/quantities.py +15 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/routers/relations.py +83 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/util/__init__.py +0 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/util/attributes.py +295 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/util/pagination.py +35 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/util/relations.py +213 -0
- ocelescope_module_ocel-0.1.0/src/ocelescope_module_ocel/util/variants.py +48 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: ocelescope-module-ocel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ocelescope backend module exposing OCEL inspection routes
|
|
5
|
+
Requires-Dist: fastapi>=0.136.0
|
|
6
|
+
Requires-Dist: ocelescope>=0.4.0,<0.5.0
|
|
7
|
+
Requires-Dist: ocelescope-backend>=0.2.0,<0.3.0
|
|
8
|
+
Requires-Dist: polars>=1.40
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# ocelescope-module-ocel
|
|
13
|
+
|
|
14
|
+
Ocelescope backend module exposing the OCEL inspection API (metadata, attributes,
|
|
15
|
+
object/event summaries, relation counts, variants, quantities and export).
|
|
16
|
+
|
|
17
|
+
Mounted at `/modules/ocel/v1`.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ocelescope-module-ocel"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Ocelescope backend module exposing OCEL inspection routes"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"fastapi>=0.136.0",
|
|
9
|
+
"ocelescope>=0.4.0,<0.5.0",
|
|
10
|
+
"ocelescope-backend>=0.2.0,<0.3.0",
|
|
11
|
+
"polars>=1.40",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.entry-points."ocelescope_backend.modules"]
|
|
15
|
+
ocelV1 = "ocelescope_module_ocel.module:Ocel"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.11.20,<0.12.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[tool.uv]
|
|
23
|
+
package = true
|
|
24
|
+
|
|
25
|
+
[tool.uv.sources]
|
|
26
|
+
ocelescope = { workspace = true }
|
|
27
|
+
ocelescope-backend = { workspace = true }
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from ocelescope_module_ocel.models.attributes import (
|
|
2
|
+
AggregatedAttribute,
|
|
3
|
+
Attribute,
|
|
4
|
+
TypedAttribute,
|
|
5
|
+
)
|
|
6
|
+
from ocelescope_module_ocel.models.metadata import OcelMetadata
|
|
7
|
+
from ocelescope_module_ocel.models.quantities import QuantityInfo
|
|
8
|
+
from ocelescope_module_ocel.models.relations import (
|
|
9
|
+
RelationCombination,
|
|
10
|
+
RelationCountSummary,
|
|
11
|
+
)
|
|
12
|
+
from ocelescope_module_ocel.models.time import (
|
|
13
|
+
Date_Distribution_Item,
|
|
14
|
+
Entity_Time_Info,
|
|
15
|
+
)
|
|
16
|
+
from ocelescope_module_ocel.models.variants import ObjectTypeVariants, ObjectVariant
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AggregatedAttribute",
|
|
20
|
+
"Attribute",
|
|
21
|
+
"TypedAttribute",
|
|
22
|
+
"OcelMetadata",
|
|
23
|
+
"QuantityInfo",
|
|
24
|
+
"RelationCombination",
|
|
25
|
+
"RelationCountSummary",
|
|
26
|
+
"Date_Distribution_Item",
|
|
27
|
+
"Entity_Time_Info",
|
|
28
|
+
"ObjectTypeVariants",
|
|
29
|
+
"ObjectVariant",
|
|
30
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ValueType(StrEnum):
|
|
7
|
+
EMPTY = "empty"
|
|
8
|
+
STRING = "string"
|
|
9
|
+
BOOL = "bool"
|
|
10
|
+
INT = "int"
|
|
11
|
+
FLOAT = "float"
|
|
12
|
+
DATE = "date"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Attribute(BaseModel):
|
|
16
|
+
name: str
|
|
17
|
+
min: str | int | float
|
|
18
|
+
max: str | int | float
|
|
19
|
+
distinct_values: int
|
|
20
|
+
type: ValueType
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AggregatedAttribute(Attribute):
|
|
24
|
+
"""An attribute summarized across every entity type that carries it."""
|
|
25
|
+
|
|
26
|
+
entity_type_names: list[str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TypedAttribute(Attribute):
|
|
30
|
+
"""An attribute summarized for a single entity type (activity / object type)."""
|
|
31
|
+
|
|
32
|
+
entity_type: str
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from ocelescope_backend.app.internal.registry import registry_manager
|
|
4
|
+
from ocelescope_backend.app.internal.registry.extension import OCELExtensionDescription
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ocelescope_backend.app.internal.model.ocel import SessionOCEL
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OcelMetadata(BaseModel):
|
|
12
|
+
id: str
|
|
13
|
+
name: str
|
|
14
|
+
created_at: str
|
|
15
|
+
extensions: list[OCELExtensionDescription]
|
|
16
|
+
filter_applied: bool | None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def from_handle(cls, handle: "SessionOCEL", filter_applied: bool | None = None):
|
|
20
|
+
descriptions = registry_manager.get_extension_descriptions()
|
|
21
|
+
return cls(
|
|
22
|
+
id=handle.id,
|
|
23
|
+
created_at=handle.created_at,
|
|
24
|
+
name=handle.name,
|
|
25
|
+
extensions=[
|
|
26
|
+
descriptions[extension.__class__.__name__]
|
|
27
|
+
for extension in handle.extensions
|
|
28
|
+
if extension.__class__.__name__ in descriptions
|
|
29
|
+
],
|
|
30
|
+
filter_applied=filter_applied,
|
|
31
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
from ocelescope import OCEL
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class QuantityInfo(BaseModel):
|
|
8
|
+
item_types: list[str]
|
|
9
|
+
total_object_count: int
|
|
10
|
+
total_event_count: int
|
|
11
|
+
object_types: list[str]
|
|
12
|
+
activities: list[str]
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_ocel(cls, ocel: OCEL) -> Self:
|
|
16
|
+
return cls(
|
|
17
|
+
item_types=ocel.quantities.item_types,
|
|
18
|
+
total_object_count=len(ocel.quantities.objects),
|
|
19
|
+
total_event_count=len(ocel.quantities.events),
|
|
20
|
+
object_types=ocel.quantities.object_types,
|
|
21
|
+
activities=ocel.quantities.activities,
|
|
22
|
+
)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RelationCombination(BaseModel):
|
|
5
|
+
"""A distinct (source, target, qualifier) combination of a relation.
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
source: The source object or event type.
|
|
9
|
+
target: The target object or event type.
|
|
10
|
+
qualifier: The relation qualifier (empty string when none applies).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
source: str
|
|
14
|
+
target: str
|
|
15
|
+
qualifier: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RelationCountSummary(BaseModel):
|
|
19
|
+
"""Summary statistics for relationship multiplicities in O2O or E2O relations.
|
|
20
|
+
|
|
21
|
+
For each (source type, target type[, qualifier]) combination, captures how many
|
|
22
|
+
related items occur per source instance.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
qualifier: The relation qualifier (empty string when none applies).
|
|
26
|
+
qualifiers: The qualifiers aggregated under a (source, target) pair. For
|
|
27
|
+
aggregated rows (``with_qualifier=False``) this lists every qualifier;
|
|
28
|
+
for per-qualifier rows it is the single-element list ``[qualifier]``.
|
|
29
|
+
source: The source object or event type.
|
|
30
|
+
target: The target object or event type.
|
|
31
|
+
min_count: The minimum number of related items for any source instance.
|
|
32
|
+
max_count: The maximum number of related items for any source instance.
|
|
33
|
+
sum: The total number of relation occurrences across all instances.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
qualifier: str
|
|
37
|
+
qualifiers: list[str] = []
|
|
38
|
+
source: str
|
|
39
|
+
target: str
|
|
40
|
+
min_count: int
|
|
41
|
+
max_count: int
|
|
42
|
+
sum: int
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Date_Distribution_Item(BaseModel):
|
|
5
|
+
start_timestamp: str
|
|
6
|
+
end_timestamp: str
|
|
7
|
+
entity_count: dict[str, int]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Entity_Time_Info(BaseModel):
|
|
11
|
+
start_time: str
|
|
12
|
+
end_time: str
|
|
13
|
+
date_distribution: list[Date_Distribution_Item]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ObjectVariant(BaseModel):
|
|
5
|
+
"""A single object variant: a distinct activity sequence objects of a type follow.
|
|
6
|
+
|
|
7
|
+
Attributes:
|
|
8
|
+
variant_id: Identifier of the variant
|
|
9
|
+
(``<object_type>_<hash of the activity sequence>``). It depends only
|
|
10
|
+
on the sequence, so it stays the same across filtered views of a log.
|
|
11
|
+
activities: The ordered activity sequence that defines the variant.
|
|
12
|
+
event_count: Number of events in the variant (length of ``activities``).
|
|
13
|
+
case_count: Number of objects (cases) that follow this variant.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
variant_id: str
|
|
17
|
+
activities: list[str]
|
|
18
|
+
event_count: int
|
|
19
|
+
case_count: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ObjectTypeVariants(BaseModel):
|
|
23
|
+
"""All variants of a single object type together with the totals across them.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
variants: The distinct variants, sorted by frequency (descending).
|
|
27
|
+
case_count: Total number of cases (objects) of the object type.
|
|
28
|
+
event_count: Total number of events across all cases of the object type.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
variants: list[ObjectVariant]
|
|
32
|
+
case_count: int
|
|
33
|
+
event_count: int
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from ocelescope_backend.app.modules import Module, ModuleMeta
|
|
3
|
+
from packaging.version import Version
|
|
4
|
+
|
|
5
|
+
from ocelescope_module_ocel.routers import router
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Ocel(Module):
|
|
9
|
+
meta = ModuleMeta(key="ocel", version=Version("1.0"))
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def create_app(cls) -> FastAPI:
|
|
13
|
+
app = FastAPI(
|
|
14
|
+
title="OCEL", version=str(cls.meta.version), docs_url=None, redoc_url=None
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
app.include_router(router)
|
|
18
|
+
|
|
19
|
+
return app
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""The OCEL module's router, assembled from the per-resource sub-routers.
|
|
2
|
+
|
|
3
|
+
``management`` is included first so its static single-segment paths (``/default``,
|
|
4
|
+
``/extension/meta``) are matched before the ``/{ocel_id}`` catch-all.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter
|
|
8
|
+
|
|
9
|
+
from ocelescope_module_ocel.routers import (
|
|
10
|
+
attributes,
|
|
11
|
+
events,
|
|
12
|
+
export,
|
|
13
|
+
management,
|
|
14
|
+
objects,
|
|
15
|
+
quantities,
|
|
16
|
+
relations,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
router = APIRouter()
|
|
20
|
+
router.include_router(management.router)
|
|
21
|
+
router.include_router(attributes.router)
|
|
22
|
+
router.include_router(objects.router)
|
|
23
|
+
router.include_router(events.router)
|
|
24
|
+
router.include_router(relations.router)
|
|
25
|
+
router.include_router(quantities.router)
|
|
26
|
+
router.include_router(export.router)
|
|
27
|
+
|
|
28
|
+
__all__ = ["router"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Aggregated and per-type attribute summaries (computation in ``util.attributes``)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Query
|
|
8
|
+
from ocelescope_backend.app.dependencies import ApiOcel
|
|
9
|
+
from ocelescope_backend.app.internal.model.base import PaginatedResponse
|
|
10
|
+
|
|
11
|
+
from ocelescope_module_ocel.models import AggregatedAttribute, TypedAttribute
|
|
12
|
+
from ocelescope_module_ocel.util import attributes as attribute_util
|
|
13
|
+
from ocelescope_module_ocel.util.attributes import EntityType
|
|
14
|
+
|
|
15
|
+
router = APIRouter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@router.get("/{ocel_id}/attributes", operation_id="AggregatedAttributes")
|
|
19
|
+
def get_aggr_attributes(
|
|
20
|
+
ocel: ApiOcel,
|
|
21
|
+
page: Annotated[int, Query(ge=1)] = 1,
|
|
22
|
+
page_size: Annotated[int, Query()] = 10,
|
|
23
|
+
entity_type: Annotated[EntityType, Query()] = "events",
|
|
24
|
+
attribute_names: Annotated[list[str] | None, Query()] = None,
|
|
25
|
+
entity_names: Annotated[list[str] | None, Query()] = None,
|
|
26
|
+
drop_constant: Annotated[bool, Query()] = False,
|
|
27
|
+
) -> PaginatedResponse[list[AggregatedAttribute]]:
|
|
28
|
+
names = attribute_util.attribute_names(
|
|
29
|
+
ocel, entity_type, attribute_names, entity_names, drop_constant
|
|
30
|
+
)
|
|
31
|
+
page_names = names[(page - 1) * page_size : page * page_size]
|
|
32
|
+
table = attribute_util.merged_table(ocel, entity_type, page_names, entity_names)
|
|
33
|
+
|
|
34
|
+
return PaginatedResponse(
|
|
35
|
+
page=page,
|
|
36
|
+
page_size=page_size,
|
|
37
|
+
total_items=len(names),
|
|
38
|
+
response=attribute_util.aggregate_attributes(table),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get(
|
|
43
|
+
"/{ocel_id}/objects/attributes",
|
|
44
|
+
response_model=list[TypedAttribute],
|
|
45
|
+
operation_id="objectAttributes",
|
|
46
|
+
)
|
|
47
|
+
def get_object_attributes(
|
|
48
|
+
ocel: ApiOcel,
|
|
49
|
+
attribute_names: Annotated[list[str], Query()] = [],
|
|
50
|
+
names: Annotated[list[str] | None, Query()] = None,
|
|
51
|
+
) -> list[TypedAttribute]:
|
|
52
|
+
table = attribute_util.merged_object_table(ocel, attribute_names or None, names)
|
|
53
|
+
return attribute_util.typed_attributes(table)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.get(
|
|
57
|
+
"/{ocel_id}/events/attributes",
|
|
58
|
+
response_model=list[TypedAttribute],
|
|
59
|
+
operation_id="eventAttributes",
|
|
60
|
+
)
|
|
61
|
+
def get_event_attributes(
|
|
62
|
+
ocel: ApiOcel,
|
|
63
|
+
attribute_names: Annotated[list[str], Query()] = [],
|
|
64
|
+
names: Annotated[list[str] | None, Query()] = None,
|
|
65
|
+
) -> list[TypedAttribute]:
|
|
66
|
+
table = attribute_util.merged_event_table(ocel, attribute_names or None, names)
|
|
67
|
+
return attribute_util.typed_attributes(table)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@router.get("/{ocel_id}/attribute/names", operation_id="AttributeNames")
|
|
71
|
+
def get_attribute_names(
|
|
72
|
+
ocel: ApiOcel,
|
|
73
|
+
entity_type: Annotated[EntityType | None, Query()] = None,
|
|
74
|
+
) -> list[str]:
|
|
75
|
+
names: list[str] = []
|
|
76
|
+
if entity_type != "events":
|
|
77
|
+
names += attribute_util.attribute_names(ocel, "objects")
|
|
78
|
+
if entity_type != "objects":
|
|
79
|
+
names += attribute_util.attribute_names(ocel, "events")
|
|
80
|
+
return names
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Event-centric summaries: activities, counts, ids and the time distribution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from fastapi import APIRouter
|
|
9
|
+
from ocelescope.ocel.constants.pm4py import ACTIVITY_COL, EID_COL, TIMESTAMP_COL
|
|
10
|
+
from ocelescope_backend.app.dependencies import ApiOcel
|
|
11
|
+
from ocelescope_backend.app.internal.model.base import PaginatedResponse
|
|
12
|
+
|
|
13
|
+
from ocelescope_module_ocel.models import Date_Distribution_Item, Entity_Time_Info
|
|
14
|
+
from ocelescope_module_ocel.util.pagination import paginate_ids
|
|
15
|
+
|
|
16
|
+
router = APIRouter()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("/{ocel_id}/events/activityNames", operation_id="Activities")
|
|
20
|
+
def get_activities(ocel: ApiOcel) -> list[str]:
|
|
21
|
+
return ocel.events.activities
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.get(
|
|
25
|
+
"/{ocel_id}/events/counts",
|
|
26
|
+
response_model=dict[str, int],
|
|
27
|
+
operation_id="eventCounts",
|
|
28
|
+
)
|
|
29
|
+
def get_event_counts(ocel: ApiOcel) -> dict[str, int]:
|
|
30
|
+
# Ordered most frequent first, which the response preserves.
|
|
31
|
+
return {
|
|
32
|
+
str(activity): int(count)
|
|
33
|
+
for activity, count in ocel.events.activity_counts.items()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("/{ocel_id}/events/ids", operation_id="eventIds")
|
|
38
|
+
def get_event_ids(
|
|
39
|
+
ocel: ApiOcel,
|
|
40
|
+
search: str | None = None,
|
|
41
|
+
size: int = 10,
|
|
42
|
+
page: int = 1,
|
|
43
|
+
) -> PaginatedResponse[list[str]]:
|
|
44
|
+
return paginate_ids(ocel.events.table, EID_COL, search, page, size)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@router.get(
|
|
48
|
+
"/{ocel_id}/events/time",
|
|
49
|
+
response_model=Entity_Time_Info,
|
|
50
|
+
operation_id="timeInfo",
|
|
51
|
+
)
|
|
52
|
+
def get_time_info(
|
|
53
|
+
ocel: ApiOcel, periods: int | None = None, freq: str | None = None
|
|
54
|
+
) -> Entity_Time_Info:
|
|
55
|
+
|
|
56
|
+
span = ocel.events.table.aggregate(
|
|
57
|
+
f'min("{TIMESTAMP_COL}") AS start, max("{TIMESTAMP_COL}") AS end'
|
|
58
|
+
).fetchone()
|
|
59
|
+
assert span is not None
|
|
60
|
+
start_time, end_time = span
|
|
61
|
+
|
|
62
|
+
bins = pd.date_range(start_time, end_time, periods=periods, freq=freq)
|
|
63
|
+
if len(bins) < 2:
|
|
64
|
+
return Entity_Time_Info(
|
|
65
|
+
start_time=start_time.isoformat(),
|
|
66
|
+
end_time=end_time.isoformat(),
|
|
67
|
+
date_distribution=[],
|
|
68
|
+
)
|
|
69
|
+
edges = [edge.to_pydatetime() for edge in bins]
|
|
70
|
+
|
|
71
|
+
rows = ocel.sql(
|
|
72
|
+
f"""
|
|
73
|
+
WITH bounds AS (SELECT ? AS edges),
|
|
74
|
+
windows AS (
|
|
75
|
+
SELECT i AS window_id, edges[i + 1] AS win_start, edges[i + 2] AS win_end
|
|
76
|
+
FROM bounds, range(0, len(edges) - 1) AS steps(i)
|
|
77
|
+
)
|
|
78
|
+
SELECT w.window_id, e."{ACTIVITY_COL}" AS activity, count(*) AS count
|
|
79
|
+
FROM events e JOIN windows w
|
|
80
|
+
ON (e."{TIMESTAMP_COL}" > w.win_start AND e."{TIMESTAMP_COL}" <= w.win_end)
|
|
81
|
+
OR (w.window_id = 0 AND e."{TIMESTAMP_COL}" = w.win_start)
|
|
82
|
+
GROUP BY w.window_id, e."{ACTIVITY_COL}"
|
|
83
|
+
""",
|
|
84
|
+
[edges],
|
|
85
|
+
).fetchall()
|
|
86
|
+
|
|
87
|
+
counts_by_window: dict[int, dict[str, int]] = defaultdict(dict)
|
|
88
|
+
for window_id, activity, count in rows:
|
|
89
|
+
counts_by_window[window_id][activity] = int(count)
|
|
90
|
+
|
|
91
|
+
date_distribution = [
|
|
92
|
+
Date_Distribution_Item(
|
|
93
|
+
start_timestamp=edges[window_id].isoformat(),
|
|
94
|
+
end_timestamp=edges[window_id + 1].isoformat(),
|
|
95
|
+
entity_count=counts_by_window[window_id],
|
|
96
|
+
)
|
|
97
|
+
for window_id in sorted(counts_by_window)
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
return Entity_Time_Info(
|
|
101
|
+
start_time=start_time.isoformat(),
|
|
102
|
+
end_time=end_time.isoformat(),
|
|
103
|
+
date_distribution=date_distribution,
|
|
104
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Download the OCEL, or a flattened XES log.
|
|
2
|
+
|
|
3
|
+
``downloadOCEL`` streams the log straight from the DuckDB store (no materialization).
|
|
4
|
+
The two XES routes flatten a chosen object type into a per-object trace log, which
|
|
5
|
+
is pm4py-only, so those do pull the tables they need into memory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Annotated, Literal
|
|
13
|
+
|
|
14
|
+
import polars as pl
|
|
15
|
+
from fastapi import APIRouter, Query
|
|
16
|
+
from ocelescope.ocel import Keep
|
|
17
|
+
from ocelescope.ocel.constants.misc import OCELFileExtensions
|
|
18
|
+
from ocelescope.ocel.constants.pm4py import OID_COL
|
|
19
|
+
from ocelescope_backend.app.dependencies import ApiOcel, ApiSession
|
|
20
|
+
from ocelescope_backend.app.internal.exceptions import NotFound
|
|
21
|
+
from ocelescope_backend.app.internal.model.response import TempFileResponse
|
|
22
|
+
|
|
23
|
+
from ocelescope import OCEL, BaseFilter
|
|
24
|
+
|
|
25
|
+
router = APIRouter()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _ObjectIdKeep(BaseFilter):
|
|
29
|
+
"""A ModuleFilter that keeps only the given object ids (for a variant sub-log)."""
|
|
30
|
+
|
|
31
|
+
object_ids: list[str]
|
|
32
|
+
|
|
33
|
+
def keep(self, ocel: OCEL) -> Keep:
|
|
34
|
+
return Keep(objects=pl.LazyFrame({OID_COL: self.object_ids}))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("/{ocel_id}/download", summary="Download OCEL", operation_id="downloadOCEL")
|
|
38
|
+
def download_ocel(
|
|
39
|
+
session: ApiSession,
|
|
40
|
+
ocel_id: str,
|
|
41
|
+
ext: OCELFileExtensions = ".json",
|
|
42
|
+
ocel_version: Literal["original", "filtered"] | None = "filtered",
|
|
43
|
+
) -> TempFileResponse:
|
|
44
|
+
if ocel_id not in session.ocels:
|
|
45
|
+
raise NotFound("OCEL not found")
|
|
46
|
+
|
|
47
|
+
name = session.ocels[ocel_id].name
|
|
48
|
+
tmp_file_prefix = datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + name
|
|
49
|
+
file_response = TempFileResponse(
|
|
50
|
+
prefix=tmp_file_prefix, suffix=ext, filename=name + ext
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
session.export_ocel(
|
|
54
|
+
ocel_id, Path(file_response.tmp_path), use_original=ocel_version == "original"
|
|
55
|
+
)
|
|
56
|
+
return file_response
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get(
|
|
60
|
+
"/{ocel_id}/download/xes",
|
|
61
|
+
summary="Download OCEL as a xes",
|
|
62
|
+
operation_id="downloadFlatLog",
|
|
63
|
+
)
|
|
64
|
+
def download_flat_log(
|
|
65
|
+
ocel: ApiOcel, ocel_id: str, session: ApiSession, object_type_name: str
|
|
66
|
+
) -> TempFileResponse:
|
|
67
|
+
name = session.ocels[ocel_id].name
|
|
68
|
+
tmp_file_prefix = f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-{name}"
|
|
69
|
+
file_response = TempFileResponse(
|
|
70
|
+
prefix=tmp_file_prefix, suffix=".xes", filename=f"{name}_{object_type_name}.xes"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
ocel.write_xes(object_type_name, Path(file_response.tmp_path))
|
|
74
|
+
return file_response
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.get(
|
|
78
|
+
"/{ocel_id}/objects/variants/download/xes",
|
|
79
|
+
summary="Download selected object variants as a flat XES log",
|
|
80
|
+
description=(
|
|
81
|
+
"Filters the OCEL to the objects of `object_type` that follow any of the "
|
|
82
|
+
"given `variant_ids`, then flattens that sub-log by `object_type` into a "
|
|
83
|
+
"XES file (one trace per object)."
|
|
84
|
+
),
|
|
85
|
+
operation_id="downloadVariantFlatLog",
|
|
86
|
+
)
|
|
87
|
+
def download_variant_flat_log(
|
|
88
|
+
ocel: ApiOcel,
|
|
89
|
+
session: ApiSession,
|
|
90
|
+
ocel_id: str,
|
|
91
|
+
object_type: str,
|
|
92
|
+
variant_ids: Annotated[list[str], Query(min_length=1)],
|
|
93
|
+
) -> TempFileResponse:
|
|
94
|
+
name = session.ocels[ocel_id].name
|
|
95
|
+
|
|
96
|
+
object_ids = ocel.executions.get_variant_object_ids(object_type, variant_ids)
|
|
97
|
+
|
|
98
|
+
if not object_ids:
|
|
99
|
+
raise NotFound("No objects were found for the given variants")
|
|
100
|
+
|
|
101
|
+
tmp_file_prefix = datetime.now().strftime("%Y%m%d-%H%M%S") + "-" + name
|
|
102
|
+
file_response = TempFileResponse(
|
|
103
|
+
prefix=tmp_file_prefix, suffix=".xes", filename=f"{name}_{object_type}.xes"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
with ocel.filter([_ObjectIdKeep(object_ids=object_ids)]) as variant_ocel:
|
|
107
|
+
variant_ocel.write_xes(object_type, Path(file_response.tmp_path))
|
|
108
|
+
return file_response
|