Skip to content

[SYNPY-1351, SYNPY-1613] Implement 'Wiki2' model into OOP #1206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 42 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fef882b
add Low-level Functionality to interact with wikipage2 endpoints
danlu1 Jun 11, 2025
977413b
remove unused module
danlu1 Jun 11, 2025
b2a5840
resort
danlu1 Jun 11, 2025
1ce73fd
add synchronous protocol parent class
danlu1 Jun 11, 2025
0685de4
add synchronous protocol for wiki2
danlu1 Jun 11, 2025
0fc3f75
remove redundant docstring
danlu1 Jun 11, 2025
d4a98e8
all pre-signed url to be downloaded directly
danlu1 Jun 13, 2025
86fa041
add rest_get_paginated_async to get paginated results from api call
danlu1 Jun 13, 2025
6601fdc
add unit test for rest_get_paginated_async
danlu1 Jun 16, 2025
b7289f2
add unit test for rest_get_paginated_async
danlu1 Jun 16, 2025
21a4d8d
tweak test cases
danlu1 Jun 16, 2025
e06a35a
use rest_get_paginated_async to get paginated results
danlu1 Jun 16, 2025
2fdfc71
reformat download_functions.py
danlu1 Jun 16, 2025
176cdf8
use specified destination for pre-signed url downloads
danlu1 Jun 17, 2025
c52996f
remove unused modules
danlu1 Jun 23, 2025
99cbb81
update function names
danlu1 Jun 23, 2025
47f38e0
define optional params
danlu1 Jun 23, 2025
a620db5
update function names
danlu1 Jun 23, 2025
4e89258
add tutorials
danlu1 Jun 23, 2025
da4dfa4
remove unwanted module name
danlu1 Jun 23, 2025
be5aae4
update typing hint
danlu1 Jun 23, 2025
1ab9888
make sure gzip file always be removed after getting the file handle id
danlu1 Jun 23, 2025
082df85
update filename for markdown gzip file
danlu1 Jun 24, 2025
43bb9a0
update tutorials
danlu1 Jun 24, 2025
0a5f2e6
update markdown name if it's created from text to make it more inform…
danlu1 Jun 24, 2025
c6077e0
reorder functions for order hint
danlu1 Jun 24, 2025
cef775b
update wikipage name and instructions
danlu1 Jun 24, 2025
e32da1b
reorder and fill in details for tutorial md
danlu1 Jun 24, 2025
50417a3
remove unwanted comments
danlu1 Jun 24, 2025
d59a9d6
make low-level functionalities importable from synapseclient.api
danlu1 Jun 25, 2025
a317bc9
rename post_wiki to post_wiki_page
danlu1 Jun 25, 2025
9ae4c81
remove debug code
danlu1 Jun 25, 2025
d52720c
remove unwanted decorator
danlu1 Jun 27, 2025
cbcbf35
refactor wikipage store function to put validation at the beginning a…
danlu1 Jun 27, 2025
5c66880
remove debug statement
danlu1 Jun 30, 2025
d8055d6
simplify get_async for wikipage
danlu1 Jun 30, 2025
9d923ff
remove redirect and reorganize args and kwargs
danlu1 Jul 3, 2025
b362230
set default values for get_wiki_history
danlu1 Jul 9, 2025
02b509a
add async and sync unit test for wiki model
danlu1 Jul 9, 2025
62f03d4
remove redirect params
danlu1 Jul 9, 2025
e56540e
update presigned url provider params for multithread downloader
danlu1 Jul 9, 2025
a46417c
remove duplicate expired presigned url checking
danlu1 Jul 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions docs/tutorials/python/tutorial_scripts/wiki.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""
Tutorial script demonstrating the Synapse Wiki models functionality.

This script shows how to:
1. Create, read, and update wiki pages
2. Work with WikiPage Markdown
Copy link
Member

@BryanFauble BryanFauble Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there another descriptive way we can word some of these actions rather than "Work with.." maybe, describing the create/update/delete/restore actions taking place during that section of code?

