# wikidot.py

> A Python library for interacting with Wikidot sites (SCP Foundation, etc.)

- Version: 4.0.x
- Python: 3.10+
- Dependencies: httpx, beautifulsoup4, lxml
- License: MIT
- Documentation: https://ukwhatn.github.io/wikidot.py/
- Repository: https://github.com/ukwhatn/wikidot.py

## Installation

```bash
pip install wikidot
# or with uv
uv add wikidot
```

---

## Quick Start

```python
import wikidot

# Unauthenticated client (read-only operations)
with wikidot.Client() as client:
    site = client.site.get("scp-jp")
    page = site.page.get("scp-173")
    print(page.title, page.rating)

# Authenticated client (full operations)
with wikidot.Client(username="user", password="pass") as client:
    site = client.site.get("scp-jp")

    # Search pages
    pages = site.pages.search(category="scp", order="rating desc", limit=10)
    for page in pages:
        print(page.fullname, page.rating)
```

---

## Client

Main entry point. Provides authentication and accessor access.

### Import

```python
import wikidot
# or
from wikidot import Client
```

### Constructor

```python
client = wikidot.Client(
    username=None,      # Wikidot username (optional)
    password=None,      # Wikidot password (optional)
    amc_config=None,    # Custom AMC configuration (optional)
    logging_level="WARNING"  # Logging level (optional)
)
```

#### Parameters

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `username` | `str \| None` | No | `None` | Wikidot username |
| `password` | `str \| None` | No | `None` | Wikidot password |
| `amc_config` | `AjaxModuleConnectorConfig \| None` | No | `None` | Custom AMC configuration |
| `logging_level` | `str` | No | `"WARNING"` | Logging level |

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `site` | `ClientSiteAccessor` | Site operations accessor |
| `user` | `ClientUserAccessor` | User operations accessor |
| `private_message` | `ClientPrivateMessageAccessor` | Private message accessor |
| `is_logged_in` | `bool` | Login status |
| `username` | `str \| None` | Logged-in username |
| `me` | `User \| None` | Logged-in user object |

### Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `login_check()` | `None` | Raise `LoginRequiredException` if not logged in |
| `close()` | `None` | Logout and cleanup resources |
| `__enter__()` | `Client` | Context manager entry |
| `__exit__(...)` | `None` | Context manager exit (auto logout) |

### Usage

```python
# With context manager (recommended)
with wikidot.Client(username="user", password="pass") as client:
    if client.is_logged_in:
        print(f"Logged in as: {client.username}")
        print(f"User ID: {client.me.id}")

    # ... operations ...
# Auto logout on exit

# Without context manager
client = wikidot.Client()
try:
    # ... operations ...
finally:
    client.close()
```

---

## Site

Site operations. Access pages, forums, and members.

### Getting a Site

```python
site = client.site.get("scp-jp")
```

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `client` | `Client` | Parent client |
| `id` | `int` | Site ID |
| `title` | `str` | Site title |
| `unix_name` | `str` | URL identifier (e.g., `scp-jp`) |
| `domain` | `str` | Site domain |
| `ssl_supported` | `bool` | SSL support flag |
| `url` | `str` | Full site URL |
| `page` | `SitePageAccessor` | Single page operations |
| `pages` | `SitePagesAccessor` | Page list operations |
| `forum` | `SiteForumAccessor` | Forum operations |
| `members` | `list[SiteMember]` | Member list (cached) |
| `moderators` | `list[SiteMember]` | Moderator list (cached) |
| `admins` | `list[SiteMember]` | Admin list (cached) |
| `applications` | `list[SiteApplication]` | Pending membership applications (login required) |

### Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `get_thread(thread_id)` | `ForumThread` | Get forum thread by ID |
| `get_threads(thread_ids)` | `ForumThreadCollection` | Get multiple threads |
| `member_lookup(user_name, user_id=None)` | `bool` | Check if user is a member |
| `invite_user(user, text)` | `None` | Invite user to site (login required) |
| `get_recent_changes(limit=None)` | `list[SiteChange]` | Get recent change history |

### SiteChange Properties

