Skip to content

Commit 27cf103

Browse files
committed
[doc] Add new recipe to import a sysml file with python
Signed-off-by: Axel RICHARD <axel.richard@obeo.fr>
1 parent 7a45363 commit 27cf103

3 files changed

Lines changed: 303 additions & 0 deletions

File tree

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
###############################################################################
2+
# Copyright (c) 2026 Obeo.
3+
# This program and the accompanying materials
4+
# are made available under the terms of the Eclipse Public License v2.0
5+
# which accompanies this distribution, and is available at
6+
# https://www.eclipse.org/legal/epl-2.0/
7+
#
8+
# SPDX-License-Identifier: EPL-2.0
9+
#
10+
# Contributors:
11+
# Obeo - initial API and implementation
12+
###############################################################################
13+
14+
import argparse
15+
import json
16+
import uuid
17+
from pathlib import Path
18+
19+
import requests # <1>
20+
21+
22+
GRAPHQL_ENDPOINT = "/api/graphql"
23+
GRAPHQL_UPLOAD_ENDPOINT = "/api/graphql/upload"
24+
25+
26+
fetch_editing_context_query = """
27+
query FetchEditingContext($projectId: ID!) {
28+
viewer {
29+
project(projectId: $projectId) {
30+
currentEditingContext {
31+
id
32+
}
33+
}
34+
}
35+
}
36+
"""
37+
38+
39+
upload_document_mutation = """
40+
mutation UploadDocument($input: UploadDocumentInput!) {
41+
uploadDocument(input: $input) {
42+
__typename
43+
... on UploadDocumentSuccessPayload {
44+
id
45+
report
46+
}
47+
... on ErrorPayload {
48+
messages {
49+
body
50+
level
51+
}
52+
}
53+
}
54+
}
55+
"""
56+
57+
58+
def build_headers(token): # <2>
59+
headers = {}
60+
if token:
61+
headers["Authorization"] = f"Bearer {token}"
62+
return headers
63+
64+
65+
def get_graphql_url(url):
66+
return f"{url.rstrip('/')}{GRAPHQL_ENDPOINT}"
67+
68+
69+
def get_graphql_upload_url(url):
70+
return f"{url.rstrip('/')}{GRAPHQL_UPLOAD_ENDPOINT}"
71+
72+
73+
def print_graphql_errors(data):
74+
errors = data.get("errors", [])
75+
for error in errors:
76+
print(f"GraphQL error: {error.get('message', error)}")
77+
78+
79+
def fetch_editing_context_id(url, project_id, token): # <3>
80+
response = requests.post(
81+
get_graphql_url(url),
82+
json={
83+
"query": fetch_editing_context_query,
84+
"variables": {"projectId": project_id},
85+
},
86+
headers=build_headers(token),
87+
)
88+
89+
if response.status_code != 200:
90+
print(f"Error fetching editing context: {response.status_code} - {response.text}")
91+
return None
92+
93+
data = response.json()
94+
if data.get("errors"):
95+
print_graphql_errors(data)
96+
return None
97+
98+
project = data.get("data", {}).get("viewer", {}).get("project")
99+
if not project:
100+
print(f"Project not found: {project_id}")
101+
return None
102+
103+
editing_context = project.get("currentEditingContext")
104+
if not editing_context:
105+
print(f"Editing context not found for project: {project_id}")
106+
return None
107+
108+
return editing_context.get("id")
109+
110+
111+
def print_messages(messages):
112+
for message in messages:
113+
print(f"{message.get('level', 'INFO')}: {message.get('body', '')}")
114+
115+
116+
def import_sysml_file(url, file_path, editing_context_id, read_only, token): # <4>
117+
operation_id = str(uuid.uuid4())
118+
operations = {
119+
"query": upload_document_mutation,
120+
"variables": {
121+
"input": {
122+
"id": operation_id,
123+
"editingContextId": editing_context_id,
124+
"file": None,
125+
"readOnly": read_only,
126+
}
127+
},
128+
}
129+
file_map = {
130+
"0": "variables.file"
131+
}
132+
133+
with open(file_path, "rb") as file:
134+
response = requests.post( # <5>
135+
get_graphql_upload_url(url),
136+
data={
137+
"operations": json.dumps(operations),
138+
"map": json.dumps(file_map),
139+
},
140+
files={
141+
"0": (file_path.name, file, "text/plain"),
142+
},
143+
headers=build_headers(token),
144+
)
145+
146+
if response.status_code not in (200, 201):
147+
print(f"Error importing SysML file: {response.status_code} - {response.text}")
148+
return False
149+
150+
data = response.json()
151+
if data.get("errors"):
152+
print_graphql_errors(data)
153+
return False
154+
155+
payload = data.get("data", {}).get("uploadDocument", {})
156+
if payload.get("__typename") == "UploadDocumentSuccessPayload":
157+
print(f"SysML file imported successfully: {file_path}")
158+
report = payload.get("report")
159+
if report:
160+
print("Import report:")
161+
print(report)
162+
return True
163+
164+
if payload.get("__typename") == "ErrorPayload":
165+
print_messages(payload.get("messages", []))
166+
return False
167+
168+
print(f"Unexpected response: {data}")
169+
return False
170+
171+
172+
def parse_arguments():
173+
parser = argparse.ArgumentParser(description="Import a SysML textual file into a SysON project")
174+
parser.add_argument(
175+
"arguments",
176+
nargs="+",
177+
help="Either: file-path project-id, or: url file-path project-id",
178+
)
179+
parser.add_argument(
180+
"--token",
181+
type=str,
182+
help="Bearer token used to authenticate on the SysON server",
183+
)
184+
parser.add_argument(
185+
"--read-only",
186+
action="store_true",
187+
help="Import the uploaded document as read-only",
188+
)
189+
args = parser.parse_args()
190+
191+
if len(args.arguments) == 2:
192+
args.url = "http://localhost:8080"
193+
args.file_path = Path(args.arguments[0])
194+
args.project_id = args.arguments[1]
195+
elif len(args.arguments) == 3:
196+
args.url = args.arguments[0]
197+
args.file_path = Path(args.arguments[1])
198+
args.project_id = args.arguments[2]
199+
else:
200+
parser.error("expected either: file-path project-id, or: url file-path project-id")
201+
202+
return args
203+
204+
205+
if __name__ == "__main__":
206+
args = parse_arguments()
207+
file_path = args.file_path.expanduser().resolve()
208+
209+
if not file_path.is_file():
210+
print(f"File not found: {file_path}")
211+
exit(1)
212+
213+
editing_context_id = fetch_editing_context_id(args.url, args.project_id, args.token) # <6>
214+
if not editing_context_id:
215+
exit(1)
216+
217+
if not import_sysml_file(args.url, file_path, editing_context_id, args.read_only, args.token): # <7>
218+
exit(1)