3. Work with WikiPage Attachments
4. Work with WikiHeader
5. Work with WikiHistorySnapshot
6. Work with WikiOrderHint
7. Delete wiki pages
"""
import gzip
import os
import uuid

from synapseclient import Synapse
from synapseclient.models import (
Project,
WikiHeader,
WikiHistorySnapshot,
WikiOrderHint,
WikiPage,
)

# Initialize Synapse client
syn = Synapse()
syn.login()

# Create a Synapse Project to work with
my_test_project = Project(
name=f"My Test Project_{uuid.uuid4()}",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check out the other tutorials created. There is a theme around a project for alzheimer's disease that i had been following.

The tutorial is great, and covers everything one would care to do. Let's see if we can tweak the theme of the content just a bit.

description="This is a test project for the wiki tutorial.",
).store()
print(f"Created project: {my_test_project.name} with ID: {my_test_project.id}")

# Section1: Create, read, and update wiki pages
# Create a new wiki page for the project with plain text markdown
root_wiki_page = WikiPage(
owner_id=my_test_project.id,
title="My Root Wiki Page",
markdown="# Welcome to My Root Wiki\n\nThis is a sample root wiki page created with the Synapse client.",
).store()

# OR you can create a wiki page with an existing markdown file. More instructions can be found in section 2.
markdown_file_path = "path/to/your_markdown_file.md"
root_wiki_page = WikiPage(
owner_id=my_test_project.id,
title="My First Root Wiki Page Version with existing markdown file",
markdown=markdown_file_path,
).store()

# Create a new wiki page with updated content
root_wiki_page_new = WikiPage(
owner_id=my_test_project.id,
title="My First Root Wiki Page NEW",
markdown="# Welcome to My Root Wiki NEW\n\nThis is a sample root wiki page created with the Synapse client.",
id=root_wiki_page.id,
).store()

# Restore the wiki page to the original version
wiki_page_restored = WikiPage(
owner_id=my_test_project.id, id=root_wiki_page.id, wiki_version="0"
).restore()

# check if the content is restored
comparisons = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably easier and cleaner to use assert statements here. Like

assert condition, "Message for failure"

That was if it does fail, it's clear which one failed the compare check

root_wiki_page.markdown_file_handle_id
== wiki_page_restored.markdown_file_handle_id,
root_wiki_page.id == wiki_page_restored.id,
root_wiki_page.title == wiki_page_restored.title,
]
print(f"All fields match: {all(comparisons)}")

# Create a sub-wiki page
sub_wiki_1 = WikiPage(
owner_id=my_test_project.id,
title="Sub Wiki Page 1",
parent_id=root_wiki_page.id,
markdown="# Sub Page 1\n\nThis is a sub-page of another wiki.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Out of curiosity. If you use a multi-line python string:

multi_line_string = """
Content
On
This
Line
"""

Does that automatically add in the newline characters? Or are you forced to use the \n characters?

).store()

# Get an existing wiki page for the project, now you can see one root wiki page and one sub-wiki page
wiki_header_tree = WikiHeader.get(owner_id=my_test_project.id)
print(wiki_header_tree)

# Once you know the wiki page id, you can retrieve the wiki page with the id
retrieved_wiki = WikiPage(owner_id=my_test_project.id, id=sub_wiki_1.id).get()
print(f"Retrieved wiki page with title: {retrieved_wiki.title}")

# Or you can retrieve the wiki page with the title
retrieved_wiki = WikiPage(owner_id=my_test_project.id, title=sub_wiki_1.title).get()
print(f"Retrieved wiki page with title: {retrieved_wiki.title}")

# Check if the retrieved wiki page is the same as the original wiki page
comparisons = [
sub_wiki_1.markdown_file_handle_id == retrieved_wiki.markdown_file_handle_id,
sub_wiki_1.id == retrieved_wiki.id,
sub_wiki_1.title == retrieved_wiki.title,
]
print(f"All fields match: {all(comparisons)}")

# Section 2: WikiPage Markdown Operations
# Create wiki page from markdown text
markdown_content = """# Sample Markdown Content

## Section 1
This is a sample markdown file with multiple sections.

## Section 2
- List item 1
- List item 2
- List item 3

