ocelescope-module-ocpn-editor 0.10.3__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.
@@ -0,0 +1,33 @@
1
+ Metadata-Version: 2.3
2
+ Name: ocelescope-module-ocpn-editor
3
+ Version: 0.10.3
4
+ Summary: An OCPN Editor
5
+ Author: Uy Sa Huynh
6
+ Requires-Dist: ocelescope-backend>=0.10.2,<0.11.0
7
+ Requires-Dist: ocelescope-module-filter>=0.10.2,<0.11.0
8
+ Requires-Dist: ocelescope-module-ocel>=0.10.2,<0.11.0
9
+ Requires-Python: >=3.11
10
+ Description-Content-Type: text/markdown
11
+
12
+ # OCPN Editor
13
+
14
+ An OCPN Editor
15
+
16
+ An [Ocelescope](https://www.ocelescope.org) backend module. It is a FastAPI app
17
+ that `ocelescope-backend` discovers through the `ocelescope_backend.modules`
18
+ entry point in `pyproject.toml` and serves under `/modules/ocpnEditor/v1`.
19
+
20
+ - `src/ocelescope_module_ocpn_editor/module.py` — module class `OCPNEditor` (key `ocpnEditor`)
21
+ - `src/ocelescope_module_ocpn_editor/routes.py` — API routes
22
+
23
+ Install it next to `ocelescope-backend` (e.g. as a uv workspace member of an
24
+ Ocelescope module project) and run `ocelescope-backend serve`.
25
+
26
+ ## Updating from the template
27
+
28
+ This module was generated with [Copier](https://copier.readthedocs.io). To pull
29
+ in later changes from the template, commit your work and run in this folder:
30
+
31
+ ```sh
32
+ uvx copier update
33
+ ```
@@ -0,0 +1,22 @@
1
+ # OCPN Editor
2
+
3
+ An OCPN Editor
4
+
5
+ An [Ocelescope](https://www.ocelescope.org) backend module. It is a FastAPI app
6
+ that `ocelescope-backend` discovers through the `ocelescope_backend.modules`
7
+ entry point in `pyproject.toml` and serves under `/modules/ocpnEditor/v1`.
8
+
9
+ - `src/ocelescope_module_ocpn_editor/module.py` — module class `OCPNEditor` (key `ocpnEditor`)
10
+ - `src/ocelescope_module_ocpn_editor/routes.py` — API routes
11
+
12
+ Install it next to `ocelescope-backend` (e.g. as a uv workspace member of an
13
+ Ocelescope module project) and run `ocelescope-backend serve`.
14
+
15
+ ## Updating from the template
16
+
17
+ This module was generated with [Copier](https://copier.readthedocs.io). To pull
18
+ in later changes from the template, commit your work and run in this folder:
19
+
20
+ ```sh
21
+ uvx copier update
22
+ ```
@@ -0,0 +1,24 @@
1
+ [project]
2
+ name = "ocelescope-module-ocpn-editor"
3
+ version = "0.10.3"
4
+ readme = "README.md"
5
+ description = "An OCPN Editor"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "ocelescope-backend>=0.10.2,<0.11.0",
9
+ "ocelescope-module-filter>=0.10.2,<0.11.0",
10
+ "ocelescope-module-ocel>=0.10.2,<0.11.0",
11
+ ]
12
+
13
+ [[project.authors]]
14
+ name = "Uy Sa Huynh"
15
+
16
+ [project.entry-points."ocelescope_backend.modules"]
17
+ ocpnEditor = "ocelescope_module_ocpn_editor.module:OCPNEditor"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.9.26,<0.12.0"]
21
+ build-backend = "uv_build"
22
+
23
+ [tool.uv]
24
+ package = true
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "ocelescope-module-ocpn-editor"
3
+ version = "0.10.3"
4
+ readme = "README.md"
5
+ description = "An OCPN Editor"
6
+ authors = [{ name = "Uy Sa Huynh" }]
7
+ requires-python = ">=3.11"
8
+ dependencies = [
9
+ "ocelescope-backend>=0.10.2,<0.11.0",
10
+ "ocelescope-module-filter>=0.10.2,<0.11.0",
11
+ "ocelescope-module-ocel>=0.10.2,<0.11.0",
12
+ ]
13
+
14
+ [project.entry-points."ocelescope_backend.modules"]
15
+ ocpnEditor = "ocelescope_module_ocpn_editor.module:OCPNEditor"
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.9.26,<0.12.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [tool.uv]
22
+ package = true
@@ -0,0 +1,35 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class EditorPlace(BaseModel):
5
+ id: str
6
+
7
+
8
+ class EditorTransition(BaseModel):
9
+ id: str
10
+ label: str | None = None
11
+
12
+
13
+ class EditorArc(BaseModel):
14
+ nodes: tuple[str, str]
15
+ weight: int | None = None
16
+
17
+
18
+ class EditorOCPN(BaseModel):
19
+ places: list[EditorPlace]
20
+ transitions: list[EditorTransition]
21
+ arcs: list[EditorArc]
22
+ initial_marking: dict[str, int] | None = None
23
+ final_marking: dict[str, int] | None = None
24
+
25
+
26
+ class OcpnExportRequest(BaseModel):
27
+ petri_net: EditorOCPN
28
+ place_object_type: dict[str, str]
29
+ place_in_out_mult: dict[str, tuple[dict[str, bool], dict[str, bool]]] | None = None
30
+
31
+
32
+ class OcpnImportResponse(BaseModel):
33
+ petri_net: EditorOCPN
34
+ place_object_type: dict[str, str]
35
+ place_in_out_mult: dict[str, tuple[dict[str, bool], dict[str, bool]]]
@@ -0,0 +1,53 @@
1
+ from pydantic import BaseModel
2
+ from ocelescope import Resource
3
+ from pydantic import Field
4
+
5
+
6
+ class ImportPlace(BaseModel):
7
+ id: str
8
+ tokens: int = 0
9
+ final_tokens: int = 0
10
+
11
+
12
+ class ImportTransition(BaseModel):
13
+ id: str
14
+ label: str | None = None
15
+
16
+
17
+ class ImportArc(BaseModel):
18
+ source: str
19
+ target: str
20
+ weight: int = 1
21
+
22
+
23
+ class ImportResponse(BaseModel):
24
+ places: list[ImportPlace]
25
+ transitions: list[ImportTransition]
26
+ arcs: list[ImportArc]
27
+
28
+ #Resource
29
+
30
+ class ClassicPlace(BaseModel):
31
+ id: str
32
+ tokens: int = 0
33
+ final_tokens: int = 0
34
+
35
+
36
+ class ClassicTransition(BaseModel):
37
+ id: str
38
+ label: str | None = None
39
+
40
+
41
+ class ClassicArc(BaseModel):
42
+ source: str
43
+ target: str
44
+ weight: int = 1
45
+
46
+
47
+ class ClassicPetriNet(Resource):
48
+ label = "Classic Petri Net"
49
+ description = "A classic Petri net"
50
+
51
+ places: list[ClassicPlace] = Field(default_factory=list)
52
+ transitions: list[ClassicTransition] = Field(default_factory=list)
53
+ arcs: list[ClassicArc] = Field(default_factory=list)
@@ -0,0 +1,15 @@
1
+ from fastapi import FastAPI
2
+ from ocelescope_backend.app.modules import Module, ModuleMeta
3
+ from packaging.version import Version
4
+ from ocelescope_module_ocpn_editor.routes import router
5
+ from ocelescope_module_ocpn_editor.model.editor_pn import ClassicPetriNet
6
+
7
+
8
+ class OCPNEditor(Module):
9
+ meta = ModuleMeta(key="ocpnEditor", version=Version("1.0"))
10
+ resources = [ClassicPetriNet]
11
+ @classmethod
12
+ def create_app(cls) -> FastAPI:
13
+ app = FastAPI(title="OCPN Editor", version=str(cls.meta.version))
14
+ app.include_router(router)
15
+ return app
@@ -0,0 +1,121 @@
1
+ from fastapi import APIRouter, UploadFile
2
+ import tempfile
3
+ import os
4
+ from r4pm.petri_net import export_pnml, import_pnml
5
+ from fastapi.responses import PlainTextResponse
6
+ from ocelescope_module_ocpn_editor.util.convert_format import convert_to_OCPN, convert_from_OCPN, convert_to_pnet_resource
7
+ from ocelescope_module_ocpn_editor.model.editor_ocpn import OcpnExportRequest, OcpnImportResponse
8
+ from ocelescope import PetriNet
9
+ from ocelescope_module_ocpn_editor.util.convert_format import convert_from_pnml
10
+ from ocelescope_module_ocpn_editor.model.editor_pn import ImportResponse, ClassicPetriNet
11
+ from ocelescope_backend.app.dependencies import ApiOcel, ApiSession
12
+ from ocelescope_backend.app.internal.model.resource import ResourceStore
13
+ from typing import cast
14
+
15
+ router = APIRouter()
16
+
17
+ @router.get("/{ocel_id}/objects/types", operation_id="objectTypes")
18
+ def get_object_types(ocel:ApiOcel) -> list[str]:
19
+ return ocel.objects.types
20
+
21
+
22
+ @router.get("/{ocel_id}/events/activities", operation_id="eventActivities")
23
+ def get_event_activities(ocel:ApiOcel) -> list[str]:
24
+ return ocel.events.activities
25
+
26
+
27
+ # ============================================
28
+
29
+ # Classic Petri-Net
30
+
31
+ # ============================================
32
+
33
+
34
+ # Export/Import
35
+
36
+ @router.post("/petri-net/export/pnml", operation_id="exportPetriNetPnml")
37
+ def export_petri_net_pnml(net: dict) -> PlainTextResponse:
38
+ with tempfile.NamedTemporaryFile(suffix=".pnml", delete=False) as tmp:
39
+ tmp_path = tmp.name
40
+
41
+ try:
42
+ export_pnml(net, tmp_path)
43
+ with open(tmp_path, "r", encoding="utf-8") as f:
44
+ pnml_text = f.read()
45
+ finally:
46
+ os.unlink(tmp_path)
47
+
48
+ return PlainTextResponse(content=pnml_text)
49
+
50
+ @router.post("/petri-net/import/pnml", operation_id="importPetriNetPnml")
51
+ async def import_petri_net_pnml(file: UploadFile) -> ImportResponse:
52
+ input = await file.read()
53
+
54
+ with tempfile.NamedTemporaryFile(suffix=".pnml", delete=False) as tmp:
55
+ tmp.write(input)
56
+ tmp_path = tmp.name
57
+
58
+ try:
59
+ net = import_pnml(tmp_path)
60
+ finally:
61
+ os.unlink(tmp_path)
62
+
63
+ return convert_from_pnml(net)
64
+
65
+ # Load/Save to Session
66
+
67
+ @router.post("/petri-net/export/session")
68
+ def save_to_session_classic(session: ApiSession, pnet: dict, name:str):
69
+ resource_pnet = convert_to_pnet_resource(pnet)
70
+ session.add_resource(ResourceStore.from_resource(resource_pnet, name=name))
71
+ print(session.list_resources())
72
+
73
+
74
+
75
+ # ============================================
76
+
77
+ # Object-Centric Petri Net
78
+
79
+ # ============================================
80
+
81
+
82
+ # Export/Import
83
+
84
+ @router.post("/ocpn/export/ocelescope", operation_id="exportOCPN")
85
+ def export_ocpn(ocpn: OcpnExportRequest) -> PlainTextResponse:
86
+ pnet = convert_to_OCPN(ocpn)
87
+
88
+ with tempfile.NamedTemporaryFile(suffix=".ocelescope", delete=False) as tmp:
89
+ tmp_path = tmp.name
90
+
91
+ try:
92
+ written_path = pnet.write(tmp_path)
93
+ with open(written_path, "r", encoding="utf-8") as f:
94
+ content = f.read()
95
+ finally:
96
+ os.unlink(written_path)
97
+
98
+ return PlainTextResponse(content=content)
99
+
100
+
101
+ @router.post("/ocpn/import/ocelescope", operation_id="importOCPN")
102
+ async def import_ocpn(file: UploadFile) -> OcpnImportResponse:
103
+ input = await file.read()
104
+ pnet = PetriNet.model_validate_json(input)
105
+ return convert_from_OCPN(pnet)
106
+
107
+
108
+ # Load/Save to session
109
+
110
+
111
+ @router.post("/ocpn/export/session", operation_id="saveSession")
112
+ def save_to_session(session: ApiSession, ocpn: OcpnExportRequest, name:str):
113
+ pnet = convert_to_OCPN(ocpn)
114
+ session.add_resource(ResourceStore.from_resource(pnet, name=name))
115
+
116
+
117
+ @router.post("/ocpn/import/session", operation_id="loadSession")
118
+ async def load_from_session(session:ApiSession, resource_id: str | None = None) -> OcpnImportResponse:
119
+ resource_store = cast(PetriNet,session.get_resource(resource_id))
120
+ petri_net = PetriNet(**resource_store.data)
121
+ return convert_from_OCPN(petri_net)
@@ -0,0 +1,126 @@
1
+ from ocelescope.resource.default.petri_net import Arc, ArcType, PetriNet, Place, Transition
2
+ from ocelescope_module_ocpn_editor.model.editor_ocpn import OcpnExportRequest, OcpnImportResponse, EditorArc, EditorOCPN, EditorPlace, EditorTransition
3
+ from ocelescope_module_ocpn_editor.model.editor_pn import ImportResponse, ImportPlace,ImportArc,ImportTransition
4
+ from ocelescope_module_ocpn_editor.model.editor_pn import ClassicPetriNet, ClassicArc, ClassicPlace, ClassicTransition
5
+
6
+
7
+ def convert_to_OCPN(ocpn: OcpnExportRequest) -> PetriNet:
8
+ net = ocpn.petri_net
9
+ object_types = ocpn.place_object_type
10
+ in_out_mult = ocpn.place_in_out_mult if ocpn.place_in_out_mult is not None else {}
11
+
12
+ ocpn = PetriNet()
13
+
14
+ for place in net.places:
15
+ ocpn.add_place(Place(name=place.id, object_type=object_types[place.id]))
16
+
17
+ for transition in net.transitions:
18
+ ocpn.add_transition(Transition(name=transition.id, label=transition.label))
19
+
20
+ place_ids = {place.name for place in ocpn.places}
21
+
22
+ def is_variable(source: str, target: str) -> bool:
23
+ if source in place_ids:
24
+ _, outgoing = in_out_mult.get(source, ({}, {}))
25
+ return bool(outgoing.get(target))
26
+ incoming, _ = in_out_mult.get(target, ({}, {}))
27
+ return bool(incoming.get(source))
28
+
29
+ for arc in net.arcs:
30
+ source, target = arc.nodes
31
+ ocpn.add_arc(
32
+ Arc(
33
+ source=source,
34
+ target=target,
35
+ type=ArcType.VARIABLE if is_variable(source, target) else ArcType.NORMAL,
36
+ weight=arc.weight if arc.weight is not None else 1,
37
+ )
38
+ )
39
+
40
+ ocpn.initial_marking = dict(net.initial_marking if net.initial_marking is not None else {})
41
+ ocpn.final_marking = dict(net.final_marking if net.final_marking is not None else {})
42
+
43
+ return ocpn
44
+
45
+
46
+ def convert_from_OCPN(ocpn:PetriNet) -> OcpnImportResponse:
47
+ places = []
48
+ place_object_type = {}
49
+
50
+
51
+ for place in ocpn.places:
52
+ places.append(EditorPlace(id=place.name))
53
+ place_object_type[place.name] = place.object_type
54
+
55
+ transitions = [EditorTransition(id=transition.name, label=transition.label) for transition in ocpn.transitions]
56
+ initial_marking = ocpn.initial_marking if ocpn.initial_marking is not None else {}
57
+ final_marking = ocpn.final_marking if ocpn.final_marking is not None else {}
58
+
59
+ place_ids = {place.id for place in places}
60
+ place_in_out_mult: dict[str, tuple[dict[str, bool], dict[str, bool]]] = {
61
+ ids: ({}, {}) for ids in place_ids
62
+ }
63
+
64
+
65
+ arcs = []
66
+ for arc in ocpn.arcs:
67
+ arcs.append(EditorArc(nodes=(arc.source,arc.target), weight=arc.weight))
68
+ if arc.type != ArcType.VARIABLE:
69
+ continue
70
+ if arc.source in place_ids:
71
+ place_id, transition_id = arc.source, arc.target
72
+ incoming, outgoing = place_in_out_mult[place_id]
73
+ place_in_out_mult[place_id] = (incoming, {**outgoing, transition_id: True})
74
+ else:
75
+ transition_id, place_id = arc.source, arc.target
76
+ incoming, outgoing = place_in_out_mult[place_id]
77
+ place_in_out_mult[place_id] = ({**incoming, transition_id: True}, outgoing)
78
+
79
+ return OcpnImportResponse(
80
+ petri_net= EditorOCPN(
81
+ places=places,
82
+ transitions=transitions,
83
+ arcs=arcs,
84
+ initial_marking=initial_marking,
85
+ final_marking=final_marking,
86
+ ),
87
+ place_object_type=place_object_type,
88
+ place_in_out_mult=place_in_out_mult,
89
+ )
90
+
91
+
92
+ def convert_from_pnml(net:dict) -> ImportResponse:
93
+ initial_marking = net.get("initial_marking") if net.get("initial_marking") is not None else {}
94
+ final_markings = net.get("final_markings") or []
95
+ final_marking = final_markings[0] if final_markings else {}
96
+
97
+ places = [
98
+ ImportPlace(
99
+ id=pid,
100
+ tokens=initial_marking.get(pid,0),
101
+ final_tokens=final_marking.get(pid,0)
102
+ )
103
+ for pid in net.get("places",{})
104
+ ]
105
+ transitions = [
106
+ ImportTransition(
107
+ id=tid,
108
+ label= transition.get("label")
109
+ )
110
+ for tid, transition in net.get("transitions",{}).items()
111
+ ]
112
+ arcs = []
113
+ for arc in net.get("arcs", []):
114
+ source, target = arc["from_to"]["nodes"]
115
+ arcs.append(ImportArc(source=source, target=target, weight=arc.get("weight", 1)))
116
+
117
+ return ImportResponse(places=places, transitions=transitions, arcs=arcs)
118
+
119
+
120
+ def convert_to_pnet_resource(net: dict) -> ClassicPetriNet:
121
+ pnet = convert_from_pnml(net)
122
+ return ClassicPetriNet(
123
+ places=[ClassicPlace(**p.model_dump()) for p in pnet.places],
124
+ transitions=[ClassicTransition(**t.model_dump()) for t in pnet.transitions],
125
+ arcs=[ClassicArc(**a.model_dump()) for a in pnet.arcs],
126
+ )