| Property | Type | Description |
|----------|------|-------------|
| `site` | `Site` | Parent site |
| `page_fullname` | `str` | Page fullname |
| `page_title` | `str` | Page title |
| `revision_no` | `int` | Revision number |
| `changed_by` | `AbstractUser` | Editor |
| `changed_at` | `datetime` | Edit date |
| `flags` | `list[str]` | Change flags (N/S/T/R/M/F/A) |
| `comment` | `str \| None` | Edit comment |

Change flags: `N`=New, `S`=Source changed, `T`=Title changed, `R`=Renamed, `M`=Moved, `F`=File, `A`=Deleted

### Usage

```python
site = client.site.get("scp-jp")

print(f"Site: {site.title}")
print(f"URL: {site.url}")
print(f"SSL: {site.ssl_supported}")

# Recent changes
changes = site.get_recent_changes(limit=100)
for change in changes:
    flags_str = "".join(change.flags)
    print(f"[{flags_str}] {change.page_fullname} (rev.{change.revision_no})")
    print(f"    by {change.changed_by.name} at {change.changed_at}")
```

---

## Page

Page operations including edit, delete, vote, files, and revisions.

### Getting a Page

```python
# Single page
page = site.page.get("scp-173")

# With option to return None instead of raising exception
page = site.page.get("nonexistent", raise_when_not_found=False)  # Returns None
```

### Creating a Page

```python
page = site.page.create(
    fullname="test:new-page",
    title="New Page Title",
    source="Page content in Wikidot markup",
    comment="Initial creation"
)
```

#### Create Parameters

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `fullname` | `str` | Yes | - | Full page name (category:name) |
| `title` | `str` | No | `""` | Page title |
| `source` | `str` | No | `""` | Page source in Wikidot markup |
| `comment` | `str` | No | `""` | Creation comment |
| `force_edit` | `bool` | No | `False` | Force edit if page exists |

### Page Properties

| Property | Type | Description |
|----------|------|-------------|
| `site` | `Site` | Parent site |
| `id` | `int` | Page ID (lazy loaded) |
| `fullname` | `str` | Full page name (e.g., `scp:scp-173`) |
| `name` | `str` | Page name without category |
| `category` | `str` | Page category |
| `title` | `str` | Page title |
| `rating` | `int \| float` | Rating value |
| `rating_votes` | `int` | Total vote count |
| `rating_percent` | `float` | 5-star rating percentage (if applicable) |
| `size` | `int` | Page size in bytes |
| `tags` | `list[str]` | Tag list (mutable) |
| `created_by` | `AbstractUser` | Page creator |
| `created_at` | `datetime` | Creation date |
| `updated_by` | `AbstractUser` | Last editor |
| `updated_at` | `datetime` | Last update date |
| `parent_fullname` | `str \| None` | Parent page fullname |
| `revisions_count` | `int` | Number of revisions |
| `children_count` | `int` | Number of child pages |
| `comments_count` | `int` | Number of comments |
| `commented_by` | `AbstractUser \| None` | Last commenter |
| `commented_at` | `datetime \| None` | Last comment date |
| `source` | `PageSource` | Page source (lazy loaded) |
| `revisions` | `PageRevisionCollection` | Revision history (lazy loaded) |
| `latest_revision` | `PageRevision` | Latest revision |
| `votes` | `PageVoteCollection` | Vote information (lazy loaded) |
| `discussion` | `ForumThread \| None` | Discussion thread (lazy loaded) |
| `files` | `PageFileCollection` | Attached files (lazy loaded) |
| `metas` | `dict[str, str]` | Meta tags (lazy loaded, settable) |

### Page Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `get_url()` | `str` | Get page URL |
| `is_id_acquired()` | `bool` | Check if page ID is cached |
| `edit(**kwargs)` | `Page` | Edit page (login required) |
| `destroy()` | `None` | Delete page (login required) |
| `rename(new_fullname)` | `Page` | Rename page (login required) |
| `set_parent(parent_fullname)` | `Page` | Set parent page (login required) |
| `commit_tags()` | `Page` | Save tag changes (login required) |
| `vote(value)` | `int` | Vote (+1/-1), returns new rating (login required) |
| `cancel_vote()` | `int` | Cancel vote, returns new rating (login required) |

### Edit Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `title` | `str \| None` | No | New page title (None = no change) |
| `source` | `str \| None` | No | New page source (None = no change) |
| `comment` | `str` | No | Edit comment |
| `force_edit` | `bool` | No | Force edit even if locked |

