Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion examples/python3/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Python3 Example

THis python3 example class provides a complete, single-class solution to both authenticated
This python3 example class provides a complete, single-class solution to both authenticated
and un-authenticated sessions.

It provides convenience methods for all currently documented API calls.
Expand Down
93 changes: 93 additions & 0 deletions examples/python3/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
simple_example.py

A simple example showing:
1) Unauthenticated call to getPerson('Windsor-1')
2) Optional authentication, then call getPerson('Windsor-1') again

Requirements:
- wt_session.py in the same folder (your updated version that supports unauthenticated use + appId)
- requests installed
"""

Choose a reason for hiding this comment

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

import os

from getpass import getpass
from pprint import pprint
import sys
import time

from wt_session import WTSession


APP_ID = "Python3-SessionExample"
KEY = "Windsor-1"

Choose a reason for hiding this comment

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

Suggested improvement.

APP_ID = os.environ.get("APP_ID", "Python3-SessionExample")
KEY = os.environ.get("KEY", "Windsor-1")



def print_get_person_summary(label: str, result):
print(f"\n=== {label} ===")
print("type:", type(result))

# The examples you tested return a list with an item containing 'status' and 'person'
if not isinstance(result, list) or not result:
print("Unexpected response payload:")
pprint(result)
return

item0 = result[0]
status = item0.get("status")
print("status:", status)

person = item0.get("person") or {}
print("Name:", person.get("Name"))
print("RealName:", person.get("RealName"))
print("Privacy:", person.get("Privacy"))


def get_person_with_retry(session: WTSession, key: str, tries: int = 3, sleep_seconds: int = 2):
"""
If the API responds with 'Limit exceeded.', pause briefly and retry.
This is only for the demo script to reduce confusion when testing.
"""
last = None
for attempt in range(1, tries + 1):
last = session.get_person(key)
if isinstance(last, list) and last and last[0].get("status") == "Limit exceeded.":
if attempt < tries:
time.sleep(sleep_seconds)
continue
break
return last


def main():
# Create the WikiTree session object (unauthenticated)
wt_session = WTSession(app_id=APP_ID)

# 1) Unauthenticated call
unauth_result = get_person_with_retry(wt_session, KEY)
print_get_person_summary("Unauthenticated get_person('Windsor-1')", unauth_result)

# 2) Optional authentication, then call again
print("\nAuthenticate? (y/N): ", end="")
ans = input().strip().lower()
if ans not in ("y", "yes"):
print("\nDone (skipped authentication).")
return

success = False
while not success:
email = input("Email (or quit): ").strip()
if email.lower() == "quit":
sys.exit(0)
password = getpass("Password: ")
success = wt_session.authenticate(email=email, password=password)
if not success:
print("Login failed, try again.\n")

print("\nLogged in as:", wt_session.user_name, "(UserID:", wt_session.user_id, ")")

auth_result = get_person_with_retry(wt_session, KEY)
print_get_person_summary("Authenticated get_person('Windsor-1')", auth_result)


if __name__ == "__main__":
main()
39 changes: 33 additions & 6 deletions examples/python3/wt_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,22 @@ class WTSession:
Provides convenience functions for each of the API calls.
"""

def __init__(self) -> None:
def __init__(self, app_id: str) -> None:
"""Just default some values."""
self._email = ""
self._authenticated = False
self._session = None

# CHANGE: Create the session immediately so unauthenticated calls work.
self._session = requests.Session()

self._authcode = None
self._wt_cookie = None
self._user_name = ""
self._user_id = ""

# CHANGE: Support appId (helps avoid "Limit exceeded." for no-appId traffic)
self._app_id = app_id

@property
def user_name(self) -> str:
"""Return the user name of the authenticated user."""
Expand Down Expand Up @@ -136,6 +142,9 @@ def authenticate(self, email: str, password: str) -> bool:

# We use a Session to hold the state (via cookie jar) for the API queries
LOGGER.debug("Starting Session")

# NOTE: We already created a Session in __init__.
# Keeping this line is fine, it simply starts a fresh session for auth.
self._session = requests.Session()

# Step 1 - POST the clientLogin action with our member credentials.
Expand All @@ -156,6 +165,9 @@ def authenticate(self, email: str, password: str) -> bool:
"doLogin": 1,
"wpEmail": email,
"wpPassword": password,

# CHANGE: include appId during login too
"appId": self._app_id,
}

response = self._session.post(
Expand Down Expand Up @@ -184,7 +196,8 @@ def authenticate(self, email: str, password: str) -> bool:
return self._authenticated

# Now looking for authcode
matches = re.search("authcode=(.*)", location)
# CHANGE: safer regex so we do not accidentally capture extra query params
matches = re.search(r"authcode=([^&]+)", location)
if matches is None:
LOGGER.error(
"Authentication failed - clientLogin POST did not return authcode"
Expand All @@ -201,7 +214,13 @@ def authenticate(self, email: str, password: str) -> bool:
# Step 2 - POST back the authcode we got. This completes the login/session setup
# at api.wikitree.com. Since we use the same Session for the post, the cookies are all
# saved. A success here is a 200 and we'll have WikiTree session cookies.
post_data = {"action": "clientLogin", "authcode": self._authcode}
post_data = {
"action": "clientLogin",
"authcode": self._authcode,

# CHANGE: include appId on this step too
"appId": self._app_id,
}
response = self._session.post(
API_URL,
data=post_data,
Expand All @@ -226,7 +245,9 @@ def authenticate(self, email: str, password: str) -> bool:
# 'wikitree_wtb_UserID': '<<WikiTree user_id>>'
# }
self._wt_cookie = self._session.cookies.get_dict()
if self._wt_cookie is None:

# CHANGE: get_dict() returns {} when empty, not None
if not self._wt_cookie:
LOGGER.error(
"Authentication failed - clientLogin(authcode) returned no Cookies."
)
Expand Down Expand Up @@ -276,9 +297,14 @@ def _do_post(self, post_data: dict, need_auth: bool = False) -> dict:
if need_auth and not self._authenticated:
return data

# CHANGE: Always include appId to reduce rate limiting for no-appId traffic.
# Copy the dict so we do not mutate the caller's object.
post_payload = dict(post_data)
post_payload.setdefault("appId", self._app_id)

response = self._session.post(
url=API_URL,
data=post_data,
data=post_payload,
# auth=("wikitree", "wikitree"),
)

Expand Down Expand Up @@ -636,6 +662,7 @@ def main():
"""

# Create the WikiTree session object.
# CHANGE: app_id is optional. If you want to set it explicitly, pass app_id="YourAppName"
wt_session = WTSession()

# Loop until we have a successful authentication
Expand Down