From a1ada24a2838ffece9060628babbe6453ad5b221 Mon Sep 17 00:00:00 2001 From: Steve Harris Date: Mon, 12 Jan 2026 11:12:52 -0600 Subject: [PATCH] Python3 Update & Example Modified `wt_session.py` to support `appId`, and tweaked the code to support unauthenticated requests. Added `example.py` for testing. Also fixed a quick typo in the `README.md` --- examples/python3/README.md | 2 +- examples/python3/example.py | 93 ++++++++++++++++++++++++++++++++++ examples/python3/wt_session.py | 39 +++++++++++--- 3 files changed, 127 insertions(+), 7 deletions(-) create mode 100644 examples/python3/example.py diff --git a/examples/python3/README.md b/examples/python3/README.md index 18e180c..101aa86 100644 --- a/examples/python3/README.md +++ b/examples/python3/README.md @@ -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. diff --git a/examples/python3/example.py b/examples/python3/example.py new file mode 100644 index 0000000..1479282 --- /dev/null +++ b/examples/python3/example.py @@ -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 +""" + +from getpass import getpass +from pprint import pprint +import sys +import time + +from wt_session import WTSession + + +APP_ID = "Python3-SessionExample" +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() diff --git a/examples/python3/wt_session.py b/examples/python3/wt_session.py index 848b3e6..5f0de6e 100644 --- a/examples/python3/wt_session.py +++ b/examples/python3/wt_session.py @@ -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.""" @@ -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. @@ -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( @@ -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" @@ -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, @@ -226,7 +245,9 @@ def authenticate(self, email: str, password: str) -> bool: # 'wikitree_wtb_UserID': '<>' # } 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." ) @@ -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"), ) @@ -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