### Usage

```python
# Get page information
page = site.page.get("scp-173")
print(f"Title: {page.title}")
print(f"Rating: {page.rating}")
print(f"Tags: {page.tags}")
print(f"URL: {page.get_url()}")

# Get source
print(page.source.wiki_text)

# Edit page (login required)
page = page.edit(
    title="Updated Title",
    source="Updated content",
    comment="Fixed typo"
)

# Modify tags
page.tags.append("new-tag")
page.tags.remove("old-tag")
page = page.commit_tags()

# Vote
new_rating = page.vote(1)   # +1 vote
new_rating = page.vote(-1)  # -1 vote
new_rating = page.cancel_vote()  # Cancel vote

# Set parent page
page = page.set_parent("parent-page")
page = page.set_parent(None)  # Remove parent

# Rename page
page = page.rename("new-fullname")

# Delete page
page.destroy()
```

---

## Page Search (ListPagesModule)

Search pages using ListPagesModule parameters.

### Import

```python
from wikidot import Client
```

### Basic Usage

```python
# Search with keyword arguments
pages = site.pages.search(
    category="scp",
    tags=["safe", "-explained"],
    rating=">100",
    order="rating desc",
    limit=50
)
```

### Search Parameters

#### Selection Parameters

| Parameter | Type | Description | Example |
|-----------|------|-------------|---------|
| `pagetype` | `str` | Page type | `"*"`, `"normal"`, `"hidden"` |
| `category` | `str` | Category filter | `"*"`, `"scp"`, `"."` (current) |
| `tags` | `str \| list[str]` | Tag filter (- for exclude) | `["scp", "euclid"]`, `["-tale"]` |
| `parent` | `str` | Parent page | `"parent-page"`, `"-"` (no parent) |
| `link_to` | `str` | Pages linking to | `"scp-173"` |
| `created_by` | `User \| str` | Author | `"username"` or User object |
| `created_at` | `str` | Creation date | `"2024"`, `"last 7 day"`, `">2024-01-01"` |
| `updated_at` | `str` | Update date | `"last 1 week"`, `"<2024-12-31"` |
| `rating` | `str` | Rating filter | `">50"`, `">=100"` |
| `votes` | `str` | Vote count filter | `">10"`, `">=5"` |
| `name` | `str` | Page name pattern | `"scp-*"`, `"about"` |
| `fullname` | `str` | Exact fullname match | `"scp:scp-173"` |
| `range` | `str` | Range specification | Page range |

#### Ordering Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `order` | `str` | `"created_at desc"` | Sort order |

Valid order values: `created_at`, `updated_at`, `rating`, `name`, `size`, `random` (add `desc` for descending)

#### Pagination Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `offset` | `int` | `0` | Starting position |
| `limit` | `int \| None` | `None` | Maximum results (None = unlimited) |
| `perPage` | `int` | `250` | Results per request (max 250) |

### Usage

```python
# Top rated SCP articles
pages = site.pages.search(
    category="scp",
    order="rating desc",
    limit=10
)
for page in pages:
    print(f"{page.fullname}: +{page.rating}")

# Pages by specific author
user = client.user.get("author-name")
pages = site.pages.search(created_by=user, limit=50)

# Recent pages with specific tags
pages = site.pages.search(
    tags=["safe", "+scp"],
    created_at="last 30 day",
    order="created_at desc"
)

# All pages in a category
pages = site.pages.search(category="component")
```

---

## PageCollection

Collection of pages with bulk operations.

### Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `find(fullname)` | `Page \| None` | Find page by fullname |
| `get_page_ids()` | `PageCollection` | Bulk fetch page IDs |
| `get_page_sources()` | `PageCollection` | Bulk fetch page sources |
| `get_page_revisions()` | `PageCollection` | Bulk fetch revision histories |
| `get_page_votes()` | `PageCollection` | Bulk fetch vote information |

### Usage

```python
pages = site.pages.search(category="scp", limit=100)

# Find specific page
page = pages.find("scp-173")

# Bulk operations (efficient)
pages.get_page_ids()
pages.get_page_sources()
pages.get_page_revisions()
pages.get_page_votes()

# Iteration
for page in pages:
    print(f"{page.fullname}: {page.title}")
    print(f"  Source length: {len(page.source.wiki_text)}")
```