## Code Example
```python
def hello_world():
print("Hello, World!")
```
"""

# Create wiki page from markdown text
sub_wiki_2 = WikiPage(
owner_id=my_test_project.id,
parent_id=root_wiki_page.id,
title="Sub Page 2 created from markdown text",
markdown=markdown_content,
).store()

# Create a wiki page from a markdown file
# Create a temporary markdown gzipped file from the markdown_content
markdown_file_path = "temp_markdown_file.md.gz"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most of the tutorials we have been using the "~/temp` directory to write files to

with gzip.open(markdown_file_path, "wt", encoding="utf-8") as gz:
gz.write("This is a markdown file")

# Create wiki page from markdown file
sub_wiki_3 = WikiPage(
owner_id=my_test_project.id,
parent_id=root_wiki_page.id,
title="Sub Page 3 created from markdown file",
markdown=markdown_file_path,
).store()

# Download the markdown file
# delete the markdown file after uploading to test the download function
os.remove(markdown_file_path)
# Note: If the markdown is generated from plain text using the client, the downloaded file will be named wiki_markdown_<wiki_page_title>.md.gz. If it is generated from an existing markdown file, the downloaded file will retain the original filename with the .gz suffix appended.
# Download the markdown file for sub_wiki_2 that is created from markdown text
wiki_page_markdown_2 = WikiPage(
owner_id=my_test_project.id, id=sub_wiki_2.id
).get_markdown(
download_file=True,
download_location=".",
download_file_name=f"wiki_markdown_{sub_wiki_2.title}.md.gz",
)
print(
f"Wiki page markdown for sub_wiki_2 successfully downloaded: {os.path.exists(f'wiki_markdown_{sub_wiki_2.title}.md.gz')}"
)
# clean up the downloaded markdown file
os.remove(f"wiki_markdown_{sub_wiki_2.title}.md.gz")

# Download the markdown file for sub_wiki_3 that is created from a markdown file
wiki_page_markdown_3 = WikiPage(
owner_id=my_test_project.id, id=sub_wiki_3.id
).get_markdown(
download_file=True, download_location=".", download_file_name=markdown_file_path
)
print(
f"Wiki page markdown for sub_wiki_3 successfully downloaded: {os.path.exists(markdown_file_path)}"
)
# clean up the downloaded markdown file
os.remove(markdown_file_path)

# Section 3: WikiPage with Attachments
# Create a temporary file for the attachment
attachment_file_name = "temp_attachment.txt.gz"
with gzip.open(attachment_file_name, "wt", encoding="utf-8") as gz:
gz.write("This is a sample attachment.")

# reformat '.' and '_' in the attachment file name to be a valid attachment path
Copy link
Member

@BryanFauble BryanFauble Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit unfortunate that we have to do this client side. I wonder if there is a better way that we can do this though. Maybe a static function on WikiPage that we can point to.

Is it possible that we take the string from the markdown field and automatically convert these? I imagine that . and _ are not the only characters that have to be encoded.

attachment_file_name_reformatted = attachment_file_name.replace(".", "%2E")
attachment_file_name_reformatted = attachment_file_name_reformatted.replace("_", "%5F")

sub_wiki_4 = WikiPage(
owner_id=my_test_project.id,
parent_id=root_wiki_page.id,
title="Sub Page 4 with Attachments",
markdown=f"# Sub Page 4 with Attachments\n\nThis is a attachment: ${{previewattachment?fileName={attachment_file_name_reformatted}}}",
attachments=[attachment_file_name],
).store()

# Get attachment handles
attachment_handles = WikiPage(
owner_id=my_test_project.id, id=sub_wiki_4.id
).get_attachment_handles()
print(f"Attachment handles: {attachment_handles['list']}")

# Get attachment URL without downloading
wiki_page_attachment_url = WikiPage(
owner_id=my_test_project.id, id=sub_wiki_4.id
).get_attachment(
file_name="temp_attachment.txt.gz",
download_file=False,
)
print(f"Attachment URL: {wiki_page_attachment_url}")

# Download an attachment
# Delete the attachment file after uploading to test the download function
os.remove(attachment_file_name)
wiki_page_attachment = WikiPage(
owner_id=my_test_project.id, id=sub_wiki_4.id
).get_attachment(
file_name=attachment_file_name,
download_file=True,
download_location=".",
)
print(f"Attachment downloaded: {os.path.exists(attachment_file_name)}")
os.remove(attachment_file_name)

# Download an attachment preview. Instead of using the file_name from the attachmenthandle response when isPreview=True, you should use the original file name in the get_attachment_preview request. The downloaded file will still be named according to the file_name provided in the response when isPreview=True.
# Get attachment preview URL without downloading
attachment_preview_url = WikiPage(
owner_id=my_test_project.id, id=sub_wiki_4.id
).get_attachment_preview(
file_name="temp_attachment.txt.gz",
download_file=False,
)
print(f"Attachment preview URL: {attachment_preview_url}")

# Download an attachment preview
attachment_preview = WikiPage(
owner_id=my_test_project.id, id=sub_wiki_4.id
).get_attachment_preview(
file_name="temp_attachment.txt.gz",
download_file=True,
download_location=".",
)
# From the attachment preview URL or attachment handle response, the downloaded preview file is preview.txt
os.remove("preview.txt")

# Section 4: WikiHeader - Working with Wiki Hierarchy
# Get wiki header tree (hierarchy)
headers = WikiHeader.get(owner_id=my_test_project.id)
print(f"Found {len(headers)} wiki pages in hierarchy")
print(f"Wiki header tree: {headers}")

# Section 5. WikiHistorySnapshot - Version History
# Get wiki history for root_wiki_page
history = WikiHistorySnapshot.get(owner_id=my_test_project.id, id=root_wiki_page.id)
print(f"History: {history}")

# Section 6. WikiOrderHint - Ordering Wiki Pages
# Set the wiki order hint
order_hint = WikiOrderHint(owner_id=my_test_project.id).get()
print(f"Order hint for {my_test_project.id}: {order_hint.id_list}")
# As you can see from the printed message, the order hint is not set by default, so you need to set it explicitly at the beginning.
order_hint.id_list = [
root_wiki_page.id,
sub_wiki_3.id,
sub_wiki_4.id,
sub_wiki_1.id,
sub_wiki_2.id,
]
order_hint.store()
print(f"Order hint for {my_test_project.id}: {order_hint}")

# Update wiki order hint
order_hint = WikiOrderHint(owner_id=my_test_project.id).get()
order_hint.id_list = [
root_wiki_page.id,
sub_wiki_1.id,
sub_wiki_2.id,
sub_wiki_3.id,
sub_wiki_4.id,
]
order_hint.store()
print(f"Order hint for {my_test_project.id}: {order_hint}")

# Delete a wiki page
wiki_page_to_delete = WikiPage(owner_id=my_test_project.id, id=sub_wiki_3.id).delete()

# clean up
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would leave the project delete call out of the tutorial.

For the wiki page that is being deleted, I might consider making a new wiki page specifically to delete, rather than delete one of the ones used earlier in the tutorial

my_test_project.delete()
Loading
Loading