doc/content/modules/developer-guide/pages/api/api-cookbook.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,5 @@ include::developer-guide:partial$element_owned_elements_recipe.adoc[leveloffset=
6565
include::developer-guide:partial$create_element_recipe.adoc[leveloffset=+2]
6666

6767
include::developer-guide:partial$new_objects_from_text.adoc[leveloffset=+2]
68+
69+
include::developer-guide:partial$import_sysml_file_recipe.adoc[leveloffset=+2]
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
= Import SysML file recipe (python script)
2+
3+
Learn how to import a textual SysML file into an existing {product} project through the Sirius Web GraphQL API.
4+
Each recipe includes a detailed explanation, step-by-step instructions, and sample code.
5+
6+
Recipes covered:
7+
8+
* <<import_sysml_file>>: imports a `.sysml` file as a document in a {product} project.
9+
10+
[#import_sysml_file]
11+
== Import a SysML file
12+
This example demonstrates how to upload a `.sysml` file by using the Sirius Web `uploadDocument` GraphQL mutation.
13+
14+
The script fetches the editing context of the selected project, uploads the file with a multipart GraphQL request, and lets {product} convert the textual SysML content into a model document.
15+
16+
[NOTE]
17+
====
18+
If the file depends on other SysML or KerML files, import those dependencies first.
19+
Otherwise, some relationships may remain unresolved.
20+
====
21+
22+
Example script to import a SysML file:
23+
24+
[source,python]
25+
.import_sysml_file.py
26+
----
27+
include::example$import_sysml_file.py[]
28+
----
29+
30+
*What this code does*:
31+
32+
<1> *Import required libraries*:
33+
34+
* `requests`: Used for sending HTTP requests.
35+
<2> *Define the `build_headers` function* to add an optional bearer token to the HTTP headers.
36+
37+
<3> *Define the `fetch_editing_context_id` function* with three parameters:
38+
39+
* `url`: The {product} server URL.
40+
* `project_id`: The UUID of the project in which the SysML file will be imported.
41+
* `token`: The optional bearer token used for authentication.
42+
43+
<4> *Define the `import_sysml_file` function* with five parameters:
44+
45+
* `url`: The {product} server URL.
46+
* `file_path`: The path of the `.sysml` file to import.
47+
* `editing_context_id`: The UUID of the Sirius Web editing context in which the file will be imported.
48+
* `read_only`: Whether the imported document should be read-only.
49+
* `token`: The optional bearer token used for authentication.
50+
51+
<5> *Sends a `POST` request* to `/api/graphql/upload` with the GraphQL multipart request format used by Sirius Web.
52+
53+
<6> *Fetches the editing context ID* from the project ID.
54+
55+
<7> *Calls the import function* and exits with an error code if the import fails.
56+
57+
Run the script:
58+
[source,bash]
59+
----
60+
$ python import_sysml_file.py file-path-of-sysml-file your-project-id
61+
$ python import_sysml_file.py url-of-syson-server file-path-of-sysml-file your-project-id
62+
----
63+
64+
For example, to import a file into a local {product} server:
65+
[source,bash]
66+
----
67+
$ python import_sysml_file.py /Users/myUserFolder/myFolder/mySysMLv2File.sysml 76bb3f29-17d1-465a-a2ca-a331978c36f3
68+
----
69+
70+
For an authenticated server, provide a bearer token:
71+
[source,bash]
72+
----
73+
$ python import_sysml_file.py https://my-syson-server.example.com /Users/myUserFolder/myFolder/mySysMLv2File.sysml 76bb3f29-17d1-465a-a2ca-a331978c36f3 --token your-token
74+
----
75+
76+
Output example:
77+
[source,bash]
78+
----
79+
SysML file imported successfully: /Users/myUserFolder/myFolder/mySysMLv2File.sysml
80+
----
81+
82+
If the server returns an import report, the script prints it after the success message.
83+
Then you can check the project in the {product} web application to see the imported document.

0 commit comments

Comments
 (0)