---

## PageRevision

Page revision information.

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `page` | `Page` | Parent page |
| `id` | `int` | Revision ID |
| `rev_no` | `int` | Revision number |
| `created_by` | `AbstractUser` | Editor |
| `created_at` | `datetime` | Edit date |
| `comment` | `str` | Edit comment |
| `source` | `PageSource` | Revision source (lazy loaded) |
| `html` | `str` | Revision HTML (lazy loaded) |

### PageRevisionCollection Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `find(id)` | `PageRevision \| None` | Find revision by ID |
| `get_sources()` | `PageRevisionCollection` | Bulk fetch sources |
| `get_htmls()` | `PageRevisionCollection` | Bulk fetch HTML |

### Usage

```python
# Get revision history
for revision in page.revisions:
    print(f"Rev {revision.rev_no}: {revision.created_at} by {revision.created_by.name}")
    print(f"  Comment: {revision.comment}")

# Get latest revision
latest = page.latest_revision
print(latest.source.wiki_text)

# Bulk fetch sources
page.revisions.get_sources()
for rev in page.revisions:
    print(len(rev.source.wiki_text))
```

---

## PageSource

Page source code container.

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `page` | `Page` | Parent page |
| `wiki_text` | `str` | Wikidot markup source |

### Usage

```python
source = page.source
print(source.wiki_text)
```

---

## PageVote

Vote information for a page.

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `page` | `Page` | Parent page |
| `user` | `AbstractUser` | Voter |
| `value` | `int` | Vote value (+1, -1, or numeric) |

### PageVoteCollection Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `find(user)` | `PageVote \| None` | Find vote by user |

### Usage

```python
for vote in page.votes:
    sign = "+" if vote.value > 0 else ""
    print(f"{vote.user.name}: {sign}{vote.value}")

# Find specific user's vote
user = client.user.get("username")
vote = page.votes.find(user)
if vote:
    print(f"User voted: {vote.value}")
```

---

## PageFile

Attached file information.

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `page` | `Page` | Parent page |
| `id` | `int` | File ID |
| `name` | `str` | File name |
| `url` | `str` | Download URL |
| `mime_type` | `str` | MIME type |
| `size` | `int` | File size in bytes |

### PageFileCollection Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `find(id)` | `PageFile \| None` | Find file by ID |
| `find_by_name(name)` | `PageFile \| None` | Find file by name |

### Usage

```python
for file in page.files:
    print(f"{file.name}: {file.size} bytes")
    print(f"  URL: {file.url}")
    print(f"  Type: {file.mime_type}")

# Find specific file
image = page.files.find_by_name("image.png")
if image:
    print(f"Image URL: {image.url}")
```

---

## ForumCategory

Forum category operations.

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `site` | `Site` | Parent site |
| `id` | `int` | Category ID |
| `title` | `str` | Category title |
| `description` | `str` | Category description |
| `threads_count` | `int` | Number of threads |
| `posts_count` | `int` | Number of posts |
| `threads` | `ForumThreadCollection` | Threads (lazy loaded, cached) |

### Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `reload_threads()` | `ForumThreadCollection` | Force reload threads |
| `create_thread(title, description, source)` | `ForumThread` | Create new thread (login required) |

### ForumCategoryCollection Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `find(id)` | `ForumCategory \| None` | Find category by ID |

### Usage

```python
# Get all categories
categories = site.forum.categories
for category in categories:
    print(f"{category.title}: {category.threads_count} threads")

# Get threads in a category
category = categories.find(123)
for thread in category.threads:
    print(f"{thread.title}: {thread.post_count} posts")

# Create new thread (login required)
thread = category.create_thread(
    title="Thread Title",
    description="Thread description",
    source="First post content in Wikidot markup"
)
print(f"Created: {thread.url}")
```

---

## ForumThread

Forum thread operations.

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `site` | `Site` | Parent site |
| `id` | `int` | Thread ID |
| `title` | `str` | Thread title |
| `description` | `str` | Thread description |
| `created_by` | `AbstractUser` | Thread creator |
| `created_at` | `datetime` | Creation date |
| `post_count` | `int` | Number of posts |
| `category` | `ForumCategory \| None` | Parent category |
| `url` | `str` | Thread URL |
| `posts` | `ForumPostCollection` | Posts (lazy loaded) |

### Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `reply(source, title="", parent_post_id=None)` | `ForumThread` | Reply to thread (login required) |

### Static Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `get_from_id(site, thread_id, category=None)` | `ForumThread` | Get thread by ID |

### Usage

```python
# Get thread by ID
thread = site.get_thread(12345)

print(f"{thread.title}: {thread.post_count} posts")
print(f"URL: {thread.url}")

# Get posts
for post in thread.posts:
    print(f"{post.title} by {post.created_by.name}")

# Reply to thread (login required)
thread.reply(source="Reply content")
thread.reply(source="Reply with title", title="Re: Title")
thread.reply(source="Reply to specific post", parent_post_id=67890)
```

---

## ForumPost

Forum post operations.

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `thread` | `ForumThread` | Parent thread |
| `id` | `int` | Post ID |
| `title` | `str` | Post title |
| `text` | `str` | Post content (HTML) |
| `source` | `str` | Post source (Wikidot markup, lazy loaded) |
| `created_by` | `AbstractUser` | Post author |
| `created_at` | `datetime` | Post date |
| `edited_by` | `AbstractUser \| None` | Last editor |
| `edited_at` | `datetime \| None` | Last edit date |
| `parent_id` | `int \| None` | Parent post ID |

### Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `edit(source, title=None)` | `ForumPost` | Edit post (login required) |

### ForumPostCollection Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `find(id)` | `ForumPost \| None` | Find post by ID |

### Usage

```python
for post in thread.posts:
    print(f"{post.title} by {post.created_by.name}")
    print(f"Source: {post.source[:100]}...")

# Edit post (login required)
post = post.edit(source="Updated content", title="Updated title")
```

---

## User

User types and operations.

### Import

```python
from wikidot import Client
from wikidot.module.user import User, AbstractUser
```

### User Types

| Type | Description |
|------|-------------|
| `User` | Regular registered user |
| `DeletedUser` | Deleted user account |
| `AnonymousUser` | Anonymous user (with IP) |
| `GuestUser` | Guest user |
| `WikidotUser` | Wikidot system user |

### AbstractUser Properties

| Property | Type | Description |
|----------|------|-------------|
| `client` | `Client` | Parent client |
| `id` | `int \| None` | User ID |
| `name` | `str \| None` | Display name |
| `unix_name` | `str \| None` | URL-safe name |
| `avatar_url` | `str \| None` | Avatar URL |
| `ip` | `str \| None` | IP address (anonymous only) |

### Getting Users

```python
# Single user
user = client.user.get("username")
user = client.user.get("username", raise_when_not_found=False)  # Returns None if not found

# Multiple users
users = client.user.get_bulk(["user1", "user2", "user3"])
```

### Usage

```python
user = client.user.get("username")

print(f"Name: {user.name}")
print(f"ID: {user.id}")
print(f"Avatar: {user.avatar_url}")

# Type checking
if isinstance(user, User):
    print("Regular user")
elif isinstance(user, DeletedUser):
    print("Deleted account")
elif isinstance(user, AnonymousUser):
    print(f"Anonymous: {user.ip}")

# Bulk fetch
users = client.user.get_bulk(["user1", "user2", "user3"])
for user in users:
    if user:
        print(user.name)
```

---

## SiteMember

Site member operations.

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `site` | `Site` | Parent site |
| `user` | `User` | User object |
| `joined_at` | `datetime \| None` | Join date |

### Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `to_moderator()` | `None` | Promote to moderator (login required) |
| `remove_moderator()` | `None` | Remove moderator status (login required) |
| `to_admin()` | `None` | Promote to admin (login required) |
| `remove_admin()` | `None` | Remove admin status (login required) |

### Usage

```python
# Member list
for member in site.members:
    print(f"{member.user.name}: joined {member.joined_at}")

# Moderator list
for mod in site.moderators:
    print(f"Mod: {mod.user.name}")

# Admin list
for admin in site.admins:
    print(f"Admin: {admin.user.name}")

# Search specific member
member = site.member_lookup("username")

# Change permissions (login required)
member.to_moderator()
member.remove_moderator()
member.to_admin()
member.remove_admin()
```

---

## SiteApplication

Membership application operations (login required).

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `site` | `Site` | Parent site |
| `user` | `User` | Applicant |
| `text` | `str` | Application text |
| `created_at` | `datetime` | Application date |

### Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `accept()` | `None` | Accept application |
| `decline()` | `None` | Decline application |

### Usage

```python
# Process applications (login required)
for application in site.applications:
    print(f"Applicant: {application.user.name}")
    print(f"Text: {application.text}")

    application.accept()   # Accept
    # application.decline()  # Or decline
```

---

## PrivateMessage

Private message operations (login required).

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `client` | `Client` | Parent client |
| `id` | `int` | Message ID |
| `sender` | `AbstractUser` | Sender |
| `recipient` | `AbstractUser` | Recipient |
| `subject` | `str` | Subject line |
| `body` | `str` | Message body |
| `created_at` | `datetime` | Send date |

### Accessor Methods

| Method | Return Type | Description |
|--------|-------------|-------------|
| `client.private_message.inbox` | `PrivateMessageInbox` | Get inbox (property) |
| `client.private_message.sentbox` | `PrivateMessageSentBox` | Get sent box (property) |
| `client.private_message.get_message(id)` | `PrivateMessage` | Get message by ID |
| `client.private_message.get_messages(ids)` | `PrivateMessageCollection` | Get multiple messages |
| `client.private_message.send(recipient, subject, body)` | `None` | Send message |

### Usage

```python
# Send PM
user = client.user.get("target-user")
client.private_message.send(
    recipient=user,
    subject="Hello",
    body="This is a message."
)

# Inbox
for message in client.private_message.inbox:
    print(f"From: {message.sender.name}")
    print(f"Subject: {message.subject}")
    print(f"Date: {message.created_at}")

# Sent box
for message in client.private_message.sentbox:
    print(f"To: {message.recipient.name}")
    print(f"Subject: {message.subject}")

# Get specific message
message = client.private_message.get_message(12345)
print(message.body)
```

---

## Exception Handling

### Exception Hierarchy

```
WikidotException (base)
├── UnexpectedException           # Internal inconsistency or bug
├── SessionCreateException        # Login failed
├── LoginRequiredException        # Operation requires login
├── AjaxModuleConnectorException  # AMC base exception
│   ├── AMCHttpStatusCodeException  # HTTP status error (e.g., 404, 500)
│   ├── WikidotStatusCodeException  # Wikidot API status error
│   └── ResponseDataException       # Response parsing failed
├── NotFoundException             # Resource not found
├── TargetExistsException         # Resource already exists
├── TargetErrorException          # Resource in invalid state
├── ForbiddenException            # Access denied
└── NoElementException            # HTML element not found
```

### Exception Properties

| Exception | Properties | Description |
|-----------|------------|-------------|
| `AMCHttpStatusCodeException` | `status_code: int` | HTTP status code |
| `WikidotStatusCodeException` | `status_code: str` | Wikidot status |

### Usage

```python
from wikidot.common.exceptions import (
    NotFoundException,
    LoginRequiredException,
    SessionCreateException
)

# Handle login failure
try:
    client = wikidot.Client(username="user", password="wrong")
except SessionCreateException:
    print("Login failed")

# Handle missing page
try:
    page = site.page.get("nonexistent")
except NotFoundException:
    print("Page not found")

# Handle login required
try:
    page.edit(source="new content")
except LoginRequiredException:
    print("Please login first")
```

---

## Directory Structure

```
src/wikidot/
├── __init__.py                   # Package entry point
├── common/
│   ├── decorators.py             # @login_required decorator
│   └── exceptions.py             # Exception definitions
├── connector/
│   ├── amc.py                    # AjaxModuleConnector
│   └── amc_config.py             # Configuration
├── module/
│   ├── client.py                 # Client class and accessors
│   ├── site.py                   # Site class and accessors
│   ├── page.py                   # Page, PageCollection, SearchPagesQuery
│   ├── page_source.py            # PageSource
│   ├── page_revision.py          # PageRevision, PageRevisionCollection
│   ├── page_votes.py             # PageVote, PageVoteCollection
│   ├── page_file.py              # PageFile, PageFileCollection
│   ├── forum_category.py         # ForumCategory, ForumCategoryCollection
│   ├── forum_thread.py           # ForumThread, ForumThreadCollection
│   ├── forum_post.py             # ForumPost, ForumPostCollection
│   ├── user.py                   # User hierarchy
│   ├── private_message.py        # PrivateMessage classes
│   ├── site_member.py            # SiteMember
│   └── auth.py                   # Authentication handling
└── util/
    ├── parser/                   # HTML parsers
    ├── quick_module.py           # QuickModule API
    └── string.py                 # String utilities
```

