From 9351b3aef5b5261471021ff3c652567aed684dc5 Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:42:34 +0800 Subject: [PATCH 01/10] add get_x_oauth_url to UserService --- wyseos/mate/constants.py | 1 + wyseos/mate/models.py | 6 ++++++ wyseos/mate/services/user.py | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/wyseos/mate/constants.py b/wyseos/mate/constants.py index 39f2181..2c9e301 100644 --- a/wyseos/mate/constants.py +++ b/wyseos/mate/constants.py @@ -27,6 +27,7 @@ # API Endpoints # User endpoints ENDPOINT_API_KEY_LIST = "/user/apikey/lists" +ENDPOINT_AUTH_URL = "/auth/url" # Team endpoints ENDPOINT_TEAM_LIST = "/team/lists" diff --git a/wyseos/mate/models.py b/wyseos/mate/models.py index d519087..bb42eee 100644 --- a/wyseos/mate/models.py +++ b/wyseos/mate/models.py @@ -312,6 +312,12 @@ class CreateAPIKeyResponse(BaseModel): ) # Full key value returned only on creation +class OAuthURLResponse(BaseModel): + """Response for retrieving an OAuth authorization URL.""" + + auth_url: str + + class ListBrowsersResponse(BaseModel): """Response for listing browsers.""" diff --git a/wyseos/mate/services/user.py b/wyseos/mate/services/user.py index 57fe740..5deb283 100644 --- a/wyseos/mate/services/user.py +++ b/wyseos/mate/services/user.py @@ -6,10 +6,14 @@ from ..constants import ( ENDPOINT_API_KEY_LIST, + ENDPOINT_AUTH_URL, ) +from ..extension_host import resolve_extension_webapp_host from ..models import ( APIKey, + APIResponse, ListOptions, + OAuthURLResponse, PaginatedResponse, ) @@ -66,3 +70,24 @@ def list_api_keys( result_model=PaginatedResponse[APIKey], params=params, ) + + def get_x_oauth_url(self) -> OAuthURLResponse: + """ + Get an OAuth authorization URL for Twitter login. + + Returns: + OAuthURLResponse: Response containing the OAuth authorization URL + """ + params = { + "type": "login", + "platform": "twitter", + "credential_type": "api_key", + "redirect_url": f"{resolve_extension_webapp_host()}/oauth/twitter", + } + + resp = self.client.get( + endpoint=ENDPOINT_AUTH_URL, + result_model=APIResponse[OAuthURLResponse], + params=params, + ) + return resp.data From ec6eabdacfe8903d5486989564ef81dd2b4c5933 Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Thu, 23 Apr 2026 18:11:54 +0800 Subject: [PATCH 02/10] add connectors apis --- wyseos/mate/constants.py | 5 ++++ wyseos/mate/models.py | 27 ++++++++++++++++- wyseos/mate/services/user.py | 56 ++++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/wyseos/mate/constants.py b/wyseos/mate/constants.py index 2c9e301..fba7056 100644 --- a/wyseos/mate/constants.py +++ b/wyseos/mate/constants.py @@ -29,6 +29,11 @@ ENDPOINT_API_KEY_LIST = "/user/apikey/lists" ENDPOINT_AUTH_URL = "/auth/url" +# X (Twitter) connector endpoints +ENDPOINT_X_CONNECTOR_ACCOUNTS = "/connectors/v1/x/accounts" +ENDPOINT_X_CONNECTOR_AUTHORIZE = "/connectors/v1/x/accounts/authorize" +ENDPOINT_X_CONNECTOR_DELETE = "/connectors/v1/x/accounts/{credential_id}" + # Team endpoints ENDPOINT_TEAM_LIST = "/team/lists" ENDPOINT_TEAM_INFO = "/team/info/{team_id}" diff --git a/wyseos/mate/models.py b/wyseos/mate/models.py index bb42eee..824e041 100644 --- a/wyseos/mate/models.py +++ b/wyseos/mate/models.py @@ -3,7 +3,7 @@ """ from datetime import datetime -from typing import Annotated, Any, Dict, Generic, List, Optional, TypeVar +from typing import Annotated, Any, Dict, Generic, List, Literal, Optional, TypeVar from pydantic import BaseModel, Field @@ -318,6 +318,31 @@ class OAuthURLResponse(BaseModel): auth_url: str +class XConnectorAccount(BaseModel): + """X (Twitter) connector account information.""" + + connector_id: str + platform: Literal["x"] + external_user_id: str + external_username: str + expires_at: str + updated_at: str + status: Literal["connected", "expired"] + + +class ListXAccountsResponse(BaseModel): + """Response for listing X connector accounts.""" + + items: List[XConnectorAccount] + + +class AuthorizeXAccountRequest(BaseModel): + """Request body for authorizing an X connector account.""" + + redirect_url: str + target_credential_id: Optional[str] = None + + class ListBrowsersResponse(BaseModel): """Response for listing browsers.""" diff --git a/wyseos/mate/services/user.py b/wyseos/mate/services/user.py index 5deb283..193aeca 100644 --- a/wyseos/mate/services/user.py +++ b/wyseos/mate/services/user.py @@ -7,12 +7,17 @@ from ..constants import ( ENDPOINT_API_KEY_LIST, ENDPOINT_AUTH_URL, + ENDPOINT_X_CONNECTOR_ACCOUNTS, + ENDPOINT_X_CONNECTOR_AUTHORIZE, + ENDPOINT_X_CONNECTOR_DELETE, ) from ..extension_host import resolve_extension_webapp_host from ..models import ( APIKey, APIResponse, + AuthorizeXAccountRequest, ListOptions, + ListXAccountsResponse, OAuthURLResponse, PaginatedResponse, ) @@ -91,3 +96,54 @@ def get_x_oauth_url(self) -> OAuthURLResponse: params=params, ) return resp.data + + def list_x_accounts(self) -> ListXAccountsResponse: + """ + List the current user's connected X (Twitter) accounts. + + Returns: + ListXAccountsResponse: Response containing the list of X connector accounts + """ + resp = self.client.get( + endpoint=ENDPOINT_X_CONNECTOR_ACCOUNTS, + result_model=APIResponse[ListXAccountsResponse], + ) + return resp.data + + def authorize_x_account( + self, target_credential_id: Optional[str] = None + ) -> OAuthURLResponse: + """ + Start the OAuth authorization flow to bind an X (Twitter) account. + + Args: + target_credential_id: Optional ID of the credential slot to bind the X + account to. When omitted, the backend creates a new credential. + + Returns: + OAuthURLResponse: Response containing the authorization URL + """ + payload = AuthorizeXAccountRequest( + target_credential_id=target_credential_id, + redirect_url=( + f"{resolve_extension_webapp_host()}" + "/settings/integrations/x/callback?scene=connector_x_bind" + ), + ) + + resp = self.client.post( + endpoint=ENDPOINT_X_CONNECTOR_AUTHORIZE, + body=payload.model_dump(exclude_none=True), + result_model=APIResponse[OAuthURLResponse], + ) + return resp.data + + def delete_x_account(self, credential_id: str) -> None: + """ + Delete a connected X (Twitter) account. + + Args: + credential_id: ID of the X connector account to delete + """ + endpoint = ENDPOINT_X_CONNECTOR_DELETE.format(credential_id=credential_id) + self.client.delete(endpoint) From 3d83dc75c68917c9f785568da2bcd45ed7e68498 Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:23:01 +0800 Subject: [PATCH 03/10] add skip auth param to request --- wyseos/mate/client.py | 18 ++++++++++++------ wyseos/mate/services/user.py | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/wyseos/mate/client.py b/wyseos/mate/client.py index 7c0c488..62f3bf7 100644 --- a/wyseos/mate/client.py +++ b/wyseos/mate/client.py @@ -58,7 +58,11 @@ def __init__(self, options: Optional[ClientOptions] = None): self.product = ProductService(self) def _do_request( - self, method: str, endpoint: str, body: Optional[Dict] = None + self, + method: str, + endpoint: str, + body: Optional[Dict] = None, + skip_auth: bool = False, ) -> requests.Response: url = urljoin(self.base_url, endpoint) @@ -68,10 +72,11 @@ def _do_request( HEADER_ACCEPT: CONTENT_TYPE_JSON, } - if self.jwt_token: - headers[HEADER_AUTHORIZATION] = self.jwt_token - elif self.api_key: - headers[HEADER_API_KEY] = self.api_key + if not skip_auth: + if self.jwt_token: + headers[HEADER_AUTHORIZATION] = self.jwt_token + elif self.api_key: + headers[HEADER_API_KEY] = self.api_key try: response = self.http_client.request( @@ -97,10 +102,11 @@ def get( endpoint: str, result_model: Type[T], params: Optional[Dict[str, str]] = None, + skip_auth: bool = False, ) -> T: if params: endpoint = self._build_url(endpoint, params) - response = self._do_request("GET", endpoint) + response = self._do_request("GET", endpoint, skip_auth=skip_auth) if result_model is dict: return response.json() return result_model.model_validate(response.json()) diff --git a/wyseos/mate/services/user.py b/wyseos/mate/services/user.py index 193aeca..ebd313b 100644 --- a/wyseos/mate/services/user.py +++ b/wyseos/mate/services/user.py @@ -94,6 +94,7 @@ def get_x_oauth_url(self) -> OAuthURLResponse: endpoint=ENDPOINT_AUTH_URL, result_model=APIResponse[OAuthURLResponse], params=params, + skip_auth=True, ) return resp.data From ab538201e2a42c7256c690dc1ae41fc9ec56cbed Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:41:43 +0800 Subject: [PATCH 04/10] credential config are not required --- wyseos/mate/config.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wyseos/mate/config.py b/wyseos/mate/config.py index 917fb63..d683abe 100644 --- a/wyseos/mate/config.py +++ b/wyseos/mate/config.py @@ -94,6 +94,14 @@ def load_config(config_path: Optional[Union[str, Path]] = None) -> ClientOptions if "mate" in config_data and isinstance(config_data["mate"], dict): config_data = config_data["mate"] + # Treat blank credential placeholders (e.g. `api_key: ""` in a template + # YAML) as "not set" — Pydantic's min_length=1 would otherwise reject + # them and force users to remove the keys entirely. + for key in ("api_key", "jwt_token"): + value = config_data.get(key) + if isinstance(value, str) and value.strip() == "": + config_data.pop(key) + return ClientOptions(**config_data) except IOError as e: From 8cf1f22b6025e4992589d158872ce5101a00ae29 Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Fri, 24 Apr 2026 11:58:17 +0800 Subject: [PATCH 05/10] add examples --- examples/auth/connectors_example.py | 117 ++++++++++++++++++++++++++++ examples/auth/mate.yaml.example | 9 +++ examples/auth/oauth_url_example.py | 48 ++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 examples/auth/connectors_example.py create mode 100644 examples/auth/mate.yaml.example create mode 100644 examples/auth/oauth_url_example.py diff --git a/examples/auth/connectors_example.py b/examples/auth/connectors_example.py new file mode 100644 index 0000000..c0cd984 --- /dev/null +++ b/examples/auth/connectors_example.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +Example: manage X (Twitter) connector accounts. + +Demonstrates the three X-connector endpoints on UserService: + - list_x_accounts() -> GET /connectors/v1/x/accounts + - authorize_x_account() -> POST /connectors/v1/x/accounts/authorize + - delete_x_account(id) -> DELETE /connectors/v1/x/accounts/{id} + +Requires a valid api_key (or jwt_token) in mate.yaml. +""" + +import os +from typing import Optional + +from wyseos.mate import Client +from wyseos.mate.config import load_config +from wyseos.mate.errors import APIError + + +def create_client() -> Optional[Client]: + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(script_dir, "mate.yaml") + try: + print(f"Loading config: {config_path}") + return Client(load_config(config_path)) + except Exception as exc: + print(f"Failed to load config: {exc}") + print("Please configure mate.yaml with a valid api_key or jwt_token.") + return None + + +def print_menu() -> None: + print("\n=== X Connector Menu ===") + print("1) List connected X accounts") + print("2) Authorize a new / existing X account") + print("3) Delete a connected X account") + print("q) Quit") + + +def do_list(client: Client) -> None: + try: + result = client.user.list_x_accounts() + except APIError as exc: + print(f"List failed: {exc}") + return + + if not result.items: + print("No X accounts are currently connected.") + return + + print(f"Connected X accounts ({len(result.items)}):") + for idx, account in enumerate(result.items, start=1): + print( + f" {idx}. connector_id={account.connector_id} " + f"username=@{account.external_username} " + f"status={account.status} " + f"expires_at={account.expires_at}" + ) + + +def do_authorize(client: Client) -> None: + target = input( + "Target credential_id (leave empty to create a new connector): " + ).strip() + try: + resp = client.user.authorize_x_account( + target_credential_id=target or None, + ) + except APIError as exc: + print(f"Authorize failed: {exc}") + return + + print("Open this URL in a browser to complete the binding:") + print(resp.auth_url) + + +def do_delete(client: Client) -> None: + credential_id = input("credential_id to delete: ").strip() + if not credential_id: + print("credential_id is required.") + return + + try: + client.user.delete_x_account(credential_id) + except APIError as exc: + print(f"Delete failed: {exc}") + return + + print(f"Deleted X connector {credential_id}.") + + +def main() -> None: + client = create_client() + if not client: + return + + actions = { + "1": do_list, + "2": do_authorize, + "3": do_delete, + } + + while True: + print_menu() + choice = input("Choose an action: ").strip().lower() + if choice == "q": + break + action = actions.get(choice) + if action is None: + print("Unknown option.") + continue + action(client) + + +if __name__ == "__main__": + main() diff --git a/examples/auth/mate.yaml.example b/examples/auth/mate.yaml.example new file mode 100644 index 0000000..6dbfd08 --- /dev/null +++ b/examples/auth/mate.yaml.example @@ -0,0 +1,9 @@ +mate: + # API base URL (override only when targeting a non-default environment). + base_url: "https://api.wyseos.com" + timeout: 30 + + # Required for connectors_example.py (list / authorize / delete X accounts). + # Not required for oauth_url_example.py — that endpoint is public. + # Uncomment and fill in to use connector endpoints: + # api_key: "your-api-key" diff --git a/examples/auth/oauth_url_example.py b/examples/auth/oauth_url_example.py new file mode 100644 index 0000000..d9f668a --- /dev/null +++ b/examples/auth/oauth_url_example.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +""" +Example: fetch the X (Twitter) OAuth login URL. + +This endpoint is called *before* the user has any credentials, so it does +not require an api_key or jwt_token — the client is constructed with the +default options. +""" + +import os + +from wyseos.mate import Client, ClientOptions +from wyseos.mate.config import load_config +from wyseos.mate.errors import APIError + + +def create_client() -> Client: + """Return a Client. Credentials are optional for this endpoint.""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(script_dir, "mate.yaml") + + if os.path.exists(config_path): + print(f"Loading config: {config_path}") + return Client(load_config(config_path)) + + print("No mate.yaml found, using default base_url without credentials.") + return Client(ClientOptions()) + + +def main() -> None: + client = create_client() + + try: + resp = client.user.get_x_oauth_url() + except APIError as exc: + print(f"Request failed: {exc}") + return + + print("X OAuth authorization URL:") + print(resp.auth_url) + print( + "\nOpen the URL in a browser to complete the login flow. The backend " + "will redirect to the configured webapp host." + ) + + +if __name__ == "__main__": + main() From cd817ed45cfd0e0882e6b3e114b84654f46c99e9 Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:41:54 +0800 Subject: [PATCH 06/10] add email verify and combine the example --- examples/auth/auth_example.py | 101 +++++++++++++++++++++++++++++ examples/auth/mate.yaml.example | 2 +- examples/auth/oauth_url_example.py | 48 -------------- wyseos/mate/client.py | 3 +- wyseos/mate/constants.py | 1 + wyseos/mate/models.py | 15 +++++ wyseos/mate/services/user.py | 27 ++++++++ 7 files changed, 147 insertions(+), 50 deletions(-) create mode 100644 examples/auth/auth_example.py delete mode 100644 examples/auth/oauth_url_example.py diff --git a/examples/auth/auth_example.py b/examples/auth/auth_example.py new file mode 100644 index 0000000..3e687f3 --- /dev/null +++ b/examples/auth/auth_example.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +""" +Interactive example: pick how to start authentication. + +Both flows are unauthenticated (no api_key / jwt_token required): + + - Email: sends a magic sign-in link via ``UserService.start_email_verification``. + - X (Twitter): returns the OAuth URL via ``UserService.get_x_oauth_url``. + +Copy ``mate.yaml.example`` to ``mate.yaml`` if you want a custom ``base_url``; +credentials in that file are not needed for this script. +""" + +from __future__ import annotations + +import os + +from wyseos.mate import Client, ClientOptions +from wyseos.mate.config import load_config +from wyseos.mate.errors import APIError + + +def create_client() -> Client: + """Client without credentials; optional mate.yaml for base_url.""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + config_path = os.path.join(script_dir, "mate.yaml") + + if os.path.exists(config_path): + print(f"Loading config: {config_path}") + return Client(load_config(config_path)) + + print("No mate.yaml found, using default base_url without credentials.") + return Client(ClientOptions()) + + +def run_email_flow(client: Client) -> None: + email = input("Email address: ").strip() + if not email: + print("Email is required.") + return + + invite = input("Invite code (optional, press Enter to skip): ").strip() + invite_code = invite or None + + try: + resp = client.user.start_email_verification( + email=email, + invite_code=invite_code, + ) + except APIError as exc: + print(f"Request failed: {exc}") + return + + print("\nSign-in link request accepted. Response:") + print(f" sign_type: {resp.sign_type}") + print(f" pre_auth_id: {resp.pre_auth_id}") + print( + "\nThe user should open the link in the email to finish sign-in." + ) + + +def run_twitter_oauth_flow(client: Client) -> None: + try: + resp = client.user.get_x_oauth_url() + except APIError as exc: + print(f"Request failed: {exc}") + return + + print("\nX (Twitter) OAuth authorization URL:") + print(resp.auth_url) + print( + "\nOpen the URL in a browser to finish login. The backend redirects to " + "the configured webapp host." + ) + + +def print_menu() -> None: + print("\n=== Choose auth method ===") + print("1) Email — send verify link for sign-in / sign-up") + print("2) X (Twitter) — get OAuth login URL") + print("q) Quit") + + +def main() -> None: + client = create_client() + + while True: + print_menu() + choice = input("Choice: ").strip().lower() + if choice == "q": + break + if choice == "1": + run_email_flow(client) + elif choice == "2": + run_twitter_oauth_flow(client) + else: + print("Unknown option.") + + +if __name__ == "__main__": + main() diff --git a/examples/auth/mate.yaml.example b/examples/auth/mate.yaml.example index 6dbfd08..8cebc9f 100644 --- a/examples/auth/mate.yaml.example +++ b/examples/auth/mate.yaml.example @@ -4,6 +4,6 @@ mate: timeout: 30 # Required for connectors_example.py (list / authorize / delete X accounts). - # Not required for oauth_url_example.py — that endpoint is public. + # Not required for auth_example.py — those flows are public. # Uncomment and fill in to use connector endpoints: # api_key: "your-api-key" diff --git a/examples/auth/oauth_url_example.py b/examples/auth/oauth_url_example.py deleted file mode 100644 index d9f668a..0000000 --- a/examples/auth/oauth_url_example.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -""" -Example: fetch the X (Twitter) OAuth login URL. - -This endpoint is called *before* the user has any credentials, so it does -not require an api_key or jwt_token — the client is constructed with the -default options. -""" - -import os - -from wyseos.mate import Client, ClientOptions -from wyseos.mate.config import load_config -from wyseos.mate.errors import APIError - - -def create_client() -> Client: - """Return a Client. Credentials are optional for this endpoint.""" - script_dir = os.path.dirname(os.path.abspath(__file__)) - config_path = os.path.join(script_dir, "mate.yaml") - - if os.path.exists(config_path): - print(f"Loading config: {config_path}") - return Client(load_config(config_path)) - - print("No mate.yaml found, using default base_url without credentials.") - return Client(ClientOptions()) - - -def main() -> None: - client = create_client() - - try: - resp = client.user.get_x_oauth_url() - except APIError as exc: - print(f"Request failed: {exc}") - return - - print("X OAuth authorization URL:") - print(resp.auth_url) - print( - "\nOpen the URL in a browser to complete the login flow. The backend " - "will redirect to the configured webapp host." - ) - - -if __name__ == "__main__": - main() diff --git a/wyseos/mate/client.py b/wyseos/mate/client.py index 62f3bf7..8f9579a 100644 --- a/wyseos/mate/client.py +++ b/wyseos/mate/client.py @@ -139,8 +139,9 @@ def post( endpoint: str, body: Optional[Dict] = None, result_model: Optional[Type[T]] = None, + skip_auth: bool = False, ) -> Optional[T]: - response = self._do_request("POST", endpoint, body) + response = self._do_request("POST", endpoint, body, skip_auth=skip_auth) if result_model and response.content: if result_model is dict: return response.json() diff --git a/wyseos/mate/constants.py b/wyseos/mate/constants.py index fba7056..537a55f 100644 --- a/wyseos/mate/constants.py +++ b/wyseos/mate/constants.py @@ -28,6 +28,7 @@ # User endpoints ENDPOINT_API_KEY_LIST = "/user/apikey/lists" ENDPOINT_AUTH_URL = "/auth/url" +ENDPOINT_AUTH_EMAIL_LINK_SIGNINUP = "/auth/otp/signinup" # X (Twitter) connector endpoints ENDPOINT_X_CONNECTOR_ACCOUNTS = "/connectors/v1/x/accounts" diff --git a/wyseos/mate/models.py b/wyseos/mate/models.py index 824e041..1fb9456 100644 --- a/wyseos/mate/models.py +++ b/wyseos/mate/models.py @@ -343,6 +343,21 @@ class AuthorizeXAccountRequest(BaseModel): target_credential_id: Optional[str] = None +class EmailLinkVerifyRequest(BaseModel): + """Request body for starting email magic-link sign in / sign up.""" + + email: str + invite_code: Optional[str] = None + credential_type: Literal["api_key"] = "api_key" + + +class EmailLinkVerifyResponse(BaseModel): + """Response after requesting a sign-in / sign-up link sent to email.""" + + sign_type: str + pre_auth_id: str + + class ListBrowsersResponse(BaseModel): """Response for listing browsers.""" diff --git a/wyseos/mate/services/user.py b/wyseos/mate/services/user.py index ebd313b..6d44e24 100644 --- a/wyseos/mate/services/user.py +++ b/wyseos/mate/services/user.py @@ -6,6 +6,7 @@ from ..constants import ( ENDPOINT_API_KEY_LIST, + ENDPOINT_AUTH_EMAIL_LINK_SIGNINUP, ENDPOINT_AUTH_URL, ENDPOINT_X_CONNECTOR_ACCOUNTS, ENDPOINT_X_CONNECTOR_AUTHORIZE, @@ -19,6 +20,8 @@ ListOptions, ListXAccountsResponse, OAuthURLResponse, + EmailLinkVerifyRequest, + EmailLinkVerifyResponse, PaginatedResponse, ) @@ -76,6 +79,30 @@ def list_api_keys( params=params, ) + def start_email_verification( + self, email: str, invite_code: Optional[str] = None + ) -> EmailLinkVerifyResponse: + """ + Start email magic-link sign in / sign up (link sent to the given address). + + Args: + email: The email address to send the sign-in link to + invite_code: Optional invite code + + Returns: + EmailLinkVerifyResponse: Contains sign_type and pre_auth_id for completing + verification after the user follows the link. + """ + payload = EmailLinkVerifyRequest(email=email, invite_code=invite_code) + + resp = self.client.post( + endpoint=ENDPOINT_AUTH_EMAIL_LINK_SIGNINUP, + body=payload.model_dump(exclude_none=True), + result_model=APIResponse[EmailLinkVerifyResponse], + skip_auth=True, + ) + return resp.data + def get_x_oauth_url(self) -> OAuthURLResponse: """ Get an OAuth authorization URL for Twitter login. From 3f3ac397f628a283b0e98c5f10f8c7ffaec97ec1 Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:57:58 +0800 Subject: [PATCH 07/10] change credential_id to connector_id --- examples/auth/connectors_example.py | 12 ++++++------ wyseos/mate/constants.py | 2 +- wyseos/mate/services/user.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/auth/connectors_example.py b/examples/auth/connectors_example.py index c0cd984..f771723 100644 --- a/examples/auth/connectors_example.py +++ b/examples/auth/connectors_example.py @@ -61,7 +61,7 @@ def do_list(client: Client) -> None: def do_authorize(client: Client) -> None: target = input( - "Target credential_id (leave empty to create a new connector): " + "Target connector_id (leave empty to create a new connector): " ).strip() try: resp = client.user.authorize_x_account( @@ -76,18 +76,18 @@ def do_authorize(client: Client) -> None: def do_delete(client: Client) -> None: - credential_id = input("credential_id to delete: ").strip() - if not credential_id: - print("credential_id is required.") + connector_id = input("connector_id to delete: ").strip() + if not connector_id: + print("connector_id is required.") return try: - client.user.delete_x_account(credential_id) + client.user.delete_x_account(connector_id) except APIError as exc: print(f"Delete failed: {exc}") return - print(f"Deleted X connector {credential_id}.") + print(f"Deleted X connector {connector_id}.") def main() -> None: diff --git a/wyseos/mate/constants.py b/wyseos/mate/constants.py index 537a55f..85e4c3a 100644 --- a/wyseos/mate/constants.py +++ b/wyseos/mate/constants.py @@ -33,7 +33,7 @@ # X (Twitter) connector endpoints ENDPOINT_X_CONNECTOR_ACCOUNTS = "/connectors/v1/x/accounts" ENDPOINT_X_CONNECTOR_AUTHORIZE = "/connectors/v1/x/accounts/authorize" -ENDPOINT_X_CONNECTOR_DELETE = "/connectors/v1/x/accounts/{credential_id}" +ENDPOINT_X_CONNECTOR_DELETE = "/connectors/v1/x/accounts/{connector_id}" # Team endpoints ENDPOINT_TEAM_LIST = "/team/lists" diff --git a/wyseos/mate/services/user.py b/wyseos/mate/services/user.py index 6d44e24..97418d6 100644 --- a/wyseos/mate/services/user.py +++ b/wyseos/mate/services/user.py @@ -166,12 +166,12 @@ def authorize_x_account( ) return resp.data - def delete_x_account(self, credential_id: str) -> None: + def delete_x_account(self, connector_id: str) -> None: """ Delete a connected X (Twitter) account. Args: - credential_id: ID of the X connector account to delete + connector_id: ID of the X connector account to delete """ - endpoint = ENDPOINT_X_CONNECTOR_DELETE.format(credential_id=credential_id) + endpoint = ENDPOINT_X_CONNECTOR_DELETE.format(connector_id=connector_id) self.client.delete(endpoint) From d11446de8e3a415202a969638e6d51dc0763ace4 Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:35:22 +0800 Subject: [PATCH 08/10] update quick-start docs and modified filed name --- examples/quickstart.md | 59 ++++++++++++++++++++++++++++++++++-- examples/quickstart_cn.md | 59 ++++++++++++++++++++++++++++++++++-- wyseos/mate/services/user.py | 6 ++-- 3 files changed, 115 insertions(+), 9 deletions(-) diff --git a/examples/quickstart.md b/examples/quickstart.md index 6f65b0b..d898f4e 100644 --- a/examples/quickstart.md +++ b/examples/quickstart.md @@ -43,7 +43,49 @@ from wyseos.mate.config import load_config client = Client(load_config("mate.yaml")) ``` -## 3. Choose One Workflow +## 3. Register an Account (Two Ways) + +Sign-in and sign-up both start without `api_key` or `jwt_token` on the client (these endpoints use `skip_auth=True`). You can use `Client(ClientOptions())` for the default `base_url`, or `load_config("mate.yaml")` if you only need a custom `base_url`. + +1. **Email magic link** — send a link to the address; the user opens it in a browser to finish sign-in or sign-up: + +```python +from wyseos.mate import Client, ClientOptions + +client = Client(ClientOptions()) # or load_config("mate.yaml") without credentials + +resp = client.user.start_email_verification( + email="you@example.com", + invite_code=None, # optional invite code +) +# resp.sign_type, resp.pre_auth_id — use after the user follows the link per product flow +``` + +2. **X (Twitter) OAuth** — get a login URL; open it in a browser to complete sign-in or sign-up with X: + +```python +url_resp = client.user.get_x_oauth_url() +print(url_resp.auth_url) # open in browser +``` + +Runnable reference: `examples/auth/auth_example.py`. + +## 4. X (Twitter) Authorization: Functions + +Distinguish **account login/registration** (no API key) from **binding X to an existing account** (requires `api_key` or `jwt_token` after you are signed in). + +| Purpose | Call | Auth required | +| ------- | ---- | ------------- | +| Sign in / sign up with X | `client.user.get_x_oauth_url()` | No | +| List connected X accounts | `client.user.list_x_accounts()` | Yes | +| Bind or re-bind an X account (connector) | `client.user.authorize_x_account(target_credential_id=None)` | Yes; optional `target_credential_id` selects the credential slot | +| Remove a connected X account | `client.user.delete_x_account(connector_id)` | Yes | + +`authorize_x_account` returns an `OAuthURLResponse` with `auth_url` — open that URL in a browser to complete the connector flow. + +Runnable reference: `examples/auth/connectors_example.py`. + +## 5. Choose One Workflow | Workflow | Entry Point | Transport | Main Output | | ----------------- | -------------------------------------------------------- | ------------ | ------------------------------------------ | @@ -187,7 +229,7 @@ Runnable example: `examples/product_analysis/example.py`. --- -## 4. Error Handling +## 6. Error Handling ```python from wyseos.mate.errors import APIError, NetworkError, ConfigError, WebSocketError @@ -205,7 +247,18 @@ except ConfigError as e: print("ConfigError:", e) ``` -## 5. Related APIs +## 7. Related APIs + +Account registration (no `api_key` / `jwt_token` required): + +- `client.user.start_email_verification(email, invite_code=None)` +- `client.user.get_x_oauth_url()` + +X connector (after sign-in, with `api_key` or `jwt_token`): + +- `client.user.list_x_accounts()` +- `client.user.authorize_x_account(target_credential_id=None)` +- `client.user.delete_x_account(connector_id)` Marketing: diff --git a/examples/quickstart_cn.md b/examples/quickstart_cn.md index 14d5a7c..b2cbb2f 100644 --- a/examples/quickstart_cn.md +++ b/examples/quickstart_cn.md @@ -42,7 +42,49 @@ from wyseos.mate.config import load_config client = Client(load_config("mate.yaml")) ``` -## 3. 选择一个工作流 +## 3. 注册账户(两种方式) + +登录与注册都可在**不提供** `api_key`、`jwt_token` 的情况下发起(这些接口在 SDK 中走 `skip_auth`)。无密钥时可用 `Client(ClientOptions())` 使用默认 `base_url`,或仅用 `load_config("mate.yaml")` 配置自定义 `base_url`。 + +1. **邮箱 magic link** — 向邮箱发送登录/注册链接,用户在浏览器中打开完成验证: + +```python +from wyseos.mate import Client, ClientOptions + +client = Client(ClientOptions()) # 或使用不含密钥的 mate.yaml + +resp = client.user.start_email_verification( + email="you@example.com", + invite_code=None, # 可选填邀请码 +) +# resp.sign_type、resp.pre_auth_id — 用户点击邮件内链接后按产品流程续接 +``` + +2. **X(Twitter)OAuth** — 获取用于登录/注册的授权 URL,在浏览器中打开完成: + +```python +url_resp = client.user.get_x_oauth_url() +print(url_resp.auth_url) # 在浏览器中打开 +``` + +可运行参考:`examples/auth/auth_example.py`。 + +## 4. 授权与绑定 X(相关函数) + +区分两类场景:**账户登录/注册**(无 API 密钥)与**将 X 账号绑定到已有账户**(登录后使用 `api_key` 或 `jwt_token`)。 + +| 用途 | 调用 | 是否需要已登录 | +| ---- | ---- | -------------- | +| 使用 X 登录或注册 | `client.user.get_x_oauth_url()` | 否 | +| 列出已绑定的 X 账号 | `client.user.list_x_accounts()` | 是 | +| 发起绑定或换绑 X(连接器) | `client.user.authorize_x_account(target_connector_id=None)` | 是;可选 `target_connector_id` 指定要写入的凭据位 | +| 解绑已连接的 X 账号 | `client.user.delete_x_account(connector_id)` | 是 | + +`authorize_x_account` 返回 `OAuthURLResponse`,通过 `auth_url` 在浏览器中完成授权。 + +可运行参考:`examples/auth/connectors_example.py`。 + +## 5. 选择一个工作流 | 工作流 | 入口 | 传输方式 | 主要输出 | | -------- | -------------------------------------------------------- | --------- | ----------------------- | @@ -186,7 +228,7 @@ if info.analysis_result and info.analysis_result.report_id: --- -## 4. 错误处理 +## 6. 错误处理 ```python from wyseos.mate.errors import APIError, NetworkError, ConfigError, WebSocketError @@ -204,7 +246,18 @@ except ConfigError as e: print("ConfigError:", e) ``` -## 5. 相关 API +## 7. 相关 API + +账户注册(不需要 `api_key` / `jwt_token`): + +- `client.user.start_email_verification(email, invite_code=None)` +- `client.user.get_x_oauth_url()` + +X 连接器(登录后、带 `api_key` 或 `jwt_token`): + +- `client.user.list_x_accounts()` +- `client.user.authorize_x_account(target_credential_id=None)` +- `client.user.delete_x_account(connector_id)` 营销: diff --git a/wyseos/mate/services/user.py b/wyseos/mate/services/user.py index 97418d6..f332ea9 100644 --- a/wyseos/mate/services/user.py +++ b/wyseos/mate/services/user.py @@ -139,20 +139,20 @@ def list_x_accounts(self) -> ListXAccountsResponse: return resp.data def authorize_x_account( - self, target_credential_id: Optional[str] = None + self, target_connector_id: Optional[str] = None ) -> OAuthURLResponse: """ Start the OAuth authorization flow to bind an X (Twitter) account. Args: - target_credential_id: Optional ID of the credential slot to bind the X + target_connector_id: Optional ID of the credential slot to bind the X account to. When omitted, the backend creates a new credential. Returns: OAuthURLResponse: Response containing the authorization URL """ payload = AuthorizeXAccountRequest( - target_credential_id=target_credential_id, + target_credential_id=target_connector_id, redirect_url=( f"{resolve_extension_webapp_host()}" "/settings/integrations/x/callback?scene=connector_x_bind" From 44ae828f4e0731cc1072299c07df19fbda20402e Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:03:56 +0800 Subject: [PATCH 09/10] fix param --- examples/auth/connectors_example.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/auth/connectors_example.py b/examples/auth/connectors_example.py index f771723..24cc90b 100644 --- a/examples/auth/connectors_example.py +++ b/examples/auth/connectors_example.py @@ -65,7 +65,7 @@ def do_authorize(client: Client) -> None: ).strip() try: resp = client.user.authorize_x_account( - target_credential_id=target or None, + target_connector_id=target or None, ) except APIError as exc: print(f"Authorize failed: {exc}") From 0c04dd783372931936645d6d84a7dfa0f34aaa92 Mon Sep 17 00:00:00 2001 From: CatFlow <22841916+Meow711@users.noreply.github.com> Date: Fri, 24 Apr 2026 19:07:48 +0800 Subject: [PATCH 10/10] update docs --- examples/quickstart.md | 4 ++-- examples/quickstart_cn.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/quickstart.md b/examples/quickstart.md index d898f4e..c6bd0ad 100644 --- a/examples/quickstart.md +++ b/examples/quickstart.md @@ -78,7 +78,7 @@ Distinguish **account login/registration** (no API key) from **binding X to an e | ------- | ---- | ------------- | | Sign in / sign up with X | `client.user.get_x_oauth_url()` | No | | List connected X accounts | `client.user.list_x_accounts()` | Yes | -| Bind or re-bind an X account (connector) | `client.user.authorize_x_account(target_credential_id=None)` | Yes; optional `target_credential_id` selects the credential slot | +| Bind or re-bind an X account (connector) | `client.user.authorize_x_account(target_connector_id=None)` | Yes; optional `target_connector_id` selects the credential slot | | Remove a connected X account | `client.user.delete_x_account(connector_id)` | Yes | `authorize_x_account` returns an `OAuthURLResponse` with `auth_url` — open that URL in a browser to complete the connector flow. @@ -257,7 +257,7 @@ Account registration (no `api_key` / `jwt_token` required): X connector (after sign-in, with `api_key` or `jwt_token`): - `client.user.list_x_accounts()` -- `client.user.authorize_x_account(target_credential_id=None)` +- `client.user.authorize_x_account(target_connector_id=None)` - `client.user.delete_x_account(connector_id)` Marketing: diff --git a/examples/quickstart_cn.md b/examples/quickstart_cn.md index b2cbb2f..d02759d 100644 --- a/examples/quickstart_cn.md +++ b/examples/quickstart_cn.md @@ -256,7 +256,7 @@ except ConfigError as e: X 连接器(登录后、带 `api_key` 或 `jwt_token`): - `client.user.list_x_accounts()` -- `client.user.authorize_x_account(target_credential_id=None)` +- `client.user.authorize_x_account(target_connector_id=None)` - `client.user.delete_x_account(connector_id)` 营销: