Skip to content

Commit ce2df17

Browse files
committed
[doc] Add new recipes in API cookbook
Signed-off-by: Axel RICHARD <axel.richard@obeo.fr>
1 parent 25c95b0 commit ce2df17

7 files changed

Lines changed: 398 additions & 34 deletions

File tree

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
from pathlib import Path
16+
17+
import requests # <1>
18+
19+
20+
GRAPHQL_ENDPOINT = "/api/graphql"
21+
DOWNLOAD_ENDPOINT = "/api/editingcontexts/{editing_context_id}/documents/{document_id}"
22+
23+
24+
fetch_editing_context_query = """
25+
query FetchEditingContext($projectId: ID!) {
26+
viewer {
27+
project(projectId: $projectId) {
28+
currentEditingContext {
29+
id
30+
}
31+
}
32+
}
33+
}
34+
"""
35+
36+
37+
def get_graphql_url(url):
38+
return f"{url.rstrip('/')}{GRAPHQL_ENDPOINT}"
39+
40+
41+
def get_download_url(url, editing_context_id, document_id):
42+
return f"{url.rstrip('/')}{DOWNLOAD_ENDPOINT.format(editing_context_id=editing_context_id, document_id=document_id)}"
43+
44+
45+
def print_graphql_errors(data):
46+
errors = data.get("errors", [])
47+
for error in errors:
48+
print(f"GraphQL error: {error.get('message', error)}")
49+
50+
51+
def fetch_editing_context_id(url, project_id): # <2>
52+
response = requests.post(
53+
get_graphql_url(url),
54+
json={
55+
"query": fetch_editing_context_query,
56+
"variables": {"projectId": project_id},
57+
},
58+
)
59+
60+
if response.status_code != 200:
61+
print(f"Error fetching editing context: {response.status_code} - {response.text}")
62+
return None
63+
64+
data = response.json()
65+
if data.get("errors"):
66+
print_graphql_errors(data)
67+
return None
68+
69+
project = data.get("data", {}).get("viewer", {}).get("project")
70+
if not project:
71+
print(f"Project not found: {project_id}")
72+
return None
73+
74+
editing_context = project.get("currentEditingContext")
75+
if not editing_context:
76+
print(f"Editing context not found for project: {project_id}")
77+
return None
78+
79+
return editing_context.get("id")
80+
81+
82+
def download_sysml_file(url, project_id, document_id, output_path): # <3>
83+
editing_context_id = fetch_editing_context_id(url, project_id) # <6>
84+
if not editing_context_id:
85+
return False
86+
87+
response = requests.get(
88+
get_download_url(url, editing_context_id, document_id), # <4>
89+
headers={"Accept": "text/html"},
90+
stream=True, # <5>
91+
)
92+
93+
if response.status_code != 200:
94+
print(f"Error downloading SysML file: {response.status_code} - {response.text}")
95+
return False
96+
97+
output_path.parent.mkdir(parents=True, exist_ok=True)
98+
with open(output_path, "wb") as file:
99+
for chunk in response.iter_content(chunk_size=8192):
100+
if chunk:
101+
file.write(chunk)
102+
103+
print(f"SysML file downloaded successfully: {output_path}")
104+
return True
105+
106+
107+
def parse_arguments():
108+
parser = argparse.ArgumentParser(description="Download a SysML textual file from a SysON project")
109+
parser.add_argument(
110+
"arguments",
111+
nargs="+",
112+
help="Either: project-id document-id output-path, or: url project-id document-id output-path",
113+
)
114+
args = parser.parse_args()
115+
116+
if len(args.arguments) == 3:
117+
args.url = "http://localhost:8080"
118+
args.project_id = args.arguments[0]
119+
args.document_id = args.arguments[1]
120+
args.output_path = Path(args.arguments[2])
121+
elif len(args.arguments) == 4:
122+
args.url = args.arguments[0]
123+
args.project_id = args.arguments[1]
124+
args.document_id = args.arguments[2]
125+
args.output_path = Path(args.arguments[3])
126+
else:
127+
parser.error("expected either: project-id document-id output-path, or: url project-id document-id output-path")
128+
129+
return args
130+
131+
132+
if __name__ == "__main__":
133+
args = parse_arguments()
134+
output_path = args.output_path.expanduser().resolve()
135+
136+
if not download_sysml_file(args.url, args.project_id, args.document_id, output_path): # <7>
137+
exit(1)

doc/content/modules/developer-guide/examples/import_sysml_file.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,6 @@
5555
"""
5656

5757

58-
def build_headers(token): # <2>
59-
headers = {}
60-
if token:
61-
headers["Authorization"] = f"Bearer {token}"
62-
return headers
63-
64-
6558
def get_graphql_url(url):
6659
return f"{url.rstrip('/')}{GRAPHQL_ENDPOINT}"
6760

@@ -76,14 +69,13 @@ def print_graphql_errors(data):
7669
print(f"GraphQL error: {error.get('message', error)}")
7770

7871

79-
def fetch_editing_context_id(url, project_id, token): # <3>
72+
def fetch_editing_context_id(url, project_id): # <2>
8073
response = requests.post(
8174
get_graphql_url(url),
8275
json={
8376
"query": fetch_editing_context_query,
8477
"variables": {"projectId": project_id},
8578
},
86-
headers=build_headers(token),
8779
)
8880

8981
if response.status_code != 200:
@@ -113,7 +105,7 @@ def print_messages(messages):
113105
print(f"{message.get('level', 'INFO')}: {message.get('body', '')}")
114106

115107

116-
def import_sysml_file(url, file_path, editing_context_id, read_only, token): # <4>
108+
def import_sysml_file(url, file_path, editing_context_id, read_only): # <3>
117109
operation_id = str(uuid.uuid4())
118110
operations = {
119111
"query": upload_document_mutation,
@@ -131,7 +123,7 @@ def import_sysml_file(url, file_path, editing_context_id, read_only, token): #
131123
}
132124

133125
with open(file_path, "rb") as file:
134-
response = requests.post( # <5>
126+
response = requests.post( # <4>
135127
get_graphql_upload_url(url),
136128
data={
137129
"operations": json.dumps(operations),
@@ -140,7 +132,6 @@ def import_sysml_file(url, file_path, editing_context_id, read_only, token): #
140132
files={
141133
"0": (file_path.name, file, "text/plain"),
142134
},
143-
headers=build_headers(token),
144135
)
145136

146137
if response.status_code not in (200, 201):
@@ -176,11 +167,6 @@ def parse_arguments():
176167
nargs="+",
177168
help="Either: file-path project-id, or: url file-path project-id",
178169
)
179-
parser.add_argument(
180-
"--token",
181-
type=str,
182-
help="Bearer token used to authenticate on the SysON server",
183-
)
184170
parser.add_argument(
185171
"--read-only",
186172
action="store_true",
@@ -210,9 +196,9 @@ def parse_arguments():
210196
print(f"File not found: {file_path}")
211197
exit(1)
212198

213-
editing_context_id = fetch_editing_context_id(args.url, args.project_id, args.token) # <6>
199+
editing_context_id = fetch_editing_context_id(args.url, args.project_id) # <5>
214200
if not editing_context_id:
215201
exit(1)
216202

217-
if not import_sysml_file(args.url, file_path, editing_context_id, args.read_only, args.token): # <7>
203+
if not import_sysml_file(args.url, file_path, editing_context_id, args.read_only): # <6>
218204
exit(1)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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 io
16+
import json
17+
import zipfile
18+
19+
import requests # <1>
20+
21+
22+
PROJECT_DOWNLOAD_ENDPOINT = "/api/projects/{project_id}"
23+
24+
25+
def get_project_download_url(url, project_id):
26+
return f"{url.rstrip('/')}{PROJECT_DOWNLOAD_ENDPOINT.format(project_id=project_id)}"
27+
28+
29+
def download_project_archive(url, project_id): # <2>
30+
response = requests.get(
31+
get_project_download_url(url, project_id), # <3>
32+
stream=True,
33+
)
34+
35+
if response.status_code != 200:
36+
print(f"Error downloading project archive: {response.status_code} - {response.text}")
37+
return None
38+
39+
archive_content = io.BytesIO()
40+
for chunk in response.iter_content(chunk_size=8192):
41+
if chunk:
42+
archive_content.write(chunk)
43+
44+
archive_content.seek(0)
45+
with zipfile.ZipFile(archive_content) as archive:
46+
manifest_name = None
47+
for entry_name in archive.namelist():
48+
if entry_name.endswith("/manifest.json"):
49+
manifest_name = entry_name
50+
break
51+
52+
if not manifest_name:
53+
print("manifest.json not found in the project archive")
54+
return None
55+
56+
with archive.open(manifest_name) as manifest_file:
57+
manifest = json.load(io.TextIOWrapper(manifest_file, encoding="utf-8")) # <4>
58+
59+
document_ids_to_name = manifest.get("documentIdsToName", {})
60+
if not document_ids_to_name:
61+
print("No documents found in the project manifest")
62+
return []
63+
64+
documents = sorted(document_ids_to_name.items(), key=lambda item: item[1].lower())
65+
return documents
66+
67+
68+
def parse_arguments():
69+
parser = argparse.ArgumentParser(description="Retrieve the documents of a SysON project")
70+
parser.add_argument(
71+
"arguments",
72+
nargs="+",
73+
help="Either: project-id, or: url project-id",
74+
)
75+
args = parser.parse_args()
76+
77+
if len(args.arguments) == 1:
78+
args.url = "http://localhost:8080"
79+
args.project_id = args.arguments[0]
80+
elif len(args.arguments) == 2:
81+
args.url = args.arguments[0]
82+
args.project_id = args.arguments[1]
83+
else:
84+
parser.error("expected either: project-id, or: url project-id")
85+
86+
return args
87+
88+
89+
if __name__ == "__main__":
90+
args = parse_arguments()
91+
documents = download_project_archive(args.url, args.project_id)
92+
93+
if documents is None:
94+
raise SystemExit(1)
95+
96+
for document_id, document_name in documents: # <5>
97+
print(f"Document Name: {document_name}, Document ID: {document_id}")

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,8 @@ include::developer-guide:partial$new_objects_from_text.adoc[leveloffset=+2]
6868

6969
include::developer-guide:partial$import_sysml_file_recipe.adoc[leveloffset=+2]
7070

71+
include::developer-guide:partial$retrieve_project_documents_recipe.adoc[leveloffset=+2]
72+
73+
include::developer-guide:partial$download_sysml_file_recipe.adoc[leveloffset=+2]
74+
7175
include::developer-guide:partial$delete_element_recipe.adoc[leveloffset=+2]

0 commit comments

Comments
 (0)