diff --git a/.vscode/settings.json b/.vscode/settings.json index eb5a2c9..c847e64 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,6 +8,7 @@ "ignoretz", "PYPI", "pytest", - "serie_number" + "serie_number", + "Tago" ] } diff --git a/docs/source/Device/index.rst b/docs/source/Device/index.rst index 23be0b5..2c68512 100644 --- a/docs/source/Device/index.rst +++ b/docs/source/Device/index.rst @@ -12,7 +12,7 @@ Instance | **token**: str | Device Token - | *Optional* **region**: Regions: "usa-1" or "env" + | *Optional* **region**: Regions: "us-e1" or "ue-w1" or "env" | Region is a optional parameter .. code-block:: @@ -20,7 +20,7 @@ Instance from tagoio_sdk import Device - myDevice = Device({"token": "my_device_token", "region": "usa-1"}) + myDevice = Device({"token": "my_device_token", "region": "us-e1"}) diff --git a/docs/source/Resources/Account/Account_type.rst b/docs/source/Resources/Account/Account_type.rst index ad387e5..69e6fe6 100644 --- a/docs/source/Resources/Account/Account_type.rst +++ b/docs/source/Resources/Account/Account_type.rst @@ -6,49 +6,193 @@ AccountOptions -------------- + **Attributes:** - | **user_view_welcome**: bool - | **decimal_separator**: str - | **thousand_separator**: str - | **last_whats_new**: Optional[datetime] + | user_view_welcome: bool + + | decimal_separator: str + + | thousand_separator: str + + | last_whats_new: Optional[datetime] .. _AccountOpt: AccountOpt ------------ +---------- + **Attributes:** - | **authenticator**: bool - | **sms**: bool - | **email**: bool + | authenticator: bool + + | sms: bool + + | email: bool .. _AccountInfo: AccountInfo ----------- + + **Attributes:** + + | active: bool + + | name: str + + | email: str + + | country: Optional[str] + + | timezone: str + + | company: Optional[str] + + | newsletter: Optional[bool] + + | developer: Optional[bool] + + | blocked: bool + + | id: :ref:`GenericID` + + | language: str + + | last_login: Optional[datetime] + + | options: :ref:`AccountOptions` + + | phone: Optional[str] + + | send_invoice: bool + + | stripe_id: Optional[str] + + | type: str + + | plan: str + + | created_at: datetime + + | updated_at: datetime + + | otp: Optional[:ref:`AccountOpt`] + + +.. _AccountCreateInfo: + +AccountCreateInfo +----------------- + + Information required to create a new TagoIO account. + + **Attributes:** + + | name: str + + | email: str + + | password: str + + | cpassword: str + + | country: Optional[str] + + | timezone: str + + | company: Optional[str] + + | newsletter: Optional[bool] + + | developer: Optional[bool] + + +.. _OTPType: + +OTPType +------- + + Type of One-Time Password authentication method. + + **Values:** + + | "sms" or "email" or "authenticator" + + +.. _TokenCreateInfo: + +TokenCreateInfo +--------------- + + Information required to create a new account token. + + **Attributes:** + + | profile_id: :ref:`GenericID` + + | email: str + + | password: str + + | pin_code: str + + | otp_type: :ref:`OTPType` + + | name: str + + +.. _LoginCredentials: + +LoginCredentials +---------------- + + Credentials required for account login. + **Attributes:** - | **active**: bool - | **name**: str - | **email**: str - | **country**: Optional[str] - | **timezone**: str - | **company**: Optional[str] - | **newsletter**: Optional[bool] - | **developer**: Optional[bool] - | **blocked**: bool - | **id**: GenericID - | **language**: str - | **last_login**: Optional[datetime] - | **options**: :ref:`AccountOptions` - | **phone**: Optional[str] - | **send_invoice**: bool - | **stripe_id**: Optional[str] - | **type**: str - | **plan**: str - | **created_at**: datetime - | **updated_at**: datetime - | **otp**: Optional[:ref:`AccountOpt`] + | email: str + + | password: str + + | otp_type: :ref:`OTPType` + + | pin_code: str + + +.. _ProfileListInfoForLogin: + +ProfileListInfoForLogin +----------------------- + + Profile information returned in login response. + + **Attributes:** + + | id: :ref:`GenericID` + + | name: str + + +.. _LoginResponse: + +LoginResponse +------------- + + Response data from account login endpoint. + + **Attributes:** + + | type: str + + | id: :ref:`GenericID` + + | email: str + + | company: str + + | name: str + + | profiles: List[:ref:`ProfileListInfoForLogin`] diff --git a/docs/source/Resources/Account/index.rst b/docs/source/Resources/Account/index.rst index 6521ecd..5b2da6e 100644 --- a/docs/source/Resources/Account/index.rst +++ b/docs/source/Resources/Account/index.rst @@ -1,3 +1,476 @@ +**Account** +=========== + +Manage your TagoIO account, authentication, and security settings. + +==== +info +==== + +Gets all account information including settings, preferences, and OTP configuration. + +See: `Edit Account `_ + + **Returns:** + + | :ref:`AccountInfo` + + .. code-block:: python + + # If receive an error "Authorization Denied", check your account token permissions. + from tagoio_sdk import Resources + + resources = Resources() + account_info = resources.account.info() + print(account_info) # {'id': 'account-id', 'name': 'My Account', ...} + + +==== +edit +==== + +Edit current account information such as name, timezone, company, and preferences. + +See: `Edit Account `_ + + **Parameters:** + + | **accountObj**: dict + | Account information to update + + **Returns:** + + | str + + .. code-block:: python + + # If receive an error "Authorization Denied", check your account token permissions. + from tagoio_sdk import Resources + + resources = Resources() + result = resources.account.edit({ + "name": "Updated Account Name", + "timezone": "America/New_York", + "company": "My Company" + }) + print(result) # Account Successfully Updated + + +====== +delete +====== + +Delete current account. This action is irreversible and will remove all profiles and data. + +See: `Deleting Your Account `_ + + **Returns:** + + | str + + .. code-block:: python + + # If receive an error "Authorization Denied", check your account token permissions. + # WARNING: This action is irreversible! + from tagoio_sdk import Resources + + resources = Resources() + result = resources.account.delete() + print(result) # Account Successfully Deleted + + +============== +passwordChange +============== + +Change account password for the authenticated user. + +See: `Resetting My Password `_ + + **Parameters:** + + | **password**: str + | New password + + **Returns:** + + | str + + .. code-block:: python + + # If receive an error "Authorization Denied", check your account token permissions. + from tagoio_sdk import Resources + + resources = Resources() + result = resources.account.passwordChange("new-secure-password") + print(result) # Password changed successfully + + +========= +enableOTP +========= + +Enable OTP (One-Time Password) for a given OTP Type (authenticator, sms, or email). +You will be requested to confirm the operation with a pin code. + +See: `Two-Factor Authentication `_ + + **Parameters:** + + | **credentials**: dict + | Dictionary with email and password + + | **typeOTP**: :ref:`OTPType` + | Type of OTP: "authenticator", "sms", or "email" + + **Returns:** + + | str + + .. code-block:: python + + # If receive an error "Authorization Denied", check your account token permissions. + from tagoio_sdk import Resources + + resources = Resources() + result = resources.account.enableOTP( + {"email": "user@example.com", "password": "your-password"}, + "email" + ) + print(result) # OTP enabled, confirmation required + + +========== +disableOTP +========== + +Disable OTP (One-Time Password) for a given OTP Type (authenticator, sms, or email). + +See: `Two-Factor Authentication `_ + + **Parameters:** + + | **credentials**: dict + | Dictionary with email and password + + | **typeOTP**: :ref:`OTPType` + | Type of OTP: "authenticator", "sms", or "email" + + **Returns:** + + | str + + .. code-block:: python + + # If receive an error "Authorization Denied", check your account token permissions. + from tagoio_sdk import Resources + + resources = Resources() + result = resources.account.disableOTP( + {"email": "user@example.com", "password": "your-password"}, + "authenticator" + ) + print(result) # OTP disabled successfully + + +========== +confirmOTP +========== + +Confirm OTP enabling process for a given OTP Type (authenticator, sms, or email). + +See: `Two-Factor Authentication `_ + + **Parameters:** + + | **pinCode**: str + | Six-digit PIN code + + | **typeOTP**: :ref:`OTPType` + | Type of OTP: "authenticator", "sms", or "email" + + **Returns:** + + | str + + .. code-block:: python + + # If receive an error "Authorization Denied", check your account token permissions. + from tagoio_sdk import Resources + + resources = Resources() + result = resources.account.confirmOTP("123456", "email") + print(result) # OTP confirmed successfully + + +=========== +tokenCreate +=========== + +Generates and retrieves a new token for the account. This is a static method that doesn't require authentication. + +See: `Account Token `_ + + **Parameters:** + + | **tokenParams**: :ref:`TokenCreateInfo` + | Token creation parameters + + | *Optional* **region**: :ref:`Regions` + | TagoIO Region Server (default: USA) + + **Returns:** + + | dict + + .. code-block:: python + + from tagoio_sdk.modules.Resources.Account import Account + + token_result = Account.tokenCreate({ + "profile_id": "profile-id-123", + "email": "user@example.com", + "password": "your-password", + "pin_code": "123456", + "otp_type": "email", + "name": "My API Token" + }) + print(token_result["token"]) # your-new-token-123 + + +===== +login +===== + +Retrieve list of profiles for login and perform authentication. This is a static method that doesn't require authentication. + +See: `Login to Account `_ + + **Parameters:** + + | **credentials**: :ref:`LoginCredentials` + | Login credentials including email, password, OTP type, and PIN code + + | *Optional* **region**: :ref:`Regions` + | TagoIO Region Server (default: USA) + + **Returns:** + + | :ref:`LoginResponse` + + .. code-block:: python + + from tagoio_sdk.modules.Resources.Account import Account + + login_result = Account.login({ + "email": "user@example.com", + "password": "your-password", + "otp_type": "email", + "pin_code": "123456" + }) + print(login_result) # {'type': 'user', 'id': '...', 'profiles': [...]} + + +=============== +passwordRecover +=============== + +Send password recovery email to the specified address. This is a static method that doesn't require authentication. + +See: `Resetting My Password `_ + + **Parameters:** + + | **email**: str + | Email address for password recovery + + | *Optional* **region**: :ref:`Regions` + | TagoIO Region Server (default: USA) + + **Returns:** + + | str + + .. code-block:: python + + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.passwordRecover("user@example.com") + print(result) # Email sent successfully + + +====== +create +====== + +Create a new TagoIO account. This is a static method that doesn't require authentication. + + **Parameters:** + + | **createParams**: :ref:`AccountCreateInfo` + | Account creation parameters + + | *Optional* **region**: :ref:`Regions` + | TagoIO Region Server (default: USA) + + **Returns:** + + | str + + .. code-block:: python + + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.create({ + "name": "John Doe", + "email": "john@example.com", + "password": "secure-password", + "cpassword": "secure-password", + "timezone": "America/New_York", + "company": "My Company", + "newsletter": False + }) + print(result) # Account created successfully + + +================== +resendConfirmation +================== + +Re-send confirmation account email to the specified address. This is a static method that doesn't require authentication. + + **Parameters:** + + | **email**: str + | Email address to resend confirmation + + | *Optional* **region**: :ref:`Regions` + | TagoIO Region Server (default: USA) + + **Returns:** + + | str + + .. code-block:: python + + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.resendConfirmation("user@example.com") + print(result) # Confirmation email sent + + +============== +confirmAccount +============== + +Confirm account creation using the token sent via email. This is a static method that doesn't require authentication. + + **Parameters:** + + | **token**: str + | Confirmation token from email + + | *Optional* **region**: :ref:`Regions` + | TagoIO Region Server (default: USA) + + **Returns:** + + | str + + .. code-block:: python + + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.confirmAccount("confirmation-token-123") + print(result) # Account confirmed successfully + + +=================== +requestLoginPINCode +=================== + +Request the PIN Code for a given OTP Type (authenticator, sms, or email). This is a static method that doesn't require authentication. + +See: `Two-Factor Authentication `_ + + **Parameters:** + + | **credentials**: dict + | Dictionary with email and password + + | **typeOTP**: :ref:`OTPType` + | Type of OTP: "authenticator", "sms", or "email" + + | *Optional* **region**: :ref:`Regions` + | TagoIO Region Server (default: USA) + + **Returns:** + + | str + + .. code-block:: python + + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.requestLoginPINCode( + {"email": "user@example.com", "password": "your-password"}, + "email" + ) + print(result) # PIN code sent + + +==================== +acceptTeamInvitation +==================== + +Accept a team member invitation to become a profile's team member. This is a static method that doesn't require authentication. + + **Parameters:** + + | **token**: str + | Invitation token from email + + | *Optional* **region**: :ref:`Regions` + | TagoIO Region Server (default: USA) + + **Returns:** + + | str + + .. code-block:: python + + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.acceptTeamInvitation("invitation-token-123") + print(result) # Invitation accepted + + +===================== +declineTeamInvitation +===================== + +Decline a team member invitation to become a profile's team member. This is a static method that doesn't require authentication. + + **Parameters:** + + | **token**: str + | Invitation token from email + + | *Optional* **region**: :ref:`Regions` + | TagoIO Region Server (default: USA) + + **Returns:** + + | str + + .. code-block:: python + + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.declineTeamInvitation("invitation-token-123") + print(result) # Invitation declined + .. toctree:: - Account_type \ No newline at end of file + Account_type + ../../regions diff --git a/docs/source/Resources/Analysis/Analysis_Type.rst b/docs/source/Resources/Analysis/Analysis_Type.rst index d7b7e83..e0fa082 100644 --- a/docs/source/Resources/Analysis/Analysis_Type.rst +++ b/docs/source/Resources/Analysis/Analysis_Type.rst @@ -102,3 +102,68 @@ AnalysisListItem | locked_at: Optional[datetime] | console: Optional[List[str]] + + +.. _SnippetRuntime: + +SnippetRuntime +-------------- + + Available runtime environments for snippets. + + **Type:** + + | Literal["node-legacy", "python-legacy", "node-rt2025", "python-rt2025", "deno-rt2025"] + + +.. _SnippetItem: + +SnippetItem +----------- + + Individual snippet metadata. + + **Attributes:** + + | id: str + | Unique identifier for the snippet + + | title: str + | Human-readable title + + | description: str + | Description of what the snippet does + + | language: str + | Programming language (typescript, javascript, python) + + | tags: List[str] + | Array of tags for categorization + + | filename: str + | Filename of the snippet + + | file_path: str + | Full path to the file in the runtime directory + + +.. _SnippetsListResponse: + +SnippetsListResponse +-------------------- + + API response containing all snippets metadata for a runtime. + + **Attributes:** + + | runtime: :ref:`SnippetRuntime` + | Runtime environment identifier + + | schema_version: int + | Schema version for the API response format + + | generated_at: str + | ISO timestamp when the response was generated + + | snippets: List[:ref:`SnippetItem`] + | Array of all available snippets for this runtime diff --git a/docs/source/Resources/Analysis/index.rst b/docs/source/Resources/Analysis/index.rst index 7086b44..46a95ca 100644 --- a/docs/source/Resources/Analysis/index.rst +++ b/docs/source/Resources/Analysis/index.rst @@ -1,13 +1,16 @@ **Analysis** ============ -Manage analysis in account. +Manage analysis in your application. ======= list ======= -Retrieves a list with all analyses from the account +Lists all analyses from the application with pagination support. +Use this to retrieve and manage analyses in your application. + +See: `Analysis `_ **Parameters:** @@ -17,238 +20,343 @@ Retrieves a list with all analyses from the account .. code-block:: :caption: **Default queryObj:** - queryObj: { + queryObj = { "page": 1, "fields": ["id", "name"], "filter": {}, "amount": 20, - "orderBy": ["name","asc"], + "orderBy": ["name", "asc"] } **Returns:** | list[:ref:`AnalysisListItem`] - .. code-block:: - :caption: **Example:** + .. code-block:: python - from tagoio_sdk import Resources + # If receive an error "Authorization Denied", check policy "Analysis" / "Access" in Access Management. + from tagoio_sdk import Resources - resources = Resources() - resources.analysis.list() + resources = Resources() + list_result = resources.analyses.list({ + "page": 1, + "fields": ["id", "name"], + "amount": 10, + "orderBy": ["name", "asc"] + }) + print(list_result) # [{'id': 'analysis-id-123', 'name': 'Analysis Test', ...}] ======= create ======= -Create a new analysis +Creates a new analysis in your application. + +See: `Creating Analysis `_ **Parameters:** - | **analysisInfo**: :ref:`AnalysisCreateInfo` - | Analysis information + | **analysisObj**: :ref:`AnalysisCreateInfo` + | Data object to create new TagoIO Analysis **Returns:** | Dict[str, GenericID | GenericToken] - .. code-block:: - :caption: **Example:** + .. code-block:: python - from tagoio_sdk import Resources + # If receive an error "Authorization Denied", check policy "Analysis" / "Create" in Access Management. + from tagoio_sdk import Resources - resources = Resources() - resources.analysis.create({ - "name": "My Analysis", - "runtime": "python", - "active": True, - }) + resources = Resources() + new_analysis = resources.analyses.create({ + "name": "My Analysis", + "runtime": "python", + "tags": [{"key": "type", "value": "data-processing"}] + }) + print(new_analysis["id"], new_analysis["token"]) # analysis-id-123, analysis-token-123 ======= edit ======= -Modify any property of the analyze +Modifies an existing analysis. + +See: `Analysis `_ **Parameters:** - | **analysisID**: GenericID: str - | Analysis ID + | **analysisID**: str + | Analysis identification - | **analysisInfo**: :ref:`AnalysisCreateInfo` - | Analysis information + | **analysisObj**: :ref:`AnalysisInfo` + | Analysis object with data to replace **Returns:** | string - .. code-block:: - :caption: **Example:** + .. code-block:: python - from tagoio_sdk import Resources + # If receive an error "Authorization Denied", check policy "Analysis" / "Create" in Access Management. + from tagoio_sdk import Resources - resources = Resources() - resources.analysis.edit("analysisID", { "name": "My Analysis Edited" }) + resources = Resources() + result = resources.analyses.edit("analysis-id-123", { + "name": "Updated Analysis", + "active": False + }) + print(result) # Successfully Updated ======= delete ======= -Deletes an analysis from the account +Deletes an analysis from your application. + +See: `Analysis `_ **Parameters:** - | **analysisID**: GenericID: str - | Analysis ID + | **analysisID**: str + | Analysis identification **Returns:** | string - .. code-block:: - :caption: **Example:** + .. code-block:: python - from tagoio_sdk import Resources + # If receive an error "Authorization Denied", check policy "Analysis" / "Delete" in Access Management. + from tagoio_sdk import Resources - resources = Resources() - resources.analysis.delete("analysisID") + resources = Resources() + result = resources.analyses.delete("analysis-id-123") + print(result) # Successfully Removed ======= info ======= -Gets information about an analysis +Retrieves detailed information about a specific analysis. + +See: `Analysis `_ **Parameters:** - | **analysisID**: GenericID: str - | Analysis ID + | **analysisID**: str + | Analysis identification **Returns:** | :ref:`AnalysisInfo` - .. code-block:: - :caption: **Example:** + .. code-block:: python - from tagoio_sdk import Resources + # If receive an error "Authorization Denied", check policy "Analysis" / "Access" in Access Management. + from tagoio_sdk import Resources - resources = Resources() - resources.analysis.info("analysisID") + resources = Resources() + analysis_info = resources.analyses.info("analysis-id-123") + print(analysis_info) # {'id': 'analysis-id-123', 'name': 'My Analysis', ...} ======= run ======= -Run an analysis +Executes an analysis with optional scope parameters. + +See: `Analysis `_ **Parameters:** - | **analysisID**: GenericID: str - | Analysis ID + | **analysisID**: str + | Analysis identification + + | *Optional* **scopeObj**: Dict[str, Any] + | Simulate scope for analysis **Returns:** | Dict[str, GenericToken] - .. code-block:: - :caption: **Example:** + .. code-block:: python - from tagoio_sdk import Resources + # If receive an error "Authorization Denied", check policy "Analysis" / "Run Analysis" in Access Management. + from tagoio_sdk import Resources - resources = Resources() - resources.analysis.run("analysisID") + resources = Resources() + result = resources.analyses.run("analysis-id-123", {"environment": "production"}) + print(result["analysis_token"]) # analysis-token-123 ============= tokenGenerate ============= -Generate a new token for the analysis +Generates a new token for the analysis. +This is only allowed when the analysis is running in external mode. + +See: `Analysis `_ **Parameters:** - | **analysisID**: GenericID: str - | Analysis ID + | **analysisID**: str + | Analysis identification **Returns:** | Dict[str, str] - .. code-block:: - :caption: **Example:** + .. code-block:: python - from tagoio_sdk import Resources + from tagoio_sdk import Resources - resources = Resources() - resources.analysis.tokenGenerate("analysisID") + resources = Resources() + token = resources.analyses.tokenGenerate("analysis-id-123") + print(token["analysis_token"]) # analysis-token-123 ============ uploadScript ============ -Upload a file (base64) to Analysis. Automatically erase the old one +Uploads a script file to an analysis. +The file content must be base64-encoded. This automatically replaces the old script. + +See: `Analysis `_ **Parameters:** - | **analysisID**: GenericID: str - | Analysis ID + | **analysisID**: str + | Analysis identification - | **file**: :ref:`ScriptFile` - | File information + | **fileObj**: :ref:`ScriptFile` + | Object with name, language and content (base64) of the file **Returns:** | string - .. code-block:: - :caption: **Example:** + .. code-block:: python - from tagoio_sdk import Resources - import base64 + # If receive an error "Authorization Denied", check policy "Analysis" / "Upload Analysis Script" in Access Management. + from tagoio_sdk import Resources - data = "print(Hello, World!)" - encoded_bytes = base64.b64encode(data.encode('utf-8')).decode('utf-8') - - resources = Resources() - resources.analysis.uploadScript("analysisID", { - "name": "My Script", - "content": encoded_bytes, - "language": "python", - }) + resources = Resources() + result = resources.analyses.uploadScript("analysis-id-123", { + "name": "script.py", + "content": "base64-encoded-content", + "language": "python" + }) + print(result) # Successfully Uploaded ============== downloadScript ============== -Get a url to download the analysis. If `version` is specified in `options`, downloads a specific version. +Gets a download URL for the analysis script. +If version is specified in options, downloads a specific version. + +See: `Analysis `_ **Parameters:** - | **analysisID**: GenericID: str - | Analysis ID + | **analysisID**: str + | Analysis identification - | *Optional* **options**: Dict["version", int] - | Options + | *Optional* **options**: Dict[Literal["version"], int] + | Options for the Analysis script to download (e.g., {"version": 1}) **Returns:** - | Dict[str, Any] + | Dict + + .. code-block:: python + + # If receive an error "Authorization Denied", check policy "Analysis" / "Download Analysis Script" in Access Management. + from tagoio_sdk import Resources + + resources = Resources() + download = resources.analyses.downloadScript("analysis-id-123", {"version": 1}) + print(download["url"]) # https://... + print(download["expire_at"]) # 2025-01-13T... + + +============ +listSnippets +============ + +Get all available snippets for a specific runtime environment. +Fetches analysis code snippets from the public TagoIO snippets repository. + +See: `Script Examples `_ + +See: `Script Editor `_ + + **Parameters:** + + | **runtime**: :ref:`SnippetRuntime` + | The runtime environment to get snippets for + + **Returns:** + + | :ref:`SnippetsListResponse` + + .. code-block:: python + + from tagoio_sdk import Resources + + resources = Resources() + deno_snippets = resources.analyses.listSnippets("deno-rt2025") + + # Print all snippet titles + for snippet in deno_snippets["snippets"]: + print(f"{snippet['title']}: {snippet['description']}") + + +============== +getSnippetFile +============== + +Get the raw source code content of a specific snippet file. +Fetches the actual code content from the TagoIO snippets repository. + +See: `Script Examples `_ + +See: `Script Editor `_ + + **Parameters:** + + | **runtime**: :ref:`SnippetRuntime` + | The runtime environment the snippet belongs to + + | **filename**: str + | The filename of the snippet to retrieve + + **Returns:** + + | str + + .. code-block:: python + + from tagoio_sdk import Resources - .. code-block:: - :caption: **Example:** + resources = Resources() - from tagoio_sdk import Resources + # Get TypeScript code for console example + code = resources.analyses.getSnippetFile("deno-rt2025", "console.ts") + print(code) - resources = Resources() - resources.analysis.downloadScript("analysisID") + # Get Python code for data processing + python_code = resources.analyses.getSnippetFile("python-rt2025", "avg-min-max.py") + print(python_code) .. toctree:: diff --git a/docs/source/Resources/Run/Run_Types.rst b/docs/source/Resources/Run/Run_Types.rst index 560f1e2..588da9b 100644 --- a/docs/source/Resources/Run/Run_Types.rst +++ b/docs/source/Resources/Run/Run_Types.rst @@ -294,7 +294,7 @@ UserInfo | **options**: object | **tags**: list[:ref:`TagsObj`] -.. _LoginResponse: +.. _LoginResponseRunUser: LoginResponse ------------- diff --git a/docs/source/Resources/Run/index.rst b/docs/source/Resources/Run/index.rst index 00af65c..3a27538 100644 --- a/docs/source/Resources/Run/index.rst +++ b/docs/source/Resources/Run/index.rst @@ -154,7 +154,7 @@ Log in as a specific user in the TagoRUN service. **Returns:** - | **result**: :ref:`LoginResponse` + | **result**: :ref:`LoginResponseRunUser` | Login response. diff --git a/docs/source/Resources/index.rst b/docs/source/Resources/index.rst index d26df7c..158d67a 100644 --- a/docs/source/Resources/index.rst +++ b/docs/source/Resources/index.rst @@ -13,7 +13,7 @@ Instance | *Optional* **token**: str | Token is a optional parameter - | *Optional* **region**: str "usa-1" or "env" + | *Optional* **region**: str "us-e1" or "ue-w1" or "env" | Region is a optional parameter .. code-block:: diff --git a/docs/source/Services/index.rst b/docs/source/Services/index.rst index 4843814..3e9bf2a 100644 --- a/docs/source/Services/index.rst +++ b/docs/source/Services/index.rst @@ -10,7 +10,7 @@ Instance | *Optional* **token**: str | Token is a optional parameter (Analysis Token). - | *Optional* **region**: str "usa-1" or "env" + | *Optional* **region**: str "us-e1" or "ue-w1" or "env" | Region is a optional parameter .. code-block:: diff --git a/docs/source/conf.py b/docs/source/conf.py index 30148d1..e178612 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -61,4 +61,4 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +# html_static_path = ["_static"] diff --git a/docs/source/regions.rst b/docs/source/regions.rst new file mode 100644 index 0000000..c35025a --- /dev/null +++ b/docs/source/regions.rst @@ -0,0 +1,7 @@ +.. _Regions: + +Regions +---------------- + + | **Regions**: Literal["us-e1", "eu-w1", "env"] + | Supported TagoIO regions diff --git a/src/tagoio_sdk/common/JSON_Parse_Safe.py b/src/tagoio_sdk/common/JSON_Parse_Safe.py new file mode 100644 index 0000000..b39eae6 --- /dev/null +++ b/src/tagoio_sdk/common/JSON_Parse_Safe.py @@ -0,0 +1,14 @@ +import json + +from typing import Any + + +def JSONParseSafe(jsonString: str, default: Any = None) -> Any: + """Safely parse JSON string with fallback to default value""" + if not jsonString: + return default + + try: + return json.loads(jsonString) + except (json.JSONDecodeError, TypeError, ValueError): + return default if default is not None else {} diff --git a/src/tagoio_sdk/infrastructure/api_socket.py b/src/tagoio_sdk/infrastructure/api_socket.py deleted file mode 100644 index dd20b55..0000000 --- a/src/tagoio_sdk/infrastructure/api_socket.py +++ /dev/null @@ -1,35 +0,0 @@ -import socketio - -from tagoio_sdk import config -from tagoio_sdk.common.tagoio_module import GenericModuleParams -from tagoio_sdk.regions import getConnectionURI - - -socketOptions = config.tagoSDKconfig["socketOpts"] - - -class APISocket: - def __init__(self, params: GenericModuleParams) -> None: - url = getConnectionURI(params.get("region"))["realtime"] - URLRealtime = "{}{}{}".format(url, "?token=", params.get("token")) - self.realtimeURL = URLRealtime - - sio = socketio.AsyncClient( - reconnection=socketOptions["reconnection"], - reconnection_delay=socketOptions["reconnectionDelay"], - ) - self.sio = sio - - async def connect(self) -> socketio.AsyncClient: - await self.sio.connect( - url=self.realtimeURL, transports=socketOptions["transports"] - ) - await self.sio.wait() - - -channels = { - "notification": "notification::data", - "analysisConsole": "analysis::console", - "analysisTrigger": "analysis::trigger", - "bucketData": "bucket::data", -} diff --git a/src/tagoio_sdk/modules/Analysis/Analysis.py b/src/tagoio_sdk/modules/Analysis/Analysis.py index a7f151c..0fdcd84 100644 --- a/src/tagoio_sdk/modules/Analysis/Analysis.py +++ b/src/tagoio_sdk/modules/Analysis/Analysis.py @@ -1,76 +1,171 @@ +import asyncio +import inspect import json import os -import signal import sys from typing import Any -from typing import Callable +from typing import List from typing import Optional +from tagoio_sdk.common.JSON_Parse_Safe import JSONParseSafe from tagoio_sdk.common.tagoio_module import TagoIOModule from tagoio_sdk.infrastructure.api_sse import openSSEListening +from tagoio_sdk.modules.Analysis.Analysis_Type import AnalysisConstructorParams from tagoio_sdk.modules.Analysis.Analysis_Type import AnalysisEnvironment -from tagoio_sdk.modules.Services import Services +from tagoio_sdk.modules.Analysis.Analysis_Type import AnalysisFunction +from tagoio_sdk.modules.Services.Console import ConsoleService +from tagoio_sdk.regions import getConnectionURI as getRegionObj +from tagoio_sdk.regions import setRuntimeRegion T_ANALYSIS_CONTEXT = os.environ.get("T_ANALYSIS_CONTEXT") or None class Analysis(TagoIOModule): - def __init__(self, params): + """ + Analysis execution context for TagoIO + + This class provides the runtime environment for executing analysis scripts in TagoIO. + It manages environment variables, console outputs, and analysis execution lifecycle. + Analyses can run locally for development or in the TagoIO cloud platform. + + Example: Basic analysis usage + ```python + from tagoio_sdk import Analysis + + def my_analysis(context, scope): + # Get analysis environment variables + environment = context.environment + + # Use console service for logging + context.log("Analysis started") + + # Your analysis logic here + print("Processing data...") + + Analysis.use(analysis=my_analysis, params={"token": "your-analysis-token"}) + ``` + + Example: Analysis with EU region + ```python + from tagoio_sdk import Analysis + + def my_analysis(context, scope): + context.log("Running in EU region") + print("Environment:", context.environment) + + # Using Analysis.use() method + Analysis.use(analysis=my_analysis, params={"token": "your-analysis-token", "region": "eu-w1"}) + ``` + + Example: Analysis with Tago Deploy + ```python + from tagoio_sdk import Analysis + + def my_analysis(context, scope): + context.log("Running in TDeploy") + print("Scope:", scope) + + # Tago Deploy requires a dictionary with tdeploy ID + Analysis.use( + analysis=my_analysis, + params={ + "token": "your-analysis-token", + "region": {"tdeploy": "your-tdeploy-id"} + } + ) + ``` + + Example: Environment variables + ```python + def my_analysis(context, scope): + env = context.environment + api_key = next((e["value"] for e in env if e["key"] == "API_KEY"), None) + + Analysis.use(analysis=my_analysis, params={"token": "your-analysis-token"}) + ``` + """ + + def __init__(self, params: Optional[AnalysisConstructorParams] = None): + if params is None: + params = {"token": "unknown"} + super().__init__(params) + self.params = params self._running = True - def _signal_handler(self, signum, frame): - """Handle Ctrl+C gracefully""" - print("\n¬ Analysis stopped by user. Goodbye!") - self._running = False - sys.exit(0) + def init(self, analysis: AnalysisFunction): + self.analysis = analysis - def init(self, analysis: Callable): - self._analysis = analysis + if not os.environ.get("T_ANALYSIS_TOKEN") and self.params.get("token"): + os.environ["T_ANALYSIS_TOKEN"] = self.params.get("token") - # Set up signal handler for graceful shutdown - signal.signal(signal.SIGINT, self._signal_handler) - signal.signal(signal.SIGTERM, self._signal_handler) + # Configure runtime region + runtimeRegion = getRegionObj(self.params["region"]) if self.params.get("region") else None + if runtimeRegion: + setRuntimeRegion(runtimeRegion) if T_ANALYSIS_CONTEXT is None: - self.__localRuntime() + self._localRuntime() else: - self.__runOnTagoIO() + self._runOnTagoIO() + + def _runOnTagoIO(self) -> None: + if not self.analysis or not callable(self.analysis): + raise TypeError("Invalid analysis function") - def __runOnTagoIO(self): def context(): pass context.log = print - context.token = os.environ["T_ANALYSIS_TOKEN"] - context.analysis_id = os.environ["T_ANALYSIS_ID"] - try: - context.environment = json.loads(os.environ["T_ANALYSIS_ENV"]) - except (KeyError, json.JSONDecodeError): - context.environment = [] + context.token = os.environ.get("T_ANALYSIS_TOKEN", "") + context.analysis_id = os.environ.get("T_ANALYSIS_ID", "") + context.environment = JSONParseSafe(os.environ.get("T_ANALYSIS_ENV", "[]"), []) - try: - data = json.loads(os.environ["T_ANALYSIS_DATA"]) - except (KeyError, json.JSONDecodeError): - data = [] + data = JSONParseSafe(os.environ.get("T_ANALYSIS_DATA", "[]"), []) - self._analysis(context, data) + self.analysis(context, data) - def __runLocal( + def _stringifyMsg(self, msg: Any) -> str: + if isinstance(msg, dict) and not isinstance(msg, list): + return json.dumps(msg) + return str(msg) + + def _runLocal( self, - environment: list[AnalysisEnvironment], - data: list[Any], - analysis_id: str, + environment: List[AnalysisEnvironment], + data: List[Any], + analysisID: str, token: str, - ): - """Run Analysis @internal""" + ) -> None: + if not self.analysis or not callable(self.analysis): + raise TypeError("Invalid analysis function") + + tagoConsole = ConsoleService({"token": token, "region": self.params.get("region")}) + + def log(*args: Any) -> None: + """Log messages to console and TagoIO""" + # Only print locally if not auto-running + if not os.environ.get("T_ANALYSIS_AUTO_RUN"): + print(*args) + + # Handle error objects with stack trace + processedArgs = [] + for arg in args: + if hasattr(arg, "stack"): + processedArgs.append(arg.stack) + else: + processedArgs.append(arg) + + # Convert all arguments to strings + argsStrings = [self._stringifyMsg(arg) for arg in processedArgs] - def log(*args: any): - print(*args) - log_message = " ".join(str(arg) for arg in args) - Services.Services({"token": token}).console.log(log_message) + # Send to TagoIO console + try: + tagoConsole.log(" ".join(argsStrings)) + except Exception as e: + print(f"Console error: {e}", file=sys.stderr) def context(): pass @@ -78,70 +173,107 @@ def context(): context.log = log context.token = token context.environment = environment - context.analysis_id = analysis_id + context.analysis_id = analysisID - self._analysis(context, data or []) + # Execute analysis function + if inspect.iscoroutinefunction(self.analysis): + # Async function + try: + asyncio.run(self.analysis(context, data or [])) + except Exception as error: + log(error) + else: + # Sync function + try: + self.analysis(context, data or []) + except Exception as error: + log(error) + + def _localRuntime(self) -> None: + """Set up local runtime environment for development""" + if self.params.get("token") == "unknown": + raise ValueError("To run analysis locally, you need a token") - def __localRuntime(self): - analysis = self.doRequest({"path": "/info", "method": "GET"}) + try: + analysis = self.doRequest({"path": "/info", "method": "GET"}) + except Exception: + analysis = None if not analysis: - print("¬ Error :: Analysis not found or not active.") + print("¬ Error :: Analysis not found or not active or invalid analysis token.", file=sys.stderr) return if analysis.get("run_on") != "external": print("¬ Warning :: Analysis is not set to run on external") - tokenEnd = self.token[-5:] - + # Open SSE connection try: sse = openSSEListening( { - "token": self.token, - "region": self.region, + "token": self.params.get("token"), + "region": self.params.get("region"), "channel": "analysis_trigger", } ) - print( - f"\n¬ Connected to TagoIO :: Analysis [{analysis['name']}]({tokenEnd}) is ready." - ) - print("¬ Waiting for analysis trigger... (Press Ctrl+C to stop)\n") except Exception as e: - print("¬ Connection was closed, trying to reconnect...") - print(f"Error: {e}") + print(f"¬ Connection error: {e}", file=sys.stderr) return + tokenEnd = str(self.params.get("token", ""))[-5:] + + print(f"\n¬ Connected to TagoIO :: Analysis [{analysis.get('name', 'Unknown')}]({tokenEnd}) is ready.") + print("¬ Waiting for analysis trigger... (Press Ctrl+C to stop)\n") + try: for event in sse.events(): if not self._running: break try: - data = json.loads(event.data).get("payload") + parsed = JSONParseSafe(event.data, {}) + payload = parsed.get("payload") - if not data: + if not payload: continue - self.__runLocal( - data["environment"], - data["data"], - data["analysis_id"], + self._runLocal( + payload.get("environment", []), + payload.get("data", []), + payload.get("analysis_id", ""), self.token, ) - except RuntimeError: - print("¬ Connection was closed, trying to reconnect...") - pass + except Exception as e: + print(f"¬ Error processing event: {e}", file=sys.stderr) + continue + except KeyboardInterrupt: print("\n¬ Analysis stopped by user. Goodbye!") except Exception as e: - print(f"\n¬ Unexpected error: {e}") + print(f"¬ Connection was closed: {e}", file=sys.stderr) + print("¬ Trying to reconnect...") finally: self._running = False @staticmethod - def use(analysis: Callable, params: Optional[str] = {"token": "unknown"}): - if not os.environ.get("T_ANALYSIS_TOKEN"): - os.environ["T_ANALYSIS_TOKEN"] = params.get("token") - Analysis(params).init(analysis) - else: - Analysis({"token": os.environ["T_ANALYSIS_TOKEN"]}).init(analysis) + def use( + analysis: AnalysisFunction, + params: Optional[AnalysisConstructorParams] = None, + ) -> "Analysis": + """ + Create and configure Analysis instance with environment setup + + This static method provides a convenient way to create an Analysis instance + while automatically configuring environment variables and runtime region. + + Example: + ```python + def my_analysis(context, scope): + context.log("Hello from analysis!") + + analysis = Analysis.use(my_analysis, {"token": "my-token"}) + ``` + """ + if params is None: + params = {"token": "unknown"} + + return Analysis(params).init(analysis) diff --git a/src/tagoio_sdk/modules/Analysis/Analysis_Type.py b/src/tagoio_sdk/modules/Analysis/Analysis_Type.py index d7e73b2..0c263f6 100644 --- a/src/tagoio_sdk/modules/Analysis/Analysis_Type.py +++ b/src/tagoio_sdk/modules/Analysis/Analysis_Type.py @@ -1 +1,57 @@ -AnalysisEnvironment = dict[str, str] +from typing import Any +from typing import Callable +from typing import Dict +from typing import List +from typing import Optional +from typing import TypedDict +from typing import Union + +from tagoio_sdk.regions import Regions +from tagoio_sdk.regions import RegionsObj + + +AnalysisFunction = Callable[[Any, Any], Any] + + +class AnalysisConstructorParams(TypedDict, total=False): + token: Optional[str] + """Analysis token for authentication""" + region: Optional[Union[Regions, RegionsObj]] + """Region configuration for the analysis""" + autostart: Optional[bool] + """ + Auto start analysis after instance the class. + If turned off, you can start analysis by calling [AnalysisInstance].start(). + Default: True + """ + load_env_on_process: Optional[bool] + """ + Load TagoIO Analysis envs on process environment. + + Warning: It's not safe to use on external analysis. + It will load all env on process, then if the external analysis receives multiple requests + simultaneously, it can mess up. + + Default: False + """ + + +AnalysisEnvironment = Dict[str, str] + + +AnalysisToken = str + + +AnalysisID = str + + +class TagoContext: + """ + TagoIO Analysis Context interface. + As current version of the SDK doesn't provide the full TagoContext interface. + """ + + token: AnalysisToken + analysis_id: AnalysisID + environment: List[AnalysisEnvironment] + log: Callable[..., None] diff --git a/src/tagoio_sdk/modules/Resources/Account.py b/src/tagoio_sdk/modules/Resources/Account.py index 928f2da..7b2d640 100644 --- a/src/tagoio_sdk/modules/Resources/Account.py +++ b/src/tagoio_sdk/modules/Resources/Account.py @@ -1,14 +1,35 @@ +from typing import Dict +from typing import Optional + +from tagoio_sdk.common.Common_Type import GenericToken from tagoio_sdk.common.tagoio_module import TagoIOModule +from tagoio_sdk.modules.Resources.Account_Types import AccountCreateInfo from tagoio_sdk.modules.Resources.Account_Types import AccountInfo +from tagoio_sdk.modules.Resources.Account_Types import LoginCredentials +from tagoio_sdk.modules.Resources.Account_Types import LoginResponse +from tagoio_sdk.modules.Resources.Account_Types import OTPType +from tagoio_sdk.modules.Resources.Account_Types import TokenCreateInfo from tagoio_sdk.modules.Utils.dateParser import dateParser +from tagoio_sdk.regions import Regions class Account(TagoIOModule): def info(self) -> AccountInfo: """ - Gets all account information. - """ + @description: + Gets all account information. + + @see: + https://api.docs.tago.io/#d1b06528-75e6-4dfc-80fb-9a553a26ea3b + @example: + If receive an error "Authorization Denied", check your account token permissions. + ```python + resources = Resources() + account_info = resources.account.info() + print(account_info) # {'id': 'account-id', 'name': 'My Account', ...} + ``` + """ result = self.doRequest( { "path": "/account", @@ -21,3 +42,426 @@ def info(self) -> AccountInfo: result["options"] = dateParser(result["options"], ["last_whats_new"]) return result + + def edit(self, accountObj: Dict) -> str: + """ + @description: + Edit current account information. + + @see: + https://api.docs.tago.io/#d1b06528-75e6-4dfc-80fb-9a553a26ea3b + + @example: + If receive an error "Authorization Denied", check your account token permissions. + ```python + resources = Resources() + result = resources.account.edit({ + "id": "account-id" + "name": "Updated Account Name", + "timezone": "America/New_York", + "company": "My Company" + }) + print(result) # Account Successfully Updated + ``` + """ + result = self.doRequest( + { + "path": "/account", + "method": "PUT", + "body": accountObj, + } + ) + + return result + + def delete(self) -> str: + """ + @description: + Delete current account. This action is irreversible and will remove all profiles and data. + + @see: + https://help.tago.io/portal/en/kb/articles/210-deleting-your-account + + @example: + If receive an error "Authorization Denied", check your account token permissions. + ```python + resources = Resources() + result = resources.account.delete() + print(result) # Account Successfully Deleted + ``` + """ + result = self.doRequest( + { + "path": "/account", + "method": "DELETE", + } + ) + + return result + + @staticmethod + def tokenCreate(tokenParams: TokenCreateInfo, region: Optional[Regions] = None) -> Dict[str, GenericToken]: + """ + @description: + Generates and retrieves a new token for the account. + + @see: + https://help.tago.io/portal/en/kb/articles/495-account-token + + @example: + ```python + from tagoio_sdk.modules.Resources.Account import Account + + token_result = Account.tokenCreate({ + "profile_id": "profile-id-123", + "email": "user@example.com", + "password": "your-password", + "pin_code": "123456", + "otp_type": "email", + "name": "My API Token" + }) + print(token_result["token"]) # your-new-token-123 + ``` + """ + result = TagoIOModule.doRequestAnonymous( + { + "path": "/account/profile/token", + "method": "POST", + "body": tokenParams, + }, + region, + ) + + return result + + @staticmethod + def login(credentials: LoginCredentials, region: Optional[Regions] = None) -> LoginResponse: + """ + @description: + Retrieve list of profiles for login and perform authentication. + + @see: + https://api.docs.tago.io/#3196249b-4aef-46ff-b5c3-f103b6f0bfbd + + @example: + ```python + from tagoio_sdk.modules.Resources.Account import Account + + login_result = Account.login({ + "email": "user@example.com", + "password": "your-password", + "otp_type": "email", + "pin_code": "123456" + }) + print(login_result) # {'type': 'user', 'id': '...', 'profiles': [...]} + ``` + """ + result = TagoIOModule.doRequestAnonymous( + { + "path": "/account/login", + "method": "POST", + "body": credentials, + }, + region, + ) + + return result + + @staticmethod + def passwordRecover(email: str, region: Optional[Regions] = None) -> str: + """ + @description: + Send password recovery email to the specified address. + + @see: + https://help.tago.io/portal/en/kb/articles/209-resetting-my-password + + @example: + ```python + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.passwordRecover("user@example.com") + print(result) # Email sent successfully + ``` + """ + result = TagoIOModule.doRequestAnonymous( + { + "path": f"/account/passwordreset/{email}", + "method": "GET", + }, + region, + ) + + return result + + def passwordChange(self, password: str) -> str: + """ + @description: + Change account password for the authenticated user. + + @see: + https://help.tago.io/portal/en/kb/articles/209-resetting-my-password + + @example: + If receive an error "Authorization Denied", check your account token permissions. + ```python + resources = Resources() + result = resources.account.passwordChange("new-secure-password") + print(result) # Password changed successfully + ``` + """ + result = self.doRequest( + { + "path": "/account/passwordreset", + "method": "POST", + "body": {"password": password}, + } + ) + + return result + + @staticmethod + def create(createParams: AccountCreateInfo, region: Optional[Regions] = None) -> str: + """ + @description: + Create a new TagoIO account. + + @example: + ```python + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.create({ + "name": "John Doe", + "email": "john@example.com", + "password": "secure-password", + "cpassword": "secure-password", + "timezone": "America/New_York", + "company": "My Company", + "newsletter": False + }) + print(result) # Account created successfully + ``` + """ + result = TagoIOModule.doRequestAnonymous( + { + "path": "/account", + "method": "POST", + "body": createParams, + }, + region, + ) + + return result + + @staticmethod + def resendConfirmation(email: str, region: Optional[Regions] = None) -> str: + """ + @description: + Re-send confirmation account email to the specified address. + + @example: + ```python + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.resendConfirmation("user@example.com") + print(result) # Confirmation email sent + ``` + """ + result = TagoIOModule.doRequestAnonymous( + { + "path": f"/account/resend_confirmation/{email}", + "method": "GET", + }, + region, + ) + + return result + + @staticmethod + def confirmAccount(token: GenericToken, region: Optional[Regions] = None) -> str: + """ + @description: + Confirm account creation using the token sent via email. + + @example: + ```python + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.confirmAccount("confirmation-token-123") + print(result) # Account confirmed successfully + ``` + """ + result = TagoIOModule.doRequestAnonymous( + { + "path": f"/account/confirm/{token}", + "method": "GET", + }, + region, + ) + + return result + + @staticmethod + def requestLoginPINCode(credentials: Dict[str, str], typeOTP: OTPType, region: Optional[Regions] = None) -> str: + """ + @description: + Request the PIN Code for a given OTP Type (authenticator, sms, or email). + + @see: + https://help.tago.io/portal/en/kb/articles/526-two-factor-authentication + + @example: + ```python + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.requestLoginPINCode( + {"email": "user@example.com", "password": "your-password"}, + "email" + ) + print(result) # PIN code sent + ``` + """ + body = {**credentials, "otp_type": typeOTP} + result = TagoIOModule.doRequestAnonymous( + { + "path": "/account/login/otp", + "method": "POST", + "body": body, + }, + region, + ) + + return result + + def enableOTP(self, credentials: Dict[str, str], typeOTP: OTPType) -> str: + """ + @description: + Enable OTP (One-Time Password) for a given OTP Type (authenticator, sms, or email). + You will be requested to confirm the operation with a pin code. + + @see: + https://help.tago.io/portal/en/kb/articles/526-two-factor-authentication + + @example: + If receive an error "Authorization Denied", check your account token permissions. + ```python + resources = Resources() + result = resources.account.enableOTP( + {"email": "user@example.com", "password": "your-password"}, + "email" + ) + print(result) # OTP enabled, confirmation required + ``` + """ + result = self.doRequest( + { + "path": f"/account/otp/{typeOTP}/enable", + "method": "POST", + "body": credentials, + } + ) + + return result + + def disableOTP(self, credentials: Dict[str, str], typeOTP: OTPType) -> str: + """ + @description: + Disable OTP (One-Time Password) for a given OTP Type (authenticator, sms, or email). + + @see: + https://help.tago.io/portal/en/kb/articles/526-two-factor-authentication + + @example: + If receive an error "Authorization Denied", check your account token permissions. + ```python + resources = Resources() + result = resources.account.disableOTP( + {"email": "user@example.com", "password": "your-password"}, + "authenticator" + ) + print(result) # OTP disabled successfully + ``` + """ + result = self.doRequest( + { + "path": f"/account/otp/{typeOTP}/disable", + "method": "POST", + "body": credentials, + } + ) + + return result + + def confirmOTP(self, pinCode: str, typeOTP: OTPType) -> str: + """ + @description: + Confirm OTP enabling process for a given OTP Type (authenticator, sms, or email). + + @see: + https://help.tago.io/portal/en/kb/articles/526-two-factor-authentication + + @example: + If receive an error "Authorization Denied", check your account token permissions. + ```python + resources = Resources() + result = resources.account.confirmOTP("123456", "email") + print(result) # OTP confirmed successfully + ``` + """ + result = self.doRequest( + { + "path": f"/account/otp/{typeOTP}/confirm", + "method": "POST", + "body": {"pin_code": pinCode}, + } + ) + + return result + + @staticmethod + def acceptTeamInvitation(token: str, region: Optional[Regions] = None) -> str: + """ + @description: + Accept a team member invitation to become a profile's team member. + + @example: + ```python + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.acceptTeamInvitation("invitation-token-123") + print(result) # Invitation accepted + ``` + """ + result = TagoIOModule.doRequestAnonymous( + { + "path": f"/profile/team/accept/{token}", + "method": "GET", + }, + region, + ) + + return result + + @staticmethod + def declineTeamInvitation(token: str, region: Optional[Regions] = None) -> str: + """ + @description: + Decline a team member invitation to become a profile's team member. + + @example: + ```python + from tagoio_sdk.modules.Resources.Account import Account + + result = Account.declineTeamInvitation("invitation-token-123") + print(result) # Invitation declined + ``` + """ + result = TagoIOModule.doRequestAnonymous( + { + "path": f"/profile/team/decline/{token}", + "method": "GET", + }, + region, + ) + + return result diff --git a/src/tagoio_sdk/modules/Resources/Account_Types.py b/src/tagoio_sdk/modules/Resources/Account_Types.py index 790ffa8..7782cf4 100644 --- a/src/tagoio_sdk/modules/Resources/Account_Types.py +++ b/src/tagoio_sdk/modules/Resources/Account_Types.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Literal from typing import Optional from typing import TypedDict @@ -40,3 +41,48 @@ class AccountInfo(TypedDict): created_at: datetime updated_at: datetime otp: Optional[AccountOpt] + + +class AccountCreateInfo(TypedDict, total=False): + name: str + email: str + password: str + cpassword: str + country: Optional[str] + timezone: str + company: Optional[str] + newsletter: Optional[bool] + developer: Optional[bool] + + +OTPType = Literal["sms", "email", "authenticator"] + + +class TokenCreateInfo(TypedDict): + profile_id: GenericID + email: str + password: str + pin_code: str + otp_type: OTPType + name: str + + +class LoginCredentials(TypedDict): + email: str + password: str + otp_type: OTPType + pin_code: str + + +class ProfileListInfoForLogin(TypedDict): + id: GenericID + name: str + + +class LoginResponse(TypedDict): + type: str + id: GenericID + email: str + company: str + name: str + profiles: list[ProfileListInfoForLogin] diff --git a/src/tagoio_sdk/modules/Resources/Analyses.py b/src/tagoio_sdk/modules/Resources/Analyses.py index af19462..8043c3f 100644 --- a/src/tagoio_sdk/modules/Resources/Analyses.py +++ b/src/tagoio_sdk/modules/Resources/Analyses.py @@ -4,6 +4,8 @@ from typing import Literal from typing import Optional +import requests + from tagoio_sdk.common.Common_Type import GenericID from tagoio_sdk.common.Common_Type import GenericToken from tagoio_sdk.common.tagoio_module import TagoIOModule @@ -12,26 +14,42 @@ from tagoio_sdk.modules.Resources.Analysis_Types import AnalysisListItem from tagoio_sdk.modules.Resources.Analysis_Types import AnalysisQuery from tagoio_sdk.modules.Resources.Analysis_Types import ScriptFile +from tagoio_sdk.modules.Resources.Analysis_Types import SnippetRuntime +from tagoio_sdk.modules.Resources.Analysis_Types import SnippetsListResponse from tagoio_sdk.modules.Utils.dateParser import dateParser from tagoio_sdk.modules.Utils.dateParser import dateParserList +# Base URL for TagoIO analysis snippets repository +SNIPPETS_BASE_URL = "https://snippets.tago.io" + + class Analyses(TagoIOModule): def list(self, queryObj: Optional[AnalysisQuery] = None) -> List[AnalysisListItem]: """ - Retrieves a list with all analyses from the account + @description: + Lists all analyses from the application with pagination support. + Use this to retrieve and manage analyses in your application. - :default: + @see: + https://docs.tago.io/docs/tagoio/analysis/ Analysis - queryObj: { + @example: + If receive an error "Authorization Denied", check policy **Analysis** / **Access** in Access Management. + ```python + resources = Resources() + list_result = resources.analyses.list({ "page": 1, "fields": ["id", "name"], - "filter": {}, - "amount": 20, - "orderBy": ["name", "asc"], - } + "amount": 10, + "orderBy": ["name", "asc"] + }) + print(list_result) # [{'id': 'analysis-id-123', 'name': 'Analysis Test', ...}] + ``` - :param AnalysisQuery queryObj: Search query params + :param AnalysisQuery queryObj: Search query params (optional) + :return: List of analysis items matching the query + :rtype: List[AnalysisListItem] """ queryObj = queryObj or {} orderBy = f"{queryObj.get('orderBy', ['name', 'asc'])[0]},{queryObj.get('orderBy', ['name', 'asc'])[1]}" @@ -55,9 +73,27 @@ def list(self, queryObj: Optional[AnalysisQuery] = None) -> List[AnalysisListIte def create(self, analysisObj: AnalysisCreateInfo) -> Dict[str, GenericID | GenericToken]: """ - Create a new analyze + @description: + Creates a new analysis in your application. + + @see: + https://help.tago.io/portal/en/kb/articles/120-creating-analysis Creating Analysis + + @example: + If receive an error "Authorization Denied", check policy **Analysis** / **Create** in Access Management. + ```python + resources = Resources() + new_analysis = resources.analyses.create({ + "name": "My Analysis", + "runtime": "python", + "tags": [{"key": "type", "value": "data-processing"}] + }) + print(new_analysis["id"], new_analysis["token"]) # analysis-id-123, analysis-token-123 + ``` - :param AnalysisCreateInfo analysisObj: Data object to create new TagoIO Analyze + :param AnalysisCreateInfo analysisObj: Data object to create new TagoIO Analysis + :return: Dictionary with the new analysis ID and token + :rtype: Dict[str, GenericID | GenericToken] """ result = self.doRequest( { @@ -70,10 +106,27 @@ def create(self, analysisObj: AnalysisCreateInfo) -> Dict[str, GenericID | Gener def edit(self, analysisID: GenericID, analysisObj: AnalysisInfo) -> str: """ - Modify any property of the analyze + @description: + Modifies an existing analysis. - :param GenericID analysisID: Analyze identification - :param Partial[AnalysisInfo] analysisObj: Analyze Object with data to replace + @see: + https://docs.tago.io/docs/tagoio/analysis/ Analysis + + @example: + If receive an error "Authorization Denied", check policy **Analysis** / **Create** in Access Management. + ```python + resources = Resources() + result = resources.analyses.edit("analysis-id-123", { + "name": "Updated Analysis", + "active": False + }) + print(result) # Successfully Updated + ``` + + :param GenericID analysisID: Analysis identification + :param AnalysisInfo analysisObj: Analysis object with data to replace + :return: Success message + :rtype: str """ result = self.doRequest( { @@ -86,9 +139,23 @@ def edit(self, analysisID: GenericID, analysisObj: AnalysisInfo) -> str: def delete(self, analysisID: GenericID) -> str: """ - Deletes an analyze from the account + @description: + Deletes an analysis from your application. + + @see: + https://docs.tago.io/docs/tagoio/analysis/ Analysis + + @example: + If receive an error "Authorization Denied", check policy **Analysis** / **Delete** in Access Management. + ```python + resources = Resources() + result = resources.analyses.delete("analysis-id-123") + print(result) # Successfully Removed + ``` - :param GenericID analysisID: Analyze identification + :param GenericID analysisID: Analysis identification + :return: Success message + :rtype: str """ result = self.doRequest( { @@ -100,9 +167,23 @@ def delete(self, analysisID: GenericID) -> str: def info(self, analysisID: GenericID) -> AnalysisInfo: """ - Gets information about the analyze + @description: + Retrieves detailed information about a specific analysis. + + @see: + https://docs.tago.io/docs/tagoio/analysis/ Analysis + + @example: + If receive an error "Authorization Denied", check policy **Analysis** / **Access** in Access Management. + ```python + resources = Resources() + analysis_info = resources.analyses.info("analysis-id-123") + print(analysis_info) # {'id': 'analysis-id-123', 'name': 'My Analysis', ...} + ``` - :param GenericID analysisID: Analyze identification + :param GenericID analysisID: Analysis identification + :return: Detailed analysis information + :rtype: AnalysisInfo """ result = self.doRequest( { @@ -115,10 +196,24 @@ def info(self, analysisID: GenericID) -> AnalysisInfo: def run(self, analysisID: GenericID, scopeObj: Optional[Dict[str, Any]] = None) -> Dict[str, GenericToken]: """ - Force analyze to run + @description: + Executes an analysis with optional scope parameters. + + @see: + https://docs.tago.io/docs/tagoio/analysis/ Analysis + + @example: + If receive an error "Authorization Denied", check policy **Analysis** / **Run Analysis** in Access Management. + ```python + resources = Resources() + result = resources.analyses.run("analysis-id-123", {"environment": "production"}) + print(result["analysis_token"]) # analysis-token-123 + ``` - :param GenericID analysisID: Analyze identification + :param GenericID analysisID: Analysis identification :param Optional[Dict[str, Any]] scopeObj: Simulate scope for analysis + :return: Dictionary containing the analysis token + :rtype: Dict[str, GenericToken] """ result = self.doRequest( { @@ -131,9 +226,23 @@ def run(self, analysisID: GenericID, scopeObj: Optional[Dict[str, Any]] = None) def tokenGenerate(self, analysisID: GenericID) -> Dict[str, str]: """ - Generate a new token for the analysis + @description: + Generates a new token for the analysis. + This is only allowed when the analysis is running in external mode. + + @see: + https://docs.tago.io/docs/tagoio/analysis/ Analysis + + @example: + ```python + resources = Resources() + token = resources.analyses.tokenGenerate("analysis-id-123") + print(token["analysis_token"]) # analysis-token-123 + ``` - :param GenericID analysisID: Analyze identification + :param GenericID analysisID: Analysis identification + :return: Dictionary containing the new analysis token + :rtype: Dict[str, str] """ result = self.doRequest( { @@ -145,10 +254,29 @@ def tokenGenerate(self, analysisID: GenericID) -> Dict[str, str]: def uploadScript(self, analysisID: GenericID, fileObj: ScriptFile) -> str: """ - Upload a file (base64) to Analysis. Automatically erase the old one + @description: + Uploads a script file to an analysis. + The file content must be base64-encoded. This automatically replaces the old script. + + @see: + https://docs.tago.io/docs/tagoio/analysis/ Analysis - :param GenericID analysisID: Analyze identification - :param ScriptFile fileObj: Object with name, language and content of the file + @example: + If receive an error "Authorization Denied", check policy **Analysis** / **Upload Analysis Script** in Access Management. + ```python + resources = Resources() + result = resources.analyses.uploadScript("analysis-id-123", { + "name": "script.py", + "content": "base64-encoded-content", + "language": "python" + }) + print(result) # Successfully Uploaded + ``` + + :param GenericID analysisID: Analysis identification + :param ScriptFile fileObj: Object with name, language and content (base64) of the file + :return: Success message + :rtype: str """ result = self.doRequest( { @@ -163,12 +291,32 @@ def uploadScript(self, analysisID: GenericID, fileObj: ScriptFile) -> str: ) return result - def downloadScript(self, analysisID: GenericID, options: Optional[Dict[Literal["version"], int]] = None) -> Dict: + def downloadScript( + self, + analysisID: GenericID, + options: Optional[Dict[Literal["version"], int]] = None, + ) -> Dict: """ - Get a url to download the analysis. If `version` is specified in `options`, downloads a specific version. + @description: + Gets a download URL for the analysis script. + If version is specified in options, downloads a specific version. + + @see: + https://docs.tago.io/docs/tagoio/analysis/ Analysis + + @example: + If receive an error "Authorization Denied", check policy **Analysis** / **Download Analysis Script** in Access Management. + ```python + resources = Resources() + download = resources.analyses.downloadScript("analysis-id-123", {"version": 1}) + print(download["url"]) # https://... + print(download["expire_at"]) # 2025-01-13T... + ``` :param GenericID analysisID: Analysis identification - :param Optional[Dict[str, int]] options: Options for the Analysis script to download + :param Optional[Dict[str, int]] options: Options for the Analysis script to download (e.g., {"version": 1}) + :return: Dictionary with download URL, size information, and expiration date + :rtype: Dict """ version = options.get("version") if options else None @@ -181,3 +329,73 @@ def downloadScript(self, analysisID: GenericID, options: Optional[Dict[Literal[" ) result = dateParser(result, ["expire_at"]) return result + + def listSnippets(self, runtime: SnippetRuntime) -> SnippetsListResponse: + """ + @description: + Get all available snippets for a specific runtime environment. + Fetches analysis code snippets from the public TagoIO snippets repository. + + @see: + https://help.tago.io/portal/en/kb/articles/64-script-examples Script Examples + https://help.tago.io/portal/en/kb/articles/104-script-editor Script Editor + + @example: + ```python + resources = Resources() + python_snippets = resources.analyses.listSnippets("python-rt2025") + + # Print all snippet titles + for snippet in python_snippets["snippets"]: + print(f"{snippet['title']}: {snippet['description']}") + ``` + + :param SnippetRuntime runtime: The runtime environment to get snippets for + :return: Snippets metadata including runtime, schema version, and list of available snippets + :rtype: SnippetsListResponse + """ + url = f"{SNIPPETS_BASE_URL}/{runtime}.json" + + try: + response = requests.get(url, headers={"Accept": "*/*"}, timeout=10) + response.raise_for_status() + return response.json() + except requests.exceptions.RequestException as e: + raise RuntimeError(f"Failed to fetch snippets: {e}") from e + + def getSnippetFile(self, runtime: SnippetRuntime, filename: str) -> str: + """ + @description: + Get the raw source code content of a specific snippet file. + Fetches the actual code content from the TagoIO snippets repository. + + @see: + https://help.tago.io/portal/en/kb/articles/64-script-examples Script Examples + https://help.tago.io/portal/en/kb/articles/104-script-editor Script Editor + + @example: + ```python + resources = Resources() + + # Get TypeScript code for console example + code = resources.analyses.getSnippetFile("deno-rt2025", "console.ts") + print(code) + + # Get Python code for data processing + python_code = resources.analyses.getSnippetFile("python-rt2025", "avg-min-max.py") + print(python_code) + ``` + + :param SnippetRuntime runtime: The runtime environment the snippet belongs to + :param str filename: The filename of the snippet to retrieve + :return: Raw file content as string + :rtype: str + """ + url = f"{SNIPPETS_BASE_URL}/{runtime}/{filename}" + + try: + response = requests.get(url, headers={"Accept": "*/*"}, timeout=10) + response.raise_for_status() + return response.text + except requests.exceptions.RequestException as e: + raise RuntimeError(f"Failed to fetch snippet file: {e}") from e diff --git a/src/tagoio_sdk/modules/Resources/Analysis_Types.py b/src/tagoio_sdk/modules/Resources/Analysis_Types.py index 3ab903d..4d32035 100644 --- a/src/tagoio_sdk/modules/Resources/Analysis_Types.py +++ b/src/tagoio_sdk/modules/Resources/Analysis_Types.py @@ -44,7 +44,11 @@ class AnalysisInfo(AnalysisCreateInfo): class AnalysisQuery(Query): - fields: Optional[List[Literal["name", "active", "run_on", "last_run", "created_at", "updated_at"]]] + fields: Optional[ + List[ + Literal["name", "active", "run_on", "last_run", "created_at", "updated_at"] + ] + ] class AnalysisListItem(TypedDict, total=False): @@ -57,3 +61,41 @@ class AnalysisListItem(TypedDict, total=False): updated_at: Optional[str] locked_at: Optional[str] console: Optional[List[str]] + + +SnippetRuntime = Literal[ + "node-legacy", "python-legacy", "node-rt2025", "python-rt2025", "deno-rt2025" +] +"""Available runtime environments for snippets""" + + +class SnippetItem(TypedDict): + """Individual snippet metadata""" + + id: str + """Unique identifier for the snippet""" + title: str + """Human-readable title""" + description: str + """Description of what the snippet does""" + language: str + """Programming language (typescript, javascript, python)""" + tags: List[str] + """Array of tags for categorization""" + filename: str + """Filename of the snippet""" + file_path: str + """Full path to the file in the runtime directory""" + + +class SnippetsListResponse(TypedDict): + """API response containing all snippets metadata for a runtime""" + + runtime: SnippetRuntime + """Runtime environment identifier""" + schema_version: int + """Schema version for the API response format""" + generated_at: str + """ISO timestamp when the response was generated""" + snippets: List[SnippetItem] + """Array of all available snippets for this runtime""" diff --git a/src/tagoio_sdk/regions.py b/src/tagoio_sdk/regions.py index 7df30f2..128d77b 100644 --- a/src/tagoio_sdk/regions.py +++ b/src/tagoio_sdk/regions.py @@ -1,55 +1,127 @@ import os -from contextlib import suppress from typing import Literal from typing import Optional from typing import TypedDict +from typing import Union -class RegionDefinition(TypedDict): +class RegionsObjApi(TypedDict): + """Region configuration with API/SSE endpoints.""" + api: str - realtime: str sse: str -# noRegionWarning = False +class RegionsObjTDeploy(TypedDict): + """Region configuration with TagoIO Deploy Project ID.""" + + tdeploy: str + + +RegionsObj = Union[RegionsObjApi, RegionsObjTDeploy] +"""Region configuration object (either API/SSE pair or TDeploy)""" + +Regions = Literal["us-e1", "eu-w1", "env"] +"""Supported TagoIO regions""" -regionsDefinition = { - "usa-1": { +# Runtime region cache +runtimeRegion: Optional[RegionsObj] = None + +# Object of Regions Definition +regionsDefinition: dict[str, Optional[RegionsObjApi]] = { + "us-e1": { "api": "https://api.tago.io", - "realtime": "wss://realtime.tago.io", "sse": "https://sse.tago.io/events", }, - "env": None, # ? process object should be on trycatch. + "eu-w1": { + "api": "https://api.eu-w1.tago.io", + "sse": "https://sse.eu-w1.tago.io/events", + }, + "env": None, # process object should be on trycatch } -Regions = Literal["usa-1", "env"] +def getConnectionURI(region: Optional[Union[Regions, RegionsObj]] = None) -> RegionsObjApi: + """ + Get connection URI for API and SSE. + + Args: + region: Region identifier or configuration object + + Returns: + Region configuration with API and SSE endpoints + + Raises: + ReferenceError: If invalid region is specified + """ + global runtimeRegion -def getConnectionURI(region: Optional[Regions]) -> RegionDefinition: - value = None - with suppress(KeyError): - value = regionsDefinition[region] + # Handle tdeploy in RegionsObj - takes precedence + if isinstance(region, dict) and "tdeploy" in region: + tdeploy = region["tdeploy"].strip() + if tdeploy: + return { + "api": f"https://api.{tdeploy}.tagoio.net", + "sse": f"https://sse.{tdeploy}.tagoio.net/events", + } + + normalized_region = region + if isinstance(normalized_region, str) and normalized_region == "usa-1": + normalized_region = "us-e1" + + value: Optional[RegionsObjApi] = None + if isinstance(normalized_region, str): + value = regionsDefinition.get(normalized_region) + elif isinstance(normalized_region, dict): + # If it's already a RegionsObj with api/sse, use it + if "api" in normalized_region and "sse" in normalized_region: + value = normalized_region if value is not None: return value + if runtimeRegion is not None: + return runtimeRegion + if region is not None and region != "env": - raise Exception(f"> TagoIO-SDK: Invalid region {region}.") + raise ReferenceError(f"> TagoIO-SDK: Invalid region {region}.") try: api = os.environ.get("TAGOIO_API") - realtime = os.environ.get("TAGOIO_REALTIME") sse = os.environ.get("TAGOIO_SSE") if not api and region != "env": raise Exception("Invalid Env") - return {"api": api, "realtime": realtime, "sse": sse} + return {"api": api or "", "sse": sse or ""} except Exception: - # global noRegionWarning - # if noRegionWarning is False: + # if not noRegionWarning: # print("> TagoIO-SDK: No region or env defined, using fallback as usa-1.") # noRegionWarning = True - return regionsDefinition["usa-1"] + return regionsDefinition["us-e1"] + + +def setRuntimeRegion(region: RegionsObj) -> None: + """ + Set region in-memory to be inherited by other modules when set in the Analysis runtime + with `Analysis.use()`. + + Example: + ```python + def my_analysis(context, scope): + # this uses the region defined through `use` + resources = Resources({"token": token}) + + # it's still possible to override if needed + europe_resources = Resources({"token": token, "region": "eu-w1"}) + + Analysis.use(my_analysis, {"region": "us-e1"}) + ``` + + Args: + region: Region configuration object + """ + global runtimeRegion + runtimeRegion = region diff --git a/tests/Regions/test_tdeploy.py b/tests/Regions/test_tdeploy.py new file mode 100644 index 0000000..166bf03 --- /dev/null +++ b/tests/Regions/test_tdeploy.py @@ -0,0 +1,92 @@ +from tagoio_sdk.regions import getConnectionURI + +"""Test suite for TagoIO Deploy (tdeploy) Region Support""" + + +def testShouldGenerateCorrectEndpointsForTdeployRegion(): + """Should generate correct endpoints for tdeploy region""" + tdeploy = "68951c0e023862b2aea00f3f" + region = {"tdeploy": tdeploy} + + result = getConnectionURI(region) + + assert result["api"] == f"https://api.{tdeploy}.tagoio.net" + assert result["sse"] == f"https://sse.{tdeploy}.tagoio.net/events" + + +def testShouldPrioritizeTdeployOverOtherFields(): + """Should prioritize tdeploy over other fields when both are provided""" + tdeploy = "68951c0e023862b2aea00f3f" + # mixing api/sse with tdeploy is no longer allowed by types; + # pass only tdeploy and ensure correct priority handling remains + region = {"tdeploy": tdeploy} + + result = getConnectionURI(region) + + assert result["api"] == f"https://api.{tdeploy}.tagoio.net" + assert result["sse"] == f"https://sse.{tdeploy}.tagoio.net/events" + + +def testShouldTrimWhitespaceFromTdeployValue(): + """Should trim whitespace from tdeploy value""" + tdeploy = " 68951c0e023862b2aea00f3f " + region = {"tdeploy": tdeploy} + + result = getConnectionURI(region) + + assert result["api"] == "https://api.68951c0e023862b2aea00f3f.tagoio.net" + assert result["sse"] == "https://sse.68951c0e023862b2aea00f3f.tagoio.net/events" + + +def testShouldFallbackToStandardBehaviorWhenTdeployIsEmpty(): + """Should fallback to standard behavior when tdeploy is empty""" + region = { + "tdeploy": "", + "api": "https://custom-api.example.com", + "sse": "https://custom-sse.example.com", + } + + # Empty tdeploy should fallback to api/sse fields + result = getConnectionURI(region) + + assert result["api"] == "https://custom-api.example.com" + assert result["sse"] == "https://custom-sse.example.com" + + +def testShouldFallbackToStandardBehaviorWhenTdeployIsWhitespaceOnly(): + """Should fallback to standard behavior when tdeploy is whitespace only""" + region = { + "tdeploy": " ", + "api": "https://custom-api.example.com", + "sse": "https://custom-sse.example.com", + } + + # Whitespace-only tdeploy should fallback to api/sse fields + result = getConnectionURI(region) + + assert result["api"] == "https://custom-api.example.com" + assert result["sse"] == "https://custom-sse.example.com" + + +def testShouldMaintainBackwardCompatibilityWithExistingRegions(): + """Should maintain backward compatibility with existing regions""" + result1 = getConnectionURI("us-e1") + assert result1["api"] == "https://api.tago.io" + assert result1["sse"] == "https://sse.tago.io/events" + + result2 = getConnectionURI("eu-w1") + assert result2["api"] == "https://api.eu-w1.tago.io" + assert result2["sse"] == "https://sse.eu-w1.tago.io/events" + + +def testShouldMaintainBackwardCompatibilityWithCustomRegionsObj(): + """Should maintain backward compatibility with custom RegionsObj""" + customRegion = { + "api": "https://my-api.com", + "sse": "https://my-sse.com", + } + + result = getConnectionURI(customRegion) + + assert result["api"] == "https://my-api.com" + assert result["sse"] == "https://my-sse.com" diff --git a/tests/Resources/test_account.py b/tests/Resources/test_account.py new file mode 100644 index 0000000..fb54a29 --- /dev/null +++ b/tests/Resources/test_account.py @@ -0,0 +1,332 @@ +import os + +from requests_mock.mocker import Mocker + +from tagoio_sdk.modules.Resources.Account import Account +from tagoio_sdk.modules.Resources.Resources import Resources + + +os.environ["T_ANALYSIS_TOKEN"] = "your_token_value" + + +def mockAccountInfo() -> dict: + return { + "status": True, + "result": { + "active": True, + "blocked": False, + "created_at": "2023-02-21T15:17:35.759Z", + "email": "email@test.com", + "id": "test_id", + "language": "en", + "last_login": "2023-03-07T01:43:45.950Z", + "name": "Tester Test", + "newsletter": False, + "options": { + "last_whats_new": "2022-06-16T15:00:00.001Z", + "decimal_separator": ".", + "user_view_welcome": True, + "thousand_separator": ",", + }, + "phone": None, + "plan": "free", + "send_invoice": False, + "stripe_id": "test_stripe_id", + "timezone": "America/Sao_Paulo", + "type": "user", + "updated_at": "2023-03-24T17:43:47.916Z", + "otp": {"authenticator": False, "sms": False, "email": True}, + "company": "tago.io", + }, + } + + +def mockLoginResponse() -> dict: + return { + "status": True, + "result": { + "type": "user", + "id": "612ea05e3cc078001371895110", + "email": "example@mail.com", + "company": "companyname", + "name": "Your Name", + "profiles": [ + { + "id": "612ea05e3cc078001371895111", + "name": "profilename", + } + ], + }, + } + + +def mockTokenCreateResponse() -> dict: + return { + "status": True, + "result": {"token": "new-generated-token-123"}, + } + + +def testAccountMethodInfo(requests_mock: Mocker) -> None: + """Test info method of Account class.""" + mock_response = mockAccountInfo() + requests_mock.get("https://api.tago.io/account", json=mock_response) + + resources = Resources({"token": "your_token_value"}) + response = resources.account.info() + + assert response["email"] == "email@test.com" + assert response["name"] == "Tester Test" + assert response["id"] == "test_id" + assert isinstance(response, dict) + + +def testAccountMethodEdit(requests_mock: Mocker) -> None: + """Test edit method of Account class.""" + mock_response = { + "status": True, + "result": "Account Successfully Updated", + } + + requests_mock.put("https://api.tago.io/account", json=mock_response) + + resources = Resources({"token": "your_token_value"}) + + account_data = { + "name": "Updated Account Name", + "timezone": "America/New_York", + "company": "My Company", + } + + result = resources.account.edit(account_data) + + assert result == "Account Successfully Updated" + + +def testAccountMethodDelete(requests_mock: Mocker) -> None: + """Test delete method of Account class.""" + mock_response = { + "status": True, + "result": "Account Successfully Deleted", + } + + requests_mock.delete("https://api.tago.io/account", json=mock_response) + + resources = Resources({"token": "your_token_value"}) + + result = resources.account.delete() + + assert result == "Account Successfully Deleted" + + +def testAccountMethodPasswordChange(requests_mock: Mocker) -> None: + """Test passwordChange method of Account class.""" + mock_response = { + "status": True, + "result": "Password changed successfully", + } + + requests_mock.post("https://api.tago.io/account/passwordreset", json=mock_response) + + resources = Resources({"token": "your_token_value"}) + + result = resources.account.passwordChange("new-secure-password") + + assert result == "Password changed successfully" + + +def testAccountMethodEnableOTP(requests_mock: Mocker) -> None: + """Test enableOTP method of Account class.""" + mock_response = { + "status": True, + "result": "OTP enabled, confirmation required", + } + + requests_mock.post("https://api.tago.io/account/otp/email/enable", json=mock_response) + + resources = Resources({"token": "your_token_value"}) + + result = resources.account.enableOTP({"email": "user@example.com", "password": "password"}, "email") + + assert result == "OTP enabled, confirmation required" + + +def testAccountMethodDisableOTP(requests_mock: Mocker) -> None: + """Test disableOTP method of Account class.""" + mock_response = { + "status": True, + "result": "OTP disabled successfully", + } + + requests_mock.post("https://api.tago.io/account/otp/authenticator/disable", json=mock_response) + + resources = Resources({"token": "your_token_value"}) + + result = resources.account.disableOTP({"email": "user@example.com", "password": "password"}, "authenticator") + + assert result == "OTP disabled successfully" + + +def testAccountMethodConfirmOTP(requests_mock: Mocker) -> None: + """Test confirmOTP method of Account class.""" + mock_response = { + "status": True, + "result": "OTP confirmed successfully", + } + + requests_mock.post("https://api.tago.io/account/otp/email/confirm", json=mock_response) + + resources = Resources({"token": "your_token_value"}) + + result = resources.account.confirmOTP("123456", "email") + + assert result == "OTP confirmed successfully" + + +def testAccountStaticMethodTokenCreate(requests_mock: Mocker) -> None: + """Test tokenCreate static method of Account class.""" + mock_response = mockTokenCreateResponse() + requests_mock.post("https://api.tago.io/account/profile/token", json=mock_response) + + token_params = { + "profile_id": "profile-id-123", + "email": "user@example.com", + "password": "your-password", + "pin_code": "123456", + "otp_type": "email", + "name": "My API Token", + } + + result = Account.tokenCreate(token_params) + + assert result["token"] == "new-generated-token-123" + + +def testAccountStaticMethodLogin(requests_mock: Mocker) -> None: + """Test login static method of Account class.""" + mock_response = mockLoginResponse() + requests_mock.post("https://api.tago.io/account/login", json=mock_response) + + credentials = { + "email": "user@example.com", + "password": "your-password", + "otp_type": "email", + "pin_code": "123456", + } + + result = Account.login(credentials) + + assert result["email"] == "example@mail.com" + assert result["type"] == "user" + assert len(result["profiles"]) == 1 + assert result["profiles"][0]["name"] == "profilename" + + +def testAccountStaticMethodPasswordRecover(requests_mock: Mocker) -> None: + """Test passwordRecover static method of Account class.""" + mock_response = { + "status": True, + "result": "Email sent successfully", + } + + requests_mock.get("https://api.tago.io/account/passwordreset/user@example.com", json=mock_response) + + result = Account.passwordRecover("user@example.com") + + assert result == "Email sent successfully" + + +def testAccountStaticMethodCreate(requests_mock: Mocker) -> None: + """Test create static method of Account class.""" + mock_response = { + "status": True, + "result": "Account created successfully", + } + + requests_mock.post("https://api.tago.io/account", json=mock_response) + + create_params = { + "name": "John Doe", + "email": "john@example.com", + "password": "secure-password", + "cpassword": "secure-password", + "timezone": "America/New_York", + "company": "My Company", + "newsletter": False, + } + + result = Account.create(create_params) + + assert result == "Account created successfully" + + +def testAccountStaticMethodResendConfirmation(requests_mock: Mocker) -> None: + """Test resendConfirmation static method of Account class.""" + mock_response = { + "status": True, + "result": "Confirmation email sent", + } + + requests_mock.get("https://api.tago.io/account/resend_confirmation/user@example.com", json=mock_response) + + result = Account.resendConfirmation("user@example.com") + + assert result == "Confirmation email sent" + + +def testAccountStaticMethodConfirmAccount(requests_mock: Mocker) -> None: + """Test confirmAccount static method of Account class.""" + mock_response = { + "status": True, + "result": "Account confirmed successfully", + } + + requests_mock.get("https://api.tago.io/account/confirm/confirmation-token-123", json=mock_response) + + result = Account.confirmAccount("confirmation-token-123") + + assert result == "Account confirmed successfully" + + +def testAccountStaticMethodRequestLoginPINCode(requests_mock: Mocker) -> None: + """Test requestLoginPINCode static method of Account class.""" + mock_response = { + "status": True, + "result": "PIN code sent", + } + + requests_mock.post("https://api.tago.io/account/login/otp", json=mock_response) + + credentials = {"email": "user@example.com", "password": "your-password"} + + result = Account.requestLoginPINCode(credentials, "email") + + assert result == "PIN code sent" + + +def testAccountStaticMethodAcceptTeamInvitation(requests_mock: Mocker) -> None: + """Test acceptTeamInvitation static method of Account class.""" + mock_response = { + "status": True, + "result": "Invitation accepted", + } + + requests_mock.get("https://api.tago.io/profile/team/accept/invitation-token-123", json=mock_response) + + result = Account.acceptTeamInvitation("invitation-token-123") + + assert result == "Invitation accepted" + + +def testAccountStaticMethodDeclineTeamInvitation(requests_mock: Mocker) -> None: + """Test declineTeamInvitation static method of Account class.""" + mock_response = { + "status": True, + "result": "Invitation declined", + } + + requests_mock.get("https://api.tago.io/profile/team/decline/invitation-token-123", json=mock_response) + + result = Account.declineTeamInvitation("invitation-token-123") + + assert result == "Invitation declined" diff --git a/tests/Resources/test_analyses.py b/tests/Resources/test_analyses.py index bbd36c7..e031223 100644 --- a/tests/Resources/test_analyses.py +++ b/tests/Resources/test_analyses.py @@ -1,8 +1,13 @@ import os + from requests_mock.mocker import Mocker +from tagoio_sdk.modules.Resources.Analysis_Types import AnalysisCreateInfo +from tagoio_sdk.modules.Resources.Analysis_Types import AnalysisInfo +from tagoio_sdk.modules.Resources.Analysis_Types import AnalysisListItem +from tagoio_sdk.modules.Resources.Analysis_Types import ScriptFile from tagoio_sdk.modules.Resources.Resources import Resources -from tagoio_sdk.modules.Resources.Analysis_Types import AnalysisCreateInfo, AnalysisInfo, ScriptFile, AnalysisListItem + os.environ["T_ANALYSIS_TOKEN"] = "your_token_value" @@ -18,7 +23,7 @@ def mockAnalysisList() -> list[AnalysisListItem]: "updated_at": "2023-03-07T01:43:45.952Z", "last_run": "2023-03-07T01:43:45.952Z", } - ] + ], } @@ -31,18 +36,12 @@ def mockAnalysisInfo() -> AnalysisInfo: "created_at": "2023-03-07T01:43:45.952Z", "updated_at": "2023-03-07T01:43:45.952Z", "last_run": "2023-03-07T01:43:45.952Z", - } + }, } def mockCreateAnalysis() -> dict: - return { - "status": True, - "result": { - "id": "analysis_id", - "token": "analysis_token" - } - } + return {"status": True, "result": {"id": "analysis_id", "token": "analysis_token"}} def testAnalysesMethodList(requests_mock: Mocker) -> None: @@ -78,7 +77,10 @@ def testAnalysesMethodEdit(requests_mock: Mocker) -> None: :param requests_mock are a plugin of pytest to mock the requests. """ analysis_data = AnalysisInfo(name="Updated Analysis") - requests_mock.put("https://api.tago.io/analysis/analysis_id", json={"status": True, "result": "success"}) + requests_mock.put( + "https://api.tago.io/analysis/analysis_id", + json={"status": True, "result": "success"}, + ) resources = Resources() response = resources.analysis.edit("analysis_id", analysis_data) @@ -91,7 +93,10 @@ def testAnalysesMethodDelete(requests_mock: Mocker) -> None: """ :param requests_mock are a plugin of pytest to mock the requests. """ - requests_mock.delete("https://api.tago.io/analysis/analysis_id", json={"status": True, "result": "success"}) + requests_mock.delete( + "https://api.tago.io/analysis/analysis_id", + json={"status": True, "result": "success"}, + ) resources = Resources() response = resources.analysis.delete("analysis_id") @@ -104,7 +109,9 @@ def testAnalysesMethodInfo(requests_mock: Mocker) -> None: """ :param requests_mock are a plugin of pytest to mock the requests. """ - requests_mock.get("https://api.tago.io/analysis/analysis_id", json=mockAnalysisInfo()) + requests_mock.get( + "https://api.tago.io/analysis/analysis_id", json=mockAnalysisInfo() + ) resources = Resources() response = resources.analysis.info("analysis_id") @@ -117,7 +124,10 @@ def testAnalysesMethodRun(requests_mock: Mocker) -> None: """ :param requests_mock are a plugin of pytest to mock the requests. """ - requests_mock.post("https://api.tago.io/analysis/analysis_id/run", json={"status": True, "result": {"token": "run_token"}}) + requests_mock.post( + "https://api.tago.io/analysis/analysis_id/run", + json={"status": True, "result": {"token": "run_token"}}, + ) resources = Resources() response = resources.analysis.run("analysis_id") @@ -130,7 +140,10 @@ def testAnalysesMethodTokenGenerate(requests_mock: Mocker) -> None: """ :param requests_mock are a plugin of pytest to mock the requests. """ - requests_mock.get("https://api.tago.io/analysis/analysis_id/token", json={"status": True, "result": {"token": "new_token"}}) + requests_mock.get( + "https://api.tago.io/analysis/analysis_id/token", + json={"status": True, "result": {"token": "new_token"}}, + ) resources = Resources() response = resources.analysis.tokenGenerate("analysis_id") @@ -143,8 +156,13 @@ def testAnalysesMethodUploadScript(requests_mock: Mocker) -> None: """ :param requests_mock are a plugin of pytest to mock the requests. """ - script_file = ScriptFile(name="script.js", language="node", content="console.log('Hello, World!');") - requests_mock.post("https://api.tago.io/analysis/analysis_id/upload", json={"status": True, "result": "success"}) + script_file = ScriptFile( + name="script.js", language="node", content="console.log('Hello, World!');" + ) + requests_mock.post( + "https://api.tago.io/analysis/analysis_id/upload", + json={"status": True, "result": "success"}, + ) resources = Resources() response = resources.analysis.uploadScript("analysis_id", script_file) @@ -157,10 +175,120 @@ def testAnalysesMethodDownloadScript(requests_mock: Mocker) -> None: """ :param requests_mock are a plugin of pytest to mock the requests. """ - requests_mock.get("https://api.tago.io/analysis/analysis_id/download", json={"status": True, "result": {"url": "https://download.url"}}) + requests_mock.get( + "https://api.tago.io/analysis/analysis_id/download", + json={"status": True, "result": {"url": "https://download.url"}}, + ) resources = Resources() response = resources.analysis.downloadScript("analysis_id") assert response == {"url": "https://download.url"} assert isinstance(response, dict) + + +def testAnalysesMethodListSnippets(requests_mock: Mocker) -> None: + """ + Test listSnippets method to retrieve all available snippets for a runtime. + :param requests_mock are a plugin of pytest to mock the requests. + """ + mock_snippets_response = { + "runtime": "python-rt2025", + "schema_version": 1, + "generated_at": "2025-01-13T12:00:00Z", + "snippets": [ + { + "id": "console-example", + "title": "Console Example", + "description": "Basic console logging example", + "language": "python", + "tags": ["basics", "logging"], + "filename": "console.py", + "file_path": "python-rt2025/console.py", + }, + { + "id": "data-processing", + "title": "Data Processing", + "description": "Process device data", + "language": "python", + "tags": ["data", "processing"], + "filename": "process-data.py", + "file_path": "python-rt2025/process-data.py", + }, + ], + } + + requests_mock.get( + "https://snippets.tago.io/python-rt2025.json", json=mock_snippets_response + ) + + resources = Resources() + response = resources.analysis.listSnippets("python-rt2025") + + assert response["runtime"] == "python-rt2025" + assert response["schema_version"] == 1 + assert len(response["snippets"]) == 2 + assert response["snippets"][0]["id"] == "console-example" + assert response["snippets"][0]["title"] == "Console Example" + assert isinstance(response, dict) + assert isinstance(response["snippets"], list) + + +def testAnalysesMethodGetSnippetFile(requests_mock: Mocker) -> None: + """ + Test getSnippetFile method to retrieve raw snippet file content. + :param requests_mock are a plugin of pytest to mock the requests. + """ + mock_file_content = """# Console Example +from tagoio_sdk import Analysis + +def my_analysis(context): + context.log("Hello from Python snippet!") + +Analysis(my_analysis) +""" + + requests_mock.get( + "https://snippets.tago.io/python-rt2025/console.py", text=mock_file_content + ) + + resources = Resources() + response = resources.analysis.getSnippetFile("python-rt2025", "console.py") + + assert "Console Example" in response + assert "context.log" in response + assert isinstance(response, str) + + +def testAnalysesMethodListSnippetsError(requests_mock: Mocker) -> None: + """ + Test listSnippets method error handling when request fails. + :param requests_mock are a plugin of pytest to mock the requests. + """ + requests_mock.get("https://snippets.tago.io/invalid-runtime.json", status_code=404) + + resources = Resources() + + try: + resources.analysis.listSnippets("invalid-runtime") + raise AssertionError("Expected RuntimeError to be raised") + except RuntimeError as e: + assert "Failed to fetch snippets" in str(e) + + +def testAnalysesMethodGetSnippetFileError(requests_mock: Mocker) -> None: + """ + Test getSnippetFile method error handling when file not found. + :param requests_mock are a plugin of pytest to mock the requests. + """ + requests_mock.get( + "https://snippets.tago.io/python-rt2025/nonexistent.py", status_code=404 + ) + + resources = Resources() + + try: + resources.analysis.getSnippetFile("python-rt2025", "nonexistent.py") + raise AssertionError("Expected RuntimeError to be raised") + except RuntimeError as e: + assert "Failed to fetch snippet file" in str(e)