---

## Recipes

### Get Top Rated Pages

```python
with wikidot.Client() as client:
    site = client.site.get("scp-jp")
    pages = site.pages.search(
        category="scp",
        order="rating desc",
        limit=10
    )
    for page in pages:
        print(f"{page.fullname}: +{page.rating}")
```

### Find Pages by Tag

```python
with wikidot.Client() as client:
    site = client.site.get("scp-jp")
    pages = site.pages.search(
        tags=["safe", "+scp", "-explained"],
        rating=">100"
    )
    for page in pages:
        print(f"{page.fullname}: {page.tags}")
```

### Get Page History

```python
with wikidot.Client() as client:
    site = client.site.get("scp-jp")
    page = site.page.get("scp-173")

    for rev in page.revisions:
        print(f"Rev {rev.rev_no}: {rev.comment} by {rev.created_by.name}")
```

### Batch Page Operations

```python
with wikidot.Client() as client:
    site = client.site.get("scp-jp")
    pages = site.pages.search(category="scp", limit=100)

    # Bulk fetch all data efficiently
    pages.get_page_ids()
    pages.get_page_sources()
    pages.get_page_votes()

    for page in pages:
        print(f"{page.fullname}: {len(page.source.wiki_text)} chars")
```

### Discussion Thread Access

```python
with wikidot.Client() as client:
    site = client.site.get("scp-jp")
    page = site.page.get("scp-173")

    if page.discussion:
        thread = page.discussion
        print(f"Comments: {thread.post_count}")

        for post in thread.posts:
            print(f"{post.created_by.name}: {post.title}")
```

### Create and Edit Page

```python
with wikidot.Client(username="user", password="pass") as client:
    site = client.site.get("sandbox")

    # Create new page
    page = site.page.create(
        fullname="test:my-page",
        title="Test Page",
        source="[[=]]\n++ Welcome\n[[/=]]",
        comment="Initial creation"
    )

    # Edit page
    page = page.edit(
        source="[[=]]\n++ Updated Content\n[[/=]]",
        comment="Updated"
    )

    # Add tags
    page.tags.append("test")
    page.tags.append("wip")
    page = page.commit_tags()

    # Delete page
    page.destroy()
```

### Forum Operations

```python
with wikidot.Client(username="user", password="pass") as client:
    site = client.site.get("scp-jp")

    # Get forum categories
    categories = site.forum.categories
    category = categories.find(123)

    if category:
        # Create new thread
        thread = category.create_thread(
            title="New Discussion",
            description="Description of the topic",
            source="First post content here."
        )
        print(f"Created: {thread.url}")

        # Reply to thread
        thread.reply(
            source="Great topic!",
            title="Re: New Discussion"
        )
```

### Process Membership Applications

```python
with wikidot.Client(username="admin", password="pass") as client:
    site = client.site.get("my-site")

    for application in site.applications:
        print(f"Applicant: {application.user.name}")
        print(f"Message: {application.text}")

        # Accept or decline
        application.accept()
```

### Monitor Recent Changes

```python
with wikidot.Client() as client:
    site = client.site.get("scp-jp")

    changes = site.get_recent_changes(limit=50)
    for change in changes:
        flags = "".join(change.flags)
        print(f"[{flags}] {change.page_fullname}")
        print(f"  Rev {change.revision_no} by {change.changed_by.name}")
        print(f"  {change.changed_at}: {change.comment or '(no comment)'}")
```

---

## Reference Links

- Official Documentation: https://ukwhatn.github.io/wikidot.py/
- Repository: https://github.com/ukwhatn/wikidot.py
- PyPI: https://pypi.org/project/wikidot/
- TypeScript version: https://github.com/ukwhatn/wikidot-ts
