diff --git a/docs-site/scripts/gen_api_docs.py b/docs-site/scripts/gen_api_docs.py index e6f71ff..550a8e3 100644 --- a/docs-site/scripts/gen_api_docs.py +++ b/docs-site/scripts/gen_api_docs.py @@ -5,12 +5,13 @@ Outputs under docs-site/docs/api/python/modules. Also writes a coverage and gaps REPORT.md. """ -import sys -import inspect + import importlib +import inspect import pkgutil +import sys from pathlib import Path -from typing import Any, Dict, List, Tuple, Optional +from typing import Any, Dict, List, Optional, Tuple ROOT = Path(__file__).resolve().parents[2] PKG_NAME = "nexla_sdk" @@ -28,7 +29,9 @@ def iter_module_names(package: str) -> List[str]: spec = importlib.util.find_spec(package) if spec is None or not spec.submodule_search_locations: return names - for m in pkgutil.walk_packages(spec.submodule_search_locations, prefix=f"{package}."): + for m in pkgutil.walk_packages( + spec.submodule_search_locations, prefix=f"{package}." + ): # Skip private or cache if any(part.startswith("_") for part in m.name.split(".")): continue @@ -50,9 +53,13 @@ def public_members(mod) -> Tuple[List[Tuple[str, Any]], List[Tuple[str, Any]]]: for n, obj in inspect.getmembers(mod): if n.startswith("_"): continue - if inspect.isclass(obj) and getattr(obj, "__module__", "").startswith(mod.__name__): + if inspect.isclass(obj) and getattr(obj, "__module__", "").startswith( + mod.__name__ + ): classes.append((n, obj)) - elif inspect.isfunction(obj) and getattr(obj, "__module__", "").startswith(mod.__name__): + elif inspect.isfunction(obj) and getattr(obj, "__module__", "").startswith( + mod.__name__ + ): functions.append((n, obj)) return classes, functions @@ -94,6 +101,7 @@ def pydantic_fields(cls) -> List[Tuple[str, str, Optional[str]]]: def enum_members(cls) -> List[Tuple[str, Any]]: try: import enum + if issubclass(cls, enum.Enum): return [(m.name, m.value) for m in cls] # type: ignore[attr-defined] except Exception: @@ -108,7 +116,9 @@ def format_signature(obj) -> str: return "()" -def write_module_page(module_name: str, mod, coverage: Dict[str, Any], gaps: List[str]) -> None: +def write_module_page( + module_name: str, mod, coverage: Dict[str, Any], gaps: List[str] +) -> None: classes, functions = public_members(mod) file, line = get_source_info(mod) title = module_name @@ -123,7 +133,7 @@ def write_module_page(module_name: str, mod, coverage: Dict[str, Any], gaps: Lis TRACE[module_name] = { "module_source": f"{file}:{line}" if file and line else None, "classes": {}, - "functions": {} + "functions": {}, } with out_path.open("w", encoding="utf-8") as f: @@ -172,7 +182,8 @@ def write_module_page(module_name: str, mod, coverage: Dict[str, Any], gaps: Lis methods = [ (n, m) for n, m in inspect.getmembers(cls, predicate=inspect.isfunction) - if not n.startswith("_") and getattr(m, "__module__", "").startswith(mod.__name__) + if not n.startswith("_") + and getattr(m, "__module__", "").startswith(mod.__name__) ] if methods: f.write("Methods:\n\n") @@ -183,7 +194,9 @@ def write_module_page(module_name: str, mod, coverage: Dict[str, Any], gaps: Lis f.write(f"- `{n}{sig}`\n") if mfile and mline: f.write(f" - Source: `{mfile}:{mline}`\n") - TRACE[module_name]["classes"][f"{cname}.{n}"] = f"{mfile}:{mline}" + TRACE[module_name]["classes"][ + f"{cname}.{n}" + ] = f"{mfile}:{mline}" if mdoc: f.write(f" - {mdoc.splitlines()[0]}\n") f.write("\n") @@ -205,7 +218,9 @@ def write_module_page(module_name: str, mod, coverage: Dict[str, Any], gaps: Lis # TODO marker if no symbols found if total_symbols == 0: - f.write("🚧 TODO: No public symbols detected. Verify module visibility and docstrings.\n\n") + f.write( + "🚧 TODO: No public symbols detected. Verify module visibility and docstrings.\n\n" + ) if file and line: gaps.append(f"No symbols in {file}:{line}") diff --git a/examples/basic_usage.py b/examples/basic_usage.py index 87b441a..cd8a9da 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -1,153 +1,164 @@ import os + from nexla_sdk import NexlaClient from nexla_sdk.models.credentials.requests import CredentialCreate -from nexla_sdk.models.sources.requests import SourceCreate -from nexla_sdk.models.nexsets.requests import NexsetCreate from nexla_sdk.models.destinations.requests import DestinationCreate +from nexla_sdk.models.nexsets.requests import NexsetCreate +from nexla_sdk.models.sources.requests import SourceCreate def main(): # Initialize client client = NexlaClient( service_key=os.getenv("NEXLA_SERVICE_KEY"), - base_url=os.getenv("NEXLA_API_URL", "https://dataops.nexla.io/nexla-api") + base_url=os.getenv("NEXLA_API_URL", "https://dataops.nexla.io/nexla-api"), ) - + # Example 1: List all sources print("=== Listing Sources ===") sources = client.sources.list() for source in sources: print(f"Source: {source.name} ({source.id}) - Status: {source.status}") - + # Example 2: Create a credential print("\n=== Creating Credential ===") - credential = client.credentials.create(CredentialCreate( - name="My S3 Bucket", - credentials_type="s3", - credentials={ - "access_key_id": "your_access_key", - "secret_access_key": "your_secret_key", - "region": "us-east-1" - } - )) + credential = client.credentials.create( + CredentialCreate( + name="My S3 Bucket", + credentials_type="s3", + credentials={ + "access_key_id": "your_access_key", + "secret_access_key": "your_secret_key", + "region": "us-east-1", + }, + ) + ) print(f"Created credential: {credential.name} ({credential.id})") - + # Example 3: Create a source print("\n=== Creating Source ===") - source = client.sources.create(SourceCreate( - name="My S3 Source", - source_type="s3", - data_credentials_id=credential.id, - source_config={ - "path": "my-bucket/data/", - "file_format": "json", - "start.cron": "0 0 * * * ?" # Daily at midnight - } - )) + source = client.sources.create( + SourceCreate( + name="My S3 Source", + source_type="s3", + data_credentials_id=credential.id, + source_config={ + "path": "my-bucket/data/", + "file_format": "json", + "start.cron": "0 0 * * * ?", # Daily at midnight + }, + ) + ) print(f"Created source: {source.name} ({source.id})") - + # Example 4: Get detected nexsets print("\n=== Detected Nexsets ===") nexsets = client.nexsets.list() source_nexsets = [n for n in nexsets if n.data_source_id == source.id] for nexset in source_nexsets: print(f"Nexset: {nexset.name} ({nexset.id})") - + # Example 5: Create a transformed nexset if source_nexsets: parent_nexset = source_nexsets[0] print(f"\n=== Creating Transformed Nexset from {parent_nexset.name} ===") - - transformed = client.nexsets.create(NexsetCreate( - name="Transformed Data", - parent_data_set_id=parent_nexset.id, - has_custom_transform=True, - transform={ - "version": 1, - "operations": [{ - "operation": "shift", - "spec": { - "*": "&" # Pass through all fields - } - }] - } - )) + + transformed = client.nexsets.create( + NexsetCreate( + name="Transformed Data", + parent_data_set_id=parent_nexset.id, + has_custom_transform=True, + transform={ + "version": 1, + "operations": [ + { + "operation": "shift", + "spec": {"*": "&"}, # Pass through all fields + } + ], + }, + ) + ) print(f"Created nexset: {transformed.name} ({transformed.id})") - + # Example 6: Create a destination print("\n=== Creating Destination ===") - destination = client.destinations.create(DestinationCreate( - name="My S3 Output", - sink_type="s3", - data_credentials_id=credential.id, - data_set_id=transformed.id, - sink_config={ - "path": "my-bucket/output/", - "file_format": "parquet", - "file_compression": "snappy" - } - )) + destination = client.destinations.create( + DestinationCreate( + name="My S3 Output", + sink_type="s3", + data_credentials_id=credential.id, + data_set_id=transformed.id, + sink_config={ + "path": "my-bucket/output/", + "file_format": "parquet", + "file_compression": "snappy", + }, + ) + ) print(f"Created destination: {destination.name} ({destination.id})") - + # Example 7: View flow print("\n=== Flow Structure ===") flows = client.flows.list(flows_only=True) if flows: flow = flows[0] print(f"Flow has {len(flow.flows)} nodes") - + # Example 8: Pagination example print("\n=== Pagination Example ===") paginator = client.sources.paginate(per_page=10) - + # Iterate through all items for source in paginator: print(f"Source: {source.name}") - + # Or iterate by pages for page in paginator.iter_pages(): print(f"Page {page.page_info.current_page}: {len(page.items)} items") - + # Example 9: Error handling print("\n=== Error Handling Example ===") try: client.sources.get(999999) # Non-existent ID except Exception as e: print(f"Expected error: {type(e).__name__}: {e}") - + # Example 10: Access control print("\n=== Access Control Example ===") if source_nexsets: nexset = source_nexsets[0] - + # Get current accessors accessors = client.nexsets.get_accessors(nexset.id) print(f"Current accessors: {len(accessors)}") - + # Add a user accessor - new_accessors = [{ - "type": "USER", - "email": "colleague@example.com", - "access_roles": ["collaborator"] - }] - + new_accessors = [ + { + "type": "USER", + "email": "colleague@example.com", + "access_roles": ["collaborator"], + } + ] + updated = client.nexsets.add_accessors(nexset.id, new_accessors) print(f"Updated accessors: {len(updated)}") - + # Example 11: Metrics print("\n=== Metrics Example ===") from datetime import datetime, timedelta - + yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") today = datetime.now().strftime("%Y-%m-%d") - + if sources: source = sources[0] metrics = client.metrics.get_resource_daily_metrics( resource_type="data_sources", resource_id=source.id, from_date=yesterday, - to_date=today + to_date=today, ) print(f"Metrics status: {metrics.status}") if metrics.metrics: diff --git a/examples/fetch_resources.py b/examples/fetch_resources.py index 0796874..75d73b9 100644 --- a/examples/fetch_resources.py +++ b/examples/fetch_resources.py @@ -20,6 +20,7 @@ """ import os + from nexla_sdk import NexlaClient from nexla_sdk.exceptions import AuthenticationError, NexlaError @@ -37,7 +38,9 @@ def initialize_client() -> NexlaClient: if access_token: return NexlaClient(access_token=access_token, base_url=base_url) - raise ValueError("Please set NEXLA_SERVICE_KEY or NEXLA_ACCESS_TOKEN environment variable") + raise ValueError( + "Please set NEXLA_SERVICE_KEY or NEXLA_ACCESS_TOKEN environment variable" + ) def list_credentials(client: NexlaClient) -> None: @@ -46,18 +49,21 @@ def list_credentials(client: NexlaClient) -> None: print("\n=== CREDENTIALS ===") credentials = client.credentials.list() print("Total credentials: {}".format(len(credentials))) - + for cred in credentials[:3]: # Show first 3 - print("- ID: {}, Name: {}, Type: {}".format( - cred.id, cred.name, getattr(cred, 'credentials_type', 'N/A'))) - + print( + "- ID: {}, Name: {}, Type: {}".format( + cred.id, cred.name, getattr(cred, "credentials_type", "N/A") + ) + ) + # Get detailed info for first credential if credentials: first_cred = client.credentials.get(credentials[0].id) print("First credential details:") print(" Name: {}".format(first_cred.name)) - print(" Created: {}".format(getattr(first_cred, 'created_at', 'N/A'))) - + print(" Created: {}".format(getattr(first_cred, "created_at", "N/A"))) + except NexlaError as e: print("Error fetching credentials: {}".format(e)) @@ -68,18 +74,21 @@ def list_sources(client: NexlaClient) -> None: print("\n=== SOURCES ===") sources = client.sources.list() print("Total sources: {}".format(len(sources))) - + for source in sources[:3]: # Show first 3 - print("- ID: {}, Name: {}, Status: {}".format( - source.id, source.name, getattr(source, 'status', 'N/A'))) - + print( + "- ID: {}, Name: {}, Status: {}".format( + source.id, source.name, getattr(source, "status", "N/A") + ) + ) + # Get detailed info for first source if sources: first_source = client.sources.get(sources[0].id) print("First source details:") print(" Name: {}".format(first_source.name)) - print(" Created: {}".format(getattr(first_source, 'created_at', 'N/A'))) - + print(" Created: {}".format(getattr(first_source, "created_at", "N/A"))) + except NexlaError as e: print("Error fetching sources: {}".format(e)) @@ -90,18 +99,21 @@ def list_destinations(client: NexlaClient) -> None: print("\n=== DESTINATIONS ===") destinations = client.destinations.list() print("Total destinations: {}".format(len(destinations))) - + for dest in destinations[:3]: # Show first 3 - print("- ID: {}, Name: {}, Status: {}".format( - dest.id, dest.name, getattr(dest, 'status', 'N/A'))) - + print( + "- ID: {}, Name: {}, Status: {}".format( + dest.id, dest.name, getattr(dest, "status", "N/A") + ) + ) + # Get detailed info for first destination if destinations: first_dest = client.destinations.get(destinations[0].id) print("First destination details:") print(" Name: {}".format(first_dest.name)) - print(" Created: {}".format(getattr(first_dest, 'created_at', 'N/A'))) - + print(" Created: {}".format(getattr(first_dest, "created_at", "N/A"))) + except NexlaError as e: print("Error fetching destinations: {}".format(e)) @@ -112,18 +124,21 @@ def list_nexsets(client: NexlaClient) -> None: print("\n=== NEXSETS ===") nexsets = client.nexsets.list() print("Total nexsets: {}".format(len(nexsets))) - + for nexset in nexsets[:3]: # Show first 3 - print("- ID: {}, Name: {}, Records: {}".format( - nexset.id, nexset.name, getattr(nexset, 'total_records', 'N/A'))) - + print( + "- ID: {}, Name: {}, Records: {}".format( + nexset.id, nexset.name, getattr(nexset, "total_records", "N/A") + ) + ) + # Get detailed info for first nexset if nexsets: first_nexset = client.nexsets.get(nexsets[0].id) print("First nexset details:") print(" Name: {}".format(first_nexset.name)) - print(" Schema: {}".format(getattr(first_nexset, 'schema', 'N/A'))) - + print(" Schema: {}".format(getattr(first_nexset, "schema", "N/A"))) + except NexlaError as e: print("Error fetching nexsets: {}".format(e)) @@ -134,20 +149,23 @@ def list_flows(client: NexlaClient) -> None: print("\n=== FLOWS ===") flows = client.flows.list() print("Total flows: {}".format(len(flows))) - + for flow in flows[:3]: # Show first 3 - print("- ID: {}, Name: {}, Status: {}".format( - getattr(flow, 'id', 'N/A'), - getattr(flow, 'name', 'N/A'), - getattr(flow, 'status', 'N/A'))) - + print( + "- ID: {}, Name: {}, Status: {}".format( + getattr(flow, "id", "N/A"), + getattr(flow, "name", "N/A"), + getattr(flow, "status", "N/A"), + ) + ) + # Get detailed info for first flow - if flows and hasattr(flows[0], 'id'): + if flows and hasattr(flows[0], "id"): first_flow = client.flows.get(flows[0].id) print("First flow details:") - print(" Name: {}".format(getattr(first_flow, 'name', 'N/A'))) - print(" Created: {}".format(getattr(first_flow, 'created_at', 'N/A'))) - + print(" Name: {}".format(getattr(first_flow, "name", "N/A"))) + print(" Created: {}".format(getattr(first_flow, "created_at", "N/A"))) + except NexlaError as e: print("Error fetching flows: {}".format(e)) @@ -158,18 +176,21 @@ def list_lookups(client: NexlaClient) -> None: print("\n=== LOOKUPS ===") lookups = client.lookups.list() print("Total lookups: {}".format(len(lookups))) - + for lookup in lookups[:3]: # Show first 3 - print("- ID: {}, Name: {}, Type: {}".format( - lookup.id, lookup.name, getattr(lookup, 'data_map_type', 'N/A'))) - + print( + "- ID: {}, Name: {}, Type: {}".format( + lookup.id, lookup.name, getattr(lookup, "data_map_type", "N/A") + ) + ) + # Get detailed info for first lookup if lookups: first_lookup = client.lookups.get(lookups[0].id) print("First lookup details:") print(" Name: {}".format(first_lookup.name)) - print(" Created: {}".format(getattr(first_lookup, 'created_at', 'N/A'))) - + print(" Created: {}".format(getattr(first_lookup, "created_at", "N/A"))) + except NexlaError as e: print("Error fetching lookups: {}".format(e)) @@ -180,20 +201,23 @@ def list_users(client: NexlaClient) -> None: print("\n=== USERS ===") users = client.users.list() print("Total users: {}".format(len(users))) - + for user in users[:3]: # Show first 3 - print("- ID: {}, Name: {}, Email: {}".format( - user.id, - getattr(user, 'full_name', 'N/A'), - getattr(user, 'email', 'N/A'))) - + print( + "- ID: {}, Name: {}, Email: {}".format( + user.id, + getattr(user, "full_name", "N/A"), + getattr(user, "email", "N/A"), + ) + ) + # Get detailed info for first user if users: first_user = client.users.get(users[0].id) print("First user details:") - print(" Name: {}".format(getattr(first_user, 'name', 'N/A'))) - print(" Email: {}".format(getattr(first_user, 'email', 'N/A'))) - + print(" Name: {}".format(getattr(first_user, "name", "N/A"))) + print(" Email: {}".format(getattr(first_user, "email", "N/A"))) + except NexlaError as e: print("Error fetching users: {}".format(e)) @@ -202,33 +226,36 @@ def demonstrate_pagination(client: NexlaClient) -> None: """Demonstrate pagination with sources.""" try: print("\n=== PAGINATION EXAMPLE ===") - + # Get paginated results paginator = client.sources.paginate(per_page=5) - + page_count = 0 total_items = 0 - + for page in paginator.iter_pages(): page_count += 1 page_items = len(page.items) total_items += page_items - + print("Page {}: {} items".format(page_count, page_items)) - + # Show first item from each page if page.items: first_item = page.items[0] - print(" First item: ID={}, Name={}".format( - first_item.id, first_item.name)) - + print( + " First item: ID={}, Name={}".format( + first_item.id, first_item.name + ) + ) + # Only show first 3 pages for demo if page_count >= 3: break - + print("Total pages processed: {}".format(page_count)) print("Total items processed: {}".format(total_items)) - + except NexlaError as e: print("Error with pagination: {}".format(e)) @@ -239,7 +266,7 @@ def main(): # Initialize client client = initialize_client() print("Successfully initialized Nexla client") - + # List and get resources for each type list_credentials(client) list_sources(client) @@ -248,13 +275,13 @@ def main(): list_flows(client) list_lookups(client) list_users(client) - + # Demonstrate pagination demonstrate_pagination(client) - + print("\n=== SUMMARY ===") print("Successfully demonstrated listing and getting resources for all types!") - + except AuthenticationError as e: print("Authentication failed: {}".format(e)) print("Please check your NEXLA_SERVICE_KEY or NEXLA_ACCESS_TOKEN") diff --git a/nexla_sdk/__init__.py b/nexla_sdk/__init__.py index 5ec1f29..010e8f0 100644 --- a/nexla_sdk/__init__.py +++ b/nexla_sdk/__init__.py @@ -2,7 +2,7 @@ # Package version try: - from importlib.metadata import version, PackageNotFoundError # Python 3.8+ + from importlib.metadata import PackageNotFoundError, version # Python 3.8+ except Exception: # pragma: no cover version = None PackageNotFoundError = Exception @@ -15,131 +15,127 @@ # Import main client from nexla_sdk.client import NexlaClient -# Import resources -from nexla_sdk.resources import ( - CredentialsResource, - FlowsResource, - SourcesResource, - DestinationsResource, - NexsetsResource, - LookupsResource, - UsersResource, - OrganizationsResource, - TeamsResource, - ProjectsResource, - NotificationsResource, - MetricsResource, - CodeContainersResource, - TransformsResource, - AttributeTransformsResource, - AsyncTasksResource, - ApprovalRequestsResource, - RuntimesResource, - MarketplaceResource, - OrgAuthConfigsResource, - GenAIResource, - SelfSignupResource, - DocContainersResource, - DataSchemasResource, -) - -# Import common models -from nexla_sdk.models import ( - BaseModel, - Owner, - Organization, - Connector, - LogEntry, - FlowNode, -) - # Import exceptions from nexla_sdk.exceptions import ( - NexlaError, AuthenticationError, AuthorizationError, + CredentialError, + FlowError, + NexlaError, NotFoundError, - ValidationError, RateLimitError, - ServerError, ResourceConflictError, - CredentialError, - FlowError, + ServerError, TransformError, + ValidationError, +) + +# Import common models +from nexla_sdk.models import ( + BaseModel, + Connector, + FlowNode, + LogEntry, + Organization, + Owner, ) # Import enums from nexla_sdk.models.enums import ( AccessRole, + ConnectorCategory, + NotificationChannel, + NotificationLevel, + OrgMembershipStatus, ResourceStatus, ResourceType, - NotificationLevel, - NotificationChannel, - UserTier, UserStatus, - OrgMembershipStatus, - ConnectorCategory, + UserTier, +) + +# Import resources +from nexla_sdk.resources import ( + ApprovalRequestsResource, + AsyncTasksResource, + AttributeTransformsResource, + CodeContainersResource, + CredentialsResource, + DataSchemasResource, + DestinationsResource, + DocContainersResource, + FlowsResource, + GenAIResource, + LookupsResource, + MarketplaceResource, + MetricsResource, + NexsetsResource, + NotificationsResource, + OrganizationsResource, + OrgAuthConfigsResource, + ProjectsResource, + RuntimesResource, + SelfSignupResource, + SourcesResource, + TeamsResource, + TransformsResource, + UsersResource, ) __all__ = [ # Client - 'NexlaClient', - + "NexlaClient", # Resources - 'CredentialsResource', - 'FlowsResource', - 'SourcesResource', - 'DestinationsResource', - 'NexsetsResource', - 'LookupsResource', - 'UsersResource', - 'OrganizationsResource', - 'TeamsResource', - 'ProjectsResource', - 'NotificationsResource', - 'MetricsResource', - 'CodeContainersResource', - 'TransformsResource', - 'AttributeTransformsResource', - 'AsyncTasksResource', - 'ApprovalRequestsResource', - 'RuntimesResource', - 'MarketplaceResource', - 'OrgAuthConfigsResource', - 'GenAIResource', - 'SelfSignupResource', - 'DocContainersResource', - 'DataSchemasResource', - + "CredentialsResource", + "FlowsResource", + "SourcesResource", + "DestinationsResource", + "NexsetsResource", + "LookupsResource", + "UsersResource", + "OrganizationsResource", + "TeamsResource", + "ProjectsResource", + "NotificationsResource", + "MetricsResource", + "CodeContainersResource", + "TransformsResource", + "AttributeTransformsResource", + "AsyncTasksResource", + "ApprovalRequestsResource", + "RuntimesResource", + "MarketplaceResource", + "OrgAuthConfigsResource", + "GenAIResource", + "SelfSignupResource", + "DocContainersResource", + "DataSchemasResource", # Models - 'BaseModel', - 'Owner', - 'Organization', - 'Connector', - 'LogEntry', - 'FlowNode', - + "BaseModel", + "Owner", + "Organization", + "Connector", + "LogEntry", + "FlowNode", # Exceptions - 'NexlaError', - 'AuthenticationError', - 'AuthorizationError', - 'NotFoundError', - 'ValidationError', - 'RateLimitError', - 'ServerError', - 'ResourceConflictError', - 'CredentialError', - 'FlowError', - 'TransformError', - + "NexlaError", + "AuthenticationError", + "AuthorizationError", + "NotFoundError", + "ValidationError", + "RateLimitError", + "ServerError", + "ResourceConflictError", + "CredentialError", + "FlowError", + "TransformError", # Enums - 'AccessRole', - 'ResourceStatus', - 'ResourceType', - 'NotificationLevel', - 'NotificationChannel', - 'UserTier', - 'UserStatus', - 'OrgMembershipStatus', - 'ConnectorCategory', + "AccessRole", + "ResourceStatus", + "ResourceType", + "NotificationLevel", + "NotificationChannel", + "UserTier", + "UserStatus", + "OrgMembershipStatus", + "ConnectorCategory", ] diff --git a/nexla_sdk/auth.py b/nexla_sdk/auth.py index 9f06604..5e23ea9 100644 --- a/nexla_sdk/auth.py +++ b/nexla_sdk/auth.py @@ -1,12 +1,13 @@ """ Authentication utilities for the Nexla SDK """ + import logging import time -from typing import Dict, Any, Optional, Union +from typing import Any, Dict, Optional, Union -from .exceptions import NexlaError, AuthenticationError -from .http_client import HttpClientInterface, RequestsHttpClient, HttpClientError +from .exceptions import AuthenticationError, NexlaError +from .http_client import HttpClientError, HttpClientInterface, RequestsHttpClient logger = logging.getLogger(__name__) @@ -14,34 +15,36 @@ class TokenAuthHandler: """ Handles authentication and token management for Nexla API - + Supports two authentication flows as per Nexla API documentation: - + 1. **Service Key Flow**: Uses service keys to obtain session tokens via POST to /token endpoint with `Authorization: Basic `. Automatically refreshes tokens before expiry using /token/refresh endpoint. - + 2. **Direct Token Flow**: Uses pre-obtained access tokens directly. These tokens expire after a configured interval (usually 1 hour). - + Responsible for: - Obtaining session tokens using service keys (Basic auth) - - Using directly provided access tokens (Bearer auth) + - Using directly provided access tokens (Bearer auth) - Refreshing session tokens before expiry (service key flow only) - Ensuring valid tokens are available for API requests - Handling authentication retries on 401 responses """ - - def __init__(self, - service_key: Optional[str] = None, - access_token: Optional[str] = None, - base_url: str = "https://dataops.nexla.io/nexla-api", - api_version: str = "v1", - token_refresh_margin: int = 3600, - http_client: Optional[HttpClientInterface] = None): + + def __init__( + self, + service_key: Optional[str] = None, + access_token: Optional[str] = None, + base_url: str = "https://dataops.nexla.io/nexla-api", + api_version: str = "v1", + token_refresh_margin: int = 3600, + http_client: Optional[HttpClientInterface] = None, + ): """ Initialize the token authentication handler - + Args: service_key: Nexla service key for authentication (mutually exclusive with access_token) access_token: Nexla access token for direct authentication (mutually exclusive with service_key) @@ -51,11 +54,11 @@ def __init__(self, http_client: HTTP client implementation (defaults to RequestsHttpClient) """ self.service_key = service_key - self.api_url = base_url.rstrip('/') + self.api_url = base_url.rstrip("/") self.api_version = api_version self.token_refresh_margin = token_refresh_margin self.http_client = http_client or RequestsHttpClient() - + # Session token management if access_token: self._using_direct_token = True @@ -69,65 +72,71 @@ def __init__(self, def get_access_token(self) -> str: """ Get the current access token - + Returns: Current access token - + Raises: AuthenticationError: If no valid token is available """ if not self._access_token: - raise AuthenticationError("No access token available. Authentication required.") + raise AuthenticationError( + "No access token available. Authentication required." + ) return self._access_token def obtain_session_token(self) -> None: """ Obtains a session token using the service key - + Raises: AuthenticationError: If authentication fails or no service key available """ if self._using_direct_token: - raise AuthenticationError("Cannot obtain session token when using direct access token. Service key required.") - + raise AuthenticationError( + "Cannot obtain session token when using direct access token. Service key required." + ) + if not self.service_key: raise AuthenticationError("Service key required to obtain session token.") - + url = f"{self.api_url}/token" headers = { "Authorization": f"Basic {self.service_key}", "Accept": f"application/vnd.nexla.api.{self.api_version}+json", - "Content-Length": "0" + "Content-Length": "0", } - + try: token_data = self.http_client.request("POST", url, headers=headers) self._access_token = token_data.get("access_token") # Calculate expiry time (current time + expires_in seconds) expires_in = token_data.get("expires_in", 86400) self._token_expiry = time.time() + expires_in - + logger.debug("Session token obtained successfully") - + except HttpClientError as e: - if getattr(e, 'status_code', None) == 401: - raise AuthenticationError("Authentication failed. Check your service key.") from e - + if getattr(e, "status_code", None) == 401: + raise AuthenticationError( + "Authentication failed. Check your service key." + ) from e + error_msg = f"Failed to obtain session token: {e}" - error_data = getattr(e, 'response', {}) - + error_data = getattr(e, "response", {}) + if error_data: if "message" in error_data: error_msg = f"Authentication error: {error_data['message']}" elif "error" in error_data: error_msg = f"Authentication error: {error_data['error']}" - + raise NexlaError( - error_msg, - status_code=getattr(e, 'status_code', None), - response=error_data + error_msg, + status_code=getattr(e, "status_code", None), + response=error_data, ) from e - + except Exception as e: raise NexlaError(f"Failed to obtain session token: {e}") from e @@ -145,10 +154,10 @@ def refresh_session_token(self) -> None: def ensure_valid_token(self) -> str: """ Ensures a valid session token is available, refreshing if necessary - + Returns: Current valid access token - + Raises: AuthenticationError: If no token is available or refresh fails """ @@ -166,7 +175,7 @@ def ensure_valid_token(self) -> str: self.obtain_session_token() return self._access_token - + def logout(self) -> None: """ Ends the current session and invalidates the NexlaSessionToken. @@ -175,7 +184,9 @@ def logout(self) -> None: url = f"{self.api_url}/token/logout" headers = { "Accept": f"application/vnd.nexla.api.{self.api_version}+json", - "Authorization": f"Bearer {self._access_token}" if self._access_token else "" + "Authorization": ( + f"Bearer {self._access_token}" if self._access_token else "" + ), } try: # Best-effort logout; ignore response body @@ -187,43 +198,51 @@ def logout(self) -> None: # Invalidate local token regardless self._access_token = None self._token_expiry = 0 - - def execute_authenticated_request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> Union[Dict[str, Any], None]: + + def execute_authenticated_request( + self, method: str, url: str, headers: Dict[str, str], **kwargs + ) -> Union[Dict[str, Any], None]: """ Execute a request with authentication handling - + Args: method: HTTP method url: Full URL to call headers: HTTP headers **kwargs: Additional arguments to pass to the HTTP client - + Returns: API response as a dictionary or None for 204 No Content responses - + Raises: AuthenticationError: If authentication fails ServerError: If the API returns an error """ # Get a valid token access_token = self.ensure_valid_token() - + # Add authorization header headers["Authorization"] = f"Bearer {access_token}" - + try: return self.http_client.request(method, url, headers=headers, **kwargs) except HttpClientError as e: - if getattr(e, 'status_code', None) == 401: + if getattr(e, "status_code", None) == 401: # On 401: if service key mode, obtain new token and retry once if not self._using_direct_token: - logger.warning("401 received; obtaining new session token and retrying once") + logger.warning( + "401 received; obtaining new session token and retrying once" + ) self.obtain_session_token() headers["Authorization"] = f"Bearer {self.get_access_token()}" - return self.http_client.request(method, url, headers=headers, **kwargs) + return self.http_client.request( + method, url, headers=headers, **kwargs + ) # Direct token cannot be refreshed - raise AuthenticationError("Authentication failed (access token invalid or expired)") from e + raise AuthenticationError( + "Authentication failed (access token invalid or expired)" + ) from e # For other errors, let the caller handle them raise diff --git a/nexla_sdk/client.py b/nexla_sdk/client.py index 4451c36..156c407 100644 --- a/nexla_sdk/client.py +++ b/nexla_sdk/client.py @@ -1,89 +1,98 @@ """ Nexla API client """ + import logging import os -from typing import Dict, Any, Optional, Type, TypeVar, Union, List +from typing import Any, Dict, List, Optional, Type, TypeVar, Union from pydantic import ValidationError as PydanticValidationError -from .exceptions import NexlaError, AuthenticationError, ServerError, ValidationError, NotFoundError -from .auth import TokenAuthHandler -from .http_client import HttpClientInterface, RequestsHttpClient, HttpClientError from . import telemetry -from .resources.flows import FlowsResource -from .resources.sources import SourcesResource -from .resources.destinations import DestinationsResource +from .auth import TokenAuthHandler +from .exceptions import ( + AuthenticationError, + NexlaError, + NotFoundError, + ServerError, + ValidationError, +) +from .http_client import HttpClientError, HttpClientInterface, RequestsHttpClient +from .resources.approval_requests import ApprovalRequestsResource +from .resources.async_tasks import AsyncTasksResource +from .resources.attribute_transforms import AttributeTransformsResource +from .resources.code_containers import CodeContainersResource from .resources.credentials import CredentialsResource +from .resources.data_schemas import DataSchemasResource +from .resources.destinations import DestinationsResource +from .resources.doc_containers import DocContainersResource +from .resources.flows import FlowsResource +from .resources.genai import GenAIResource from .resources.lookups import LookupsResource +from .resources.marketplace import MarketplaceResource +from .resources.metrics import MetricsResource from .resources.nexsets import NexsetsResource -from .resources.users import UsersResource +from .resources.notifications import NotificationsResource +from .resources.org_auth_configs import OrgAuthConfigsResource from .resources.organizations import OrganizationsResource -from .resources.teams import TeamsResource from .resources.projects import ProjectsResource -from .resources.notifications import NotificationsResource -from .resources.metrics import MetricsResource -from .resources.code_containers import CodeContainersResource -from .resources.transforms import TransformsResource -from .resources.attribute_transforms import AttributeTransformsResource -from .resources.async_tasks import AsyncTasksResource -from .resources.approval_requests import ApprovalRequestsResource from .resources.runtimes import RuntimesResource -from .resources.marketplace import MarketplaceResource -from .resources.org_auth_configs import OrgAuthConfigsResource -from .resources.genai import GenAIResource from .resources.self_signup import SelfSignupResource -from .resources.doc_containers import DocContainersResource -from .resources.data_schemas import DataSchemasResource +from .resources.sources import SourcesResource +from .resources.teams import TeamsResource +from .resources.transforms import TransformsResource +from .resources.users import UsersResource from .resources.webhooks import WebhooksResource logger = logging.getLogger(__name__) -T = TypeVar('T') +T = TypeVar("T") class NexlaClient: """ Client for the Nexla API - + The Nexla API supports two authentication methods: - + 1. **Service Key Authentication** (recommended): Service keys are long-lived credentials created in the Nexla UI. The SDK obtains session tokens using the service key on demand and re-obtains a new token as needed. No refresh endpoint is used. - + 2. **Direct Access Token Authentication**: Use a pre-obtained access token directly. These tokens are not refreshed by the SDK. - + Examples: # Method 1: Using service key (recommended for automation) client = NexlaClient(service_key="your-service-key") - + # Method 2: Using access token directly (manual/short-term use) client = NexlaClient(access_token="your-access-token") - + # Using the client (same regardless of authentication method) flows = client.flows.list() - + Note: - Service keys should be treated as highly sensitive credentials - Only provide either service_key OR access_token, not both - When using direct access tokens, ensure they have sufficient lifetime for your operations as they cannot be automatically refreshed """ - - def __init__(self, - service_key: Optional[str] = None, - access_token: Optional[str] = None, - base_url: Optional[str] = None, - api_version: str = "v1", - token_refresh_margin: int = 3600, - http_client: Optional[HttpClientInterface] = None, - trace_enabled: Optional[bool] = None): + + def __init__( + self, + service_key: Optional[str] = None, + access_token: Optional[str] = None, + base_url: Optional[str] = None, + api_version: str = "v1", + token_refresh_margin: int = 3600, + http_client: Optional[HttpClientInterface] = None, + trace_enabled: Optional[bool] = None, + ): """ Initialize the Nexla client - + Args: service_key: Nexla service key for authentication (mutually exclusive with access_token) access_token: Nexla access token for direct authentication (mutually exclusive with service_key) @@ -93,10 +102,10 @@ def __init__(self, http_client: HTTP client implementation (defaults to RequestsHttpClient) trace_enabled: Explicitly enable/disable OpenTelemetry tracing. If None, tracing auto-enables when a global OTEL config is detected. - + Raises: NexlaError: If neither or both authentication methods are provided - + Environment Variables: NEXLA_SERVICE_KEY: Service key (used if no authentication parameters are provided) NEXLA_ACCESS_TOKEN: Access token (used if no authentication parameters are provided and NEXLA_SERVICE_KEY is not set) @@ -109,13 +118,13 @@ def __init__(self, # Only check for access_token if service_key is not available if not service_key: access_token = os.getenv("NEXLA_ACCESS_TOKEN") - + # Check for base_url in environment if not provided as parameter if not base_url: base_url = os.getenv("NEXLA_API_URL") if not base_url: base_url = "https://dataops.nexla.io/nexla-api" - + # Validate authentication parameters if not service_key and not access_token: raise NexlaError( @@ -123,9 +132,11 @@ def __init__(self, "or via NEXLA_SERVICE_KEY/NEXLA_ACCESS_TOKEN environment variables" ) if service_key and access_token: - raise NexlaError("Cannot provide both service_key and access_token. Choose one authentication method.") - - self.api_url = base_url.rstrip('/') + raise NexlaError( + "Cannot provide both service_key and access_token. Choose one authentication method." + ) + + self.api_url = base_url.rstrip("/") self.api_version = api_version # Determine if tracing should be active and get a tracer @@ -133,14 +144,16 @@ def __init__(self, if trace_enabled is True: self._trace_enabled = True elif trace_enabled is None and telemetry.is_tracing_configured(): - logger.debug("Global OpenTelemetry configuration detected. Enabling tracing for Nexla SDK.") + logger.debug( + "Global OpenTelemetry configuration detected. Enabling tracing for Nexla SDK." + ) self._trace_enabled = True self.tracer = telemetry.get_tracer(self._trace_enabled) # Initialize HTTP client (instrumented if tracer provided) self.http_client = http_client or RequestsHttpClient(tracer=self.tracer) - + # Initialize authentication handler self.auth_handler = TokenAuthHandler( service_key=service_key, @@ -148,9 +161,9 @@ def __init__(self, base_url=base_url, api_version=api_version, token_refresh_margin=token_refresh_margin, - http_client=self.http_client + http_client=self.http_client, ) - + # Initialize API endpoints self.flows = FlowsResource(self) self.sources = SourcesResource(self) @@ -180,20 +193,20 @@ def __init__(self, def get_access_token(self) -> str: """ Get a valid access token. - + For service keys, the SDK obtains tokens as needed and re-obtains a new one if the current token is near expiry. Direct access tokens are used as-is. - + Returns: A valid access token string - + Raises: AuthenticationError: If no valid token is available or refresh fails - + Examples: # Get a valid access token token = client.get_access_token() - + # Use the token for external API calls headers = {"Authorization": f"Bearer {token}"} """ @@ -202,16 +215,16 @@ def get_access_token(self) -> str: def refresh_access_token(self) -> str: """ Obtain a fresh token and return it. - + For service keys, this obtains a new token. Direct access tokens cannot be refreshed and will raise an AuthenticationError. - + Returns: Refreshed access token string - + Raises: AuthenticationError: If token refresh fails - + Examples: # Force refresh and get new token new_token = client.refresh_access_token() @@ -265,49 +278,53 @@ def create_webhook_client(self, api_key: str) -> WebhooksResource: """ return WebhooksResource(api_key=api_key, http_client=self.http_client) - def _convert_to_model(self, data: Union[Dict[str, Any], List[Dict[str, Any]]], model_class: Type[T]) -> Union[T, List[T]]: + def _convert_to_model( + self, data: Union[Dict[str, Any], List[Dict[str, Any]]], model_class: Type[T] + ) -> Union[T, List[T]]: """ Convert API response data to a Pydantic model - + Args: data: API response data, either a dict or a list of dicts model_class: Pydantic model class to convert to - + Returns: Pydantic model instance or list of instances - + Raises: ValidationError: If validation fails """ try: logger.debug(f"Converting data to model: {model_class.__name__}") logger.debug(f"Data to convert: {data}") - + if isinstance(data, list): result = [model_class.model_validate(item) for item in data] logger.debug(f"Converted list result: {result}") return result - + result = model_class.model_validate(data) logger.debug(f"Converted single result: {result}") return result except PydanticValidationError as e: # Log the validation error details logger.error(f"Validation error converting to {model_class.__name__}: {e}") - raise ValidationError(f"Failed to convert API response to {model_class.__name__}: {e}") - + raise ValidationError( + f"Failed to convert API response to {model_class.__name__}: {e}" + ) + def request(self, method: str, path: str, **kwargs) -> Union[Dict[str, Any], None]: """ Send a request to the Nexla API - + Args: method: HTTP method path: API path **kwargs: Additional arguments to pass to HTTP client - + Returns: API response as a dictionary or None for 204 No Content responses - + Raises: AuthenticationError: If authentication fails ServerError: If the API returns an error @@ -315,20 +332,17 @@ def request(self, method: str, path: str, **kwargs) -> Union[Dict[str, Any], Non url = f"{self.api_url}{path}" headers = { "Accept": f"application/vnd.nexla.api.{self.api_version}+json", - "Content-Type": "application/json" + "Content-Type": "application/json", } - + # If custom headers are provided, merge them with the default headers if "headers" in kwargs: headers.update(kwargs.pop("headers")) - + try: # Let auth handler manage getting a valid token and handling auth retries return self.auth_handler.execute_authenticated_request( - method=method, - url=url, - headers=headers, - **kwargs + method=method, url=url, headers=headers, **kwargs ) except HttpClientError as e: # Map HTTP client errors to appropriate Nexla exceptions @@ -344,38 +358,42 @@ def request(self, method: str, path: str, **kwargs) -> Union[Dict[str, Any], Non "method": method, "path": path, "url": url, - "kwargs": {k: v for k, v in kwargs.items() if k not in ['json', 'data']} + "kwargs": { + k: v for k, v in kwargs.items() if k not in ["json", "data"] + }, }, - original_error=e + original_error=e, ) from e - def _handle_http_error(self, error: HttpClientError, method: str, path: str, url: str, kwargs: dict): + def _handle_http_error( + self, error: HttpClientError, method: str, path: str, url: str, kwargs: dict + ): """ Handle HTTP client errors by mapping them to appropriate Nexla exceptions - + Args: error: The HTTP client error method: HTTP method that failed path: API path that failed url: Full URL that failed kwargs: Request parameters - + Raises: AuthenticationError: If authentication fails (401) NotFoundError: If resource not found (404) ServerError: For other API errors """ - status_code = getattr(error, 'status_code', None) - error_data = getattr(error, 'response', {}) - + status_code = getattr(error, "status_code", None) + error_data = getattr(error, "response", {}) + error_msg = f"API request failed: {error}" - + if error_data: if "message" in error_data: error_msg = f"API error: {error_data['message']}" elif "error" in error_data: error_msg = f"API error: {error_data['error']}" - + # Extract resource information (prefer server-provided fields, fallback to path) resource_type = None resource_id = None @@ -385,7 +403,7 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url if not resource_type or not resource_id: # Fallback to parsing the path if path: - path_parts = path.strip('/').split('/') + path_parts = path.strip("/").split("/") if not resource_type and len(path_parts) >= 1: resource_type = path_parts[0] if not resource_id and len(path_parts) >= 2 and path_parts[1].isdigit(): @@ -393,7 +411,7 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url # Final defaults if not resource_type: resource_type = "unknown" - + # Build context context = { "method": method, @@ -401,9 +419,11 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url "url": url, "status_code": status_code, "api_response": error_data, - "request_params": {k: v for k, v in kwargs.items() if k not in ['json', 'data']} + "request_params": { + k: v for k, v in kwargs.items() if k not in ["json", "data"] + }, } - + # Map status codes to specific exceptions if status_code == 400: raise ValidationError( @@ -414,7 +434,7 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url resource_type=resource_type, resource_id=resource_id, context=context, - original_error=error + original_error=error, ) from error elif status_code == 401: raise AuthenticationError( @@ -423,10 +443,11 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url resource_type=resource_type, resource_id=resource_id, context=context, - original_error=error + original_error=error, ) from error elif status_code == 403: from .exceptions import AuthorizationError + raise AuthorizationError( error_msg, status_code=status_code, @@ -435,7 +456,7 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url resource_type=resource_type, resource_id=resource_id, context=context, - original_error=error + original_error=error, ) from error elif status_code == 404: raise NotFoundError( @@ -444,10 +465,11 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url resource_id=resource_id, operation=f"{method.lower()}_request", context=context, - original_error=error + original_error=error, ) from error elif status_code == 409: from .exceptions import ResourceConflictError + raise ResourceConflictError( error_msg, status_code=status_code, @@ -456,22 +478,25 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url resource_type=resource_type, resource_id=resource_id, context=context, - original_error=error + original_error=error, ) from error elif status_code == 429: from .exceptions import RateLimitError + retry_after = None # Try to parse retry-after from headers or body - headers = getattr(error, 'headers', {}) or {} + headers = getattr(error, "headers", {}) or {} if headers: - retry_after_hdr = headers.get('Retry-After') or headers.get('retry-after') + retry_after_hdr = headers.get("Retry-After") or headers.get( + "retry-after" + ) if retry_after_hdr: try: retry_after = int(retry_after_hdr) except Exception: retry_after = None if not retry_after and isinstance(error_data, dict): - retry_after = error_data.get('retry_after') + retry_after = error_data.get("retry_after") raise RateLimitError( error_msg, retry_after=retry_after, @@ -481,7 +506,7 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url resource_type=resource_type, resource_id=resource_id, context=context, - original_error=error + original_error=error, ) from error else: raise ServerError( @@ -492,5 +517,5 @@ def _handle_http_error(self, error: HttpClientError, method: str, path: str, url resource_type=resource_type, resource_id=resource_id, context=context, - original_error=error - ) from error + original_error=error, + ) from error diff --git a/nexla_sdk/exceptions.py b/nexla_sdk/exceptions.py index 2e98ab7..75219c3 100644 --- a/nexla_sdk/exceptions.py +++ b/nexla_sdk/exceptions.py @@ -1,20 +1,22 @@ -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional class NexlaError(Exception): """Base exception for all Nexla errors.""" - - def __init__(self, - message: str, - details: Optional[Dict[str, Any]] = None, - operation: Optional[str] = None, - resource_type: Optional[str] = None, - resource_id: Optional[str] = None, - step: Optional[str] = None, - context: Optional[Dict[str, Any]] = None, - original_error: Optional[Exception] = None, - status_code: Optional[int] = None, - response: Optional[Dict[str, Any]] = None): + + def __init__( + self, + message: str, + details: Optional[Dict[str, Any]] = None, + operation: Optional[str] = None, + resource_type: Optional[str] = None, + resource_id: Optional[str] = None, + step: Optional[str] = None, + context: Optional[Dict[str, Any]] = None, + original_error: Optional[Exception] = None, + status_code: Optional[int] = None, + response: Optional[Dict[str, Any]] = None, + ): super().__init__(message) self.message = message self.details = details or {} @@ -26,11 +28,11 @@ def __init__(self, self.original_error = original_error self.status_code = status_code self.response = response - + def __str__(self): """Provide detailed error information.""" parts = [] - + if self.step: parts.append(f"Step: {self.step}") if self.operation: @@ -39,18 +41,18 @@ def __str__(self): parts.append(f"Resource: {self.resource_type}") if self.resource_id: parts.append(f"ID: {self.resource_id}") - + parts.append(f"Error: {self.message}") - + if self.details: parts.append(f"Details: {self.details}") if self.context: parts.append(f"Context: {self.context}") if self.original_error: parts.append(f"Original Error: {self.original_error}") - + return " | ".join(parts) - + def get_error_summary(self) -> Dict[str, Any]: """Get structured error information.""" return { @@ -63,38 +65,41 @@ def get_error_summary(self) -> Dict[str, Any]: "context": self.context, "status_code": self.status_code, "response": self.response, - "original_error": str(self.original_error) if self.original_error else None + "original_error": str(self.original_error) if self.original_error else None, } class AuthenticationError(NexlaError): """Raised when authentication fails.""" - + def __init__(self, message: str = "Authentication failed", **kwargs): # If operation is not provided, default to "authentication" - if 'operation' not in kwargs: - kwargs['operation'] = "authentication" + if "operation" not in kwargs: + kwargs["operation"] = "authentication" super().__init__(message, **kwargs) class AuthorizationError(NexlaError): """Raised when user lacks permission.""" + pass class NotFoundError(NexlaError): """Raised when a resource is not found.""" + pass class ValidationError(NexlaError): """Raised when request validation fails.""" + pass class RateLimitError(NexlaError): """Raised when rate limit is exceeded.""" - + def __init__(self, message: str, retry_after: Optional[int] = None, **kwargs): super().__init__(message, **kwargs) self.retry_after = retry_after @@ -102,47 +107,55 @@ def __init__(self, message: str, retry_after: Optional[int] = None, **kwargs): class ServerError(NexlaError): """Raised when server returns 5xx error.""" + pass class ResourceConflictError(NexlaError): """Raised when resource conflicts occur.""" + pass class CredentialError(NexlaError): """Raised when credential validation fails.""" - + def __init__(self, message: str, credential_id: Optional[str] = None, **kwargs): # Set defaults if not provided - kwargs.setdefault('operation', 'credential_validation') - kwargs.setdefault('resource_type', 'credential') + kwargs.setdefault("operation", "credential_validation") + kwargs.setdefault("resource_type", "credential") if credential_id: - kwargs.setdefault('resource_id', credential_id) + kwargs.setdefault("resource_id", credential_id) super().__init__(message, **kwargs) class FlowError(NexlaError): """Raised when flow operations fail.""" - - def __init__(self, message: str, flow_id: Optional[str] = None, flow_step: Optional[str] = None, **kwargs): + + def __init__( + self, + message: str, + flow_id: Optional[str] = None, + flow_step: Optional[str] = None, + **kwargs, + ): # Set defaults if not provided - kwargs.setdefault('operation', 'flow_operation') - kwargs.setdefault('resource_type', 'flow') + kwargs.setdefault("operation", "flow_operation") + kwargs.setdefault("resource_type", "flow") if flow_id: - kwargs.setdefault('resource_id', flow_id) + kwargs.setdefault("resource_id", flow_id) if flow_step: - kwargs.setdefault('step', flow_step) + kwargs.setdefault("step", flow_step) super().__init__(message, **kwargs) class TransformError(NexlaError): """Raised when transform operations fail.""" - + def __init__(self, message: str, transform_id: Optional[str] = None, **kwargs): # Set defaults if not provided - kwargs.setdefault('operation', 'transform_operation') - kwargs.setdefault('resource_type', 'transform') + kwargs.setdefault("operation", "transform_operation") + kwargs.setdefault("resource_type", "transform") if transform_id: - kwargs.setdefault('resource_id', transform_id) + kwargs.setdefault("resource_id", transform_id) super().__init__(message, **kwargs) diff --git a/nexla_sdk/http_client.py b/nexla_sdk/http_client.py index f6faf42..e292f0b 100644 --- a/nexla_sdk/http_client.py +++ b/nexla_sdk/http_client.py @@ -1,11 +1,13 @@ """ HTTP client interface and implementations for Nexla SDK """ + from abc import ABC, abstractmethod -from typing import Dict, Any, Optional, Union +from typing import Any, Dict, Optional, Union import requests from requests.adapters import HTTPAdapter + try: # urllib3 Retry API from urllib3.util.retry import Retry except Exception: # pragma: no cover @@ -13,15 +15,17 @@ try: from importlib.metadata import version # Python 3.8+ + _SDK_VERSION = version("nexla-sdk") except Exception: # pragma: no cover _SDK_VERSION = "unknown" # Optional OpenTelemetry imports (guarded by availability) from . import telemetry + try: # pragma: no cover - optional dependency - from opentelemetry.trace import SpanKind, Status, StatusCode # type: ignore from opentelemetry.propagate import inject # type: ignore + from opentelemetry.trace import SpanKind, Status, StatusCode # type: ignore except Exception: # pragma: no cover SpanKind = None # type: ignore[assignment] Status = None # type: ignore[assignment] @@ -36,21 +40,23 @@ class HttpClientInterface(ABC): Abstract interface for HTTP clients used by the Nexla SDK. This allows for different HTTP client implementations or mocks for testing. """ - + @abstractmethod - def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> Union[Dict[str, Any], None]: + def request( + self, method: str, url: str, headers: Dict[str, str], **kwargs + ) -> Union[Dict[str, Any], None]: """ Send an HTTP request - + Args: method: HTTP method (GET, POST, PUT, DELETE, etc.) url: Request URL headers: Request headers **kwargs: Additional arguments for the request - + Returns: Response data as dictionary or None for 204 No Content responses - + Raises: HttpClientError: If the request fails """ @@ -59,7 +65,14 @@ def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> U class HttpClientError(Exception): """Base exception for HTTP client errors""" - def __init__(self, message: str, status_code: Optional[int] = None, response: Optional[Dict[str, Any]] = None, headers: Optional[Dict[str, Any]] = None): + + def __init__( + self, + message: str, + status_code: Optional[int] = None, + response: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, Any]] = None, + ): super().__init__(message) self.status_code = status_code self.response = response or {} @@ -88,23 +101,41 @@ def __init__( connect=max_retries, backoff_factor=backoff_factor, status_forcelist=[429, 502, 503, 504], - allowed_methods=["HEAD", "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"], + allowed_methods=[ + "HEAD", + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "OPTIONS", + ], raise_on_status=False, ) adapter = HTTPAdapter(max_retries=retry) self.session.mount("http://", adapter) self.session.mount("https://", adapter) - def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> Union[Dict[str, Any], None]: + def request( + self, method: str, url: str, headers: Dict[str, str], **kwargs + ) -> Union[Dict[str, Any], None]: """Send an HTTP request using a session with sane defaults.""" span_name = f"Nexla API {method.upper()}" - kind = SpanKind.CLIENT if telemetry._opentelemetry_available and SpanKind is not None else None # type: ignore[assignment] + kind = ( + SpanKind.CLIENT + if telemetry._opentelemetry_available and SpanKind is not None + else None + ) # type: ignore[assignment] with self.tracer.start_as_current_span(span_name, kind=kind): # type: ignore[arg-type] # We intentionally fetch the current span after creating it to set attributes span = None try: # Get the span from the current context if available (best-effort) - if telemetry._opentelemetry_available and hasattr(telemetry, "trace") and telemetry.trace: + if ( + telemetry._opentelemetry_available + and hasattr(telemetry, "trace") + and telemetry.trace + ): span = telemetry.trace.get_current_span() # type: ignore[attr-defined] except Exception: span = None @@ -138,7 +169,9 @@ def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> U except Exception: pass - response = self.session.request(method, url, headers=merged_headers, timeout=timeout, **kwargs) + response = self.session.request( + method, url, headers=merged_headers, timeout=timeout, **kwargs + ) response.raise_for_status() # Add response attributes @@ -153,8 +186,8 @@ def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> U return None # Check if response content type indicates JSON - content_type = response.headers.get('content-type', '').lower() - if 'application/json' in content_type or 'text/json' in content_type: + content_type = response.headers.get("content-type", "").lower() + if "application/json" in content_type or "text/json" in content_type: return response.json() # Try to parse as JSON anyway, but handle cases where it's not JSON @@ -162,12 +195,21 @@ def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> U return response.json() except (ValueError, requests.exceptions.JSONDecodeError): # If it's not JSON, return the response as text in a dict - return {"raw_text": response.text, "status_code": response.status_code} + return { + "raw_text": response.text, + "status_code": response.status_code, + } except requests.exceptions.HTTPError as e: # Record exception on span try: - if span and getattr(span, "is_recording", lambda: False)() and telemetry._opentelemetry_available and Status is not None and StatusCode is not None: + if ( + span + and getattr(span, "is_recording", lambda: False)() + and telemetry._opentelemetry_available + and Status is not None + and StatusCode is not None + ): span.record_exception(e) span.set_status(Status(status_code=StatusCode.ERROR)) # type: ignore[call-arg] except Exception: @@ -175,12 +217,12 @@ def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> U # Create standardized error with status code and response data error_data: Dict[str, Any] = {} - if 'response' in e.__dict__: + if "response" in e.__dict__: resp = e.response else: resp = response # type: ignore[name-defined] - if resp is not None and getattr(resp, 'content', None): + if resp is not None and getattr(resp, "content", None): try: error_data = resp.json() except ValueError: @@ -188,15 +230,21 @@ def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> U raise HttpClientError( message=str(e), - status_code=getattr(resp, 'status_code', None), + status_code=getattr(resp, "status_code", None), response=error_data, - headers=dict(getattr(resp, 'headers', {}) or {}) + headers=dict(getattr(resp, "headers", {}) or {}), ) from e except requests.exceptions.RequestException as e: # Record exception on span try: - if span and getattr(span, "is_recording", lambda: False)() and telemetry._opentelemetry_available and Status is not None and StatusCode is not None: + if ( + span + and getattr(span, "is_recording", lambda: False)() + and telemetry._opentelemetry_available + and Status is not None + and StatusCode is not None + ): span.record_exception(e) span.set_status(Status(status_code=StatusCode.ERROR)) # type: ignore[call-arg] except Exception: diff --git a/nexla_sdk/models/__init__.py b/nexla_sdk/models/__init__.py index 37ba8c3..3ff2f19 100644 --- a/nexla_sdk/models/__init__.py +++ b/nexla_sdk/models/__init__.py @@ -1,303 +1,380 @@ -from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.common import ( - Owner, Organization, Connector, LogEntry, - FlowNode -) from nexla_sdk.models.access import ( - UserAccessorRequest, TeamAccessorRequest, OrgAccessorRequest, - UserAccessorResponse, TeamAccessorResponse, OrgAccessorResponse, - AccessorRequest, AccessorResponse, AccessorsRequest, - AccessorRequestList, AccessorResponseList, AccessorType + AccessorRequest, + AccessorRequestList, + AccessorResponse, + AccessorResponseList, + AccessorsRequest, + AccessorType, + OrgAccessorRequest, + OrgAccessorResponse, + TeamAccessorRequest, + TeamAccessorResponse, + UserAccessorRequest, + UserAccessorResponse, ) -from nexla_sdk.models.enums import ( - AccessRole, ResourceStatus, ResourceType, NotificationLevel, - NotificationChannel, UserTier, UserStatus, OrgMembershipStatus, - ConnectorCategory +from nexla_sdk.models.approval_requests import ApprovalDecision, ApprovalRequest +from nexla_sdk.models.async_tasks import ( + AsyncTask, + AsyncTaskCreate, + AsyncTaskResult, + DownloadLink, +) +from nexla_sdk.models.attribute_transforms import ( + AttributeTransform, + AttributeTransformCreate, + AttributeTransformUpdate, ) +from nexla_sdk.models.base import BaseModel +from nexla_sdk.models.code_containers import ( + CodeContainer, + CodeContainerCreate, + CodeContainerUpdate, +) +from nexla_sdk.models.common import Connector, FlowNode, LogEntry, Organization, Owner # Import all models from subpackages from nexla_sdk.models.credentials import ( - CredentialType, VerifiedStatus, Credential, ProbeTreeResponse, ProbeSampleResponse, - CredentialCreate, CredentialUpdate, ProbeTreeRequest, ProbeSampleRequest -) -from nexla_sdk.models.flows import ( - FlowResponse, FlowMetrics, FlowElements, FlowCopyOptions, - FlowLogEntry, FlowLogsMeta, FlowLogsResponse, - FlowMetricData, FlowMetricsMeta, FlowMetricsData, FlowMetricsApiResponse, - DocsRecommendation -) -from nexla_sdk.models.sources import ( - SourceStatus, SourceType, IngestMethod, FlowType, Source, DataSetBrief, RunInfo, - SourceCreate, SourceUpdate, SourceCopyOptions + Credential, + CredentialCreate, + CredentialType, + CredentialUpdate, + ProbeSampleRequest, + ProbeSampleResponse, + ProbeTreeRequest, + ProbeTreeResponse, + VerifiedStatus, ) +from nexla_sdk.models.data_schemas import DataSchema from nexla_sdk.models.destinations import ( - DestinationStatus, DestinationType, DestinationFormat, Destination, DataSetInfo, DataMapInfo, - DestinationCreate, DestinationUpdate, DestinationCopyOptions -) -from nexla_sdk.models.nexsets import ( - NexsetStatus, TransformType, OutputType, Nexset, NexsetSample, DataSinkSimplified, - NexsetCreate, NexsetUpdate, NexsetCopyOptions + DataMapInfo, + DataSetInfo, + Destination, + DestinationCopyOptions, + DestinationCreate, + DestinationFormat, + DestinationStatus, + DestinationType, + DestinationUpdate, ) -from nexla_sdk.models.lookups import ( - Lookup, LookupCreate, LookupUpdate, LookupEntriesUpsert -) -from nexla_sdk.models.users import ( - User, UserExpanded, UserSettings, DefaultOrg, OrgMembership, AccountSummary, - UserCreate, UserUpdate +from nexla_sdk.models.doc_containers import DocContainer +from nexla_sdk.models.enums import ( + AccessRole, + ConnectorCategory, + NotificationChannel, + NotificationLevel, + OrgMembershipStatus, + ResourceStatus, + ResourceType, + UserStatus, + UserTier, ) -from nexla_sdk.models.organizations import ( - OrgMember, OrgTier, OrganizationUpdate, OrgMemberUpdate, OrgMemberList, OrgMemberDelete, - OrgCustodianRef, OrgCustodiansPayload, CustodianUser, +from nexla_sdk.models.flows import ( + DocsRecommendation, + FlowCopyOptions, + FlowElements, + FlowLogEntry, + FlowLogsMeta, + FlowLogsResponse, + FlowMetricData, + FlowMetrics, + FlowMetricsApiResponse, + FlowMetricsData, + FlowMetricsMeta, + FlowResponse, ) -from nexla_sdk.models.teams import ( - Team, TeamMember, TeamCreate, TeamUpdate, TeamMemberRequest, TeamMemberList +from nexla_sdk.models.genai import ( + ActiveConfigView, + GenAiConfig, + GenAiConfigCreatePayload, + GenAiConfigPayload, + GenAiOrgSetting, + GenAiOrgSettingPayload, ) -from nexla_sdk.models.projects import ( - Project, ProjectDataFlow, ProjectCreate, ProjectUpdate, ProjectFlowIdentifier, ProjectFlowList +from nexla_sdk.models.lookups import ( + Lookup, + LookupCreate, + LookupEntriesUpsert, + LookupUpdate, ) -from nexla_sdk.models.notifications import ( - Notification, NotificationType, NotificationChannelSetting, NotificationSetting, NotificationCount, - NotificationChannelSettingCreate, NotificationChannelSettingUpdate, NotificationSettingCreate, NotificationSettingUpdate +from nexla_sdk.models.marketplace import ( + CustodiansPayload, + MarketplaceDomain, + MarketplaceDomainCreate, + MarketplaceDomainsItem, + MarketplaceDomainsItemCreate, ) from nexla_sdk.models.metrics import ( - AccountMetrics, DashboardMetrics, MetricsResponse, MetricsByRunResponse, ResourceMetricDaily, ResourceMetricsByRun -) -from nexla_sdk.models.code_containers import ( - CodeContainer, CodeContainerCreate, CodeContainerUpdate, -) -from nexla_sdk.models.transforms import ( - Transform, TransformCreate, TransformUpdate, -) -from nexla_sdk.models.attribute_transforms import ( - AttributeTransform, AttributeTransformCreate, AttributeTransformUpdate, -) -from nexla_sdk.models.async_tasks import ( - AsyncTask, AsyncTaskCreate, AsyncTaskResult, DownloadLink, -) -from nexla_sdk.models.approval_requests import ( - ApprovalRequest, ApprovalDecision, + AccountMetrics, + DashboardMetrics, + MetricsByRunResponse, + MetricsResponse, + ResourceMetricDaily, + ResourceMetricsByRun, ) -from nexla_sdk.models.runtimes import ( - Runtime, RuntimeCreate, RuntimeUpdate, -) -from nexla_sdk.models.marketplace import ( - MarketplaceDomain, MarketplaceDomainsItem, - MarketplaceDomainCreate, MarketplaceDomainsItemCreate, CustodiansPayload, +from nexla_sdk.models.nexsets import ( + DataSinkSimplified, + Nexset, + NexsetCopyOptions, + NexsetCreate, + NexsetSample, + NexsetStatus, + NexsetUpdate, + OutputType, + TransformType, ) -from nexla_sdk.models.org_auth_configs import ( - AuthConfig, AuthConfigPayload, +from nexla_sdk.models.notifications import ( + Notification, + NotificationChannelSetting, + NotificationChannelSettingCreate, + NotificationChannelSettingUpdate, + NotificationCount, + NotificationSetting, + NotificationSettingCreate, + NotificationSettingUpdate, + NotificationType, ) -from nexla_sdk.models.genai import ( - GenAiConfig, GenAiOrgSetting, ActiveConfigView, - GenAiConfigPayload, GenAiConfigCreatePayload, GenAiOrgSettingPayload, +from nexla_sdk.models.org_auth_configs import AuthConfig, AuthConfigPayload +from nexla_sdk.models.organizations import ( + CustodianUser, + OrganizationUpdate, + OrgCustodianRef, + OrgCustodiansPayload, + OrgMember, + OrgMemberDelete, + OrgMemberList, + OrgMemberUpdate, + OrgTier, ) -from nexla_sdk.models.self_signup import ( - SelfSignupRequest, BlockedDomain, +from nexla_sdk.models.projects import ( + Project, + ProjectCreate, + ProjectDataFlow, + ProjectFlowIdentifier, + ProjectFlowList, + ProjectUpdate, ) -from nexla_sdk.models.doc_containers import ( - DocContainer, +from nexla_sdk.models.runtimes import Runtime, RuntimeCreate, RuntimeUpdate +from nexla_sdk.models.self_signup import BlockedDomain, SelfSignupRequest +from nexla_sdk.models.sources import ( + DataSetBrief, + FlowType, + IngestMethod, + RunInfo, + Source, + SourceCopyOptions, + SourceCreate, + SourceStatus, + SourceType, + SourceUpdate, ) -from nexla_sdk.models.data_schemas import ( - DataSchema, +from nexla_sdk.models.teams import ( + Team, + TeamCreate, + TeamMember, + TeamMemberList, + TeamMemberRequest, + TeamUpdate, ) -from nexla_sdk.models.webhooks import ( - WebhookSendOptions, WebhookResponse, +from nexla_sdk.models.transforms import Transform, TransformCreate, TransformUpdate +from nexla_sdk.models.users import ( + AccountSummary, + DefaultOrg, + OrgMembership, + User, + UserCreate, + UserExpanded, + UserSettings, + UserUpdate, ) +from nexla_sdk.models.webhooks import WebhookResponse, WebhookSendOptions __all__ = [ # Base and Common models - 'BaseModel', - 'Owner', - 'Organization', - 'Connector', - 'LogEntry', - 'FlowNode', - + "BaseModel", + "Owner", + "Organization", + "Connector", + "LogEntry", + "FlowNode", # Accessor models - 'UserAccessorRequest', - 'TeamAccessorRequest', - 'OrgAccessorRequest', - 'UserAccessorResponse', - 'TeamAccessorResponse', - 'OrgAccessorResponse', - 'AccessorRequest', - 'AccessorResponse', - 'AccessorsRequest', - 'AccessorRequestList', - 'AccessorResponseList', - 'AccessorType', - + "UserAccessorRequest", + "TeamAccessorRequest", + "OrgAccessorRequest", + "UserAccessorResponse", + "TeamAccessorResponse", + "OrgAccessorResponse", + "AccessorRequest", + "AccessorResponse", + "AccessorsRequest", + "AccessorRequestList", + "AccessorResponseList", + "AccessorType", # General Enums - 'AccessRole', - 'ResourceStatus', - 'ResourceType', - 'NotificationLevel', - 'NotificationChannel', - 'UserTier', - 'UserStatus', - 'OrgMembershipStatus', - 'ConnectorCategory', - + "AccessRole", + "ResourceStatus", + "ResourceType", + "NotificationLevel", + "NotificationChannel", + "UserTier", + "UserStatus", + "OrgMembershipStatus", + "ConnectorCategory", # Credential models and enums - 'CredentialType', - 'VerifiedStatus', - 'Credential', - 'ProbeTreeResponse', - 'ProbeSampleResponse', - 'CredentialCreate', - 'CredentialUpdate', - 'ProbeTreeRequest', - 'ProbeSampleRequest', - + "CredentialType", + "VerifiedStatus", + "Credential", + "ProbeTreeResponse", + "ProbeSampleResponse", + "CredentialCreate", + "CredentialUpdate", + "ProbeTreeRequest", + "ProbeSampleRequest", # Flow models - 'FlowResponse', - 'FlowMetrics', - 'FlowElements', - 'FlowCopyOptions', - 'FlowLogEntry', - 'FlowLogsMeta', - 'FlowLogsResponse', - 'FlowMetricData', - 'FlowMetricsMeta', - 'FlowMetricsData', - 'FlowMetricsApiResponse', - 'DocsRecommendation', - + "FlowResponse", + "FlowMetrics", + "FlowElements", + "FlowCopyOptions", + "FlowLogEntry", + "FlowLogsMeta", + "FlowLogsResponse", + "FlowMetricData", + "FlowMetricsMeta", + "FlowMetricsData", + "FlowMetricsApiResponse", + "DocsRecommendation", # Source models and enums - 'SourceStatus', - 'SourceType', - 'IngestMethod', - 'FlowType', - 'Source', - 'DataSetBrief', - 'RunInfo', - 'SourceCreate', - 'SourceUpdate', - 'SourceCopyOptions', - + "SourceStatus", + "SourceType", + "IngestMethod", + "FlowType", + "Source", + "DataSetBrief", + "RunInfo", + "SourceCreate", + "SourceUpdate", + "SourceCopyOptions", # Destination models and enums - 'DestinationStatus', - 'DestinationType', - 'DestinationFormat', - 'Destination', - 'DataSetInfo', - 'DataMapInfo', - 'DestinationCreate', - 'DestinationUpdate', - 'DestinationCopyOptions', - + "DestinationStatus", + "DestinationType", + "DestinationFormat", + "Destination", + "DataSetInfo", + "DataMapInfo", + "DestinationCreate", + "DestinationUpdate", + "DestinationCopyOptions", # Nexset models and enums - 'NexsetStatus', - 'TransformType', - 'OutputType', - 'Nexset', - 'NexsetSample', - 'DataSinkSimplified', - 'NexsetCreate', - 'NexsetUpdate', - 'NexsetCopyOptions', - + "NexsetStatus", + "TransformType", + "OutputType", + "Nexset", + "NexsetSample", + "DataSinkSimplified", + "NexsetCreate", + "NexsetUpdate", + "NexsetCopyOptions", # Lookup models - 'Lookup', - 'LookupCreate', - 'LookupUpdate', - 'LookupEntriesUpsert', - + "Lookup", + "LookupCreate", + "LookupUpdate", + "LookupEntriesUpsert", # User models - 'User', - 'UserExpanded', - 'UserSettings', - 'DefaultOrg', - 'OrgMembership', - 'AccountSummary', - 'UserCreate', - 'UserUpdate', - + "User", + "UserExpanded", + "UserSettings", + "DefaultOrg", + "OrgMembership", + "AccountSummary", + "UserCreate", + "UserUpdate", # Organization models (note: Organization from common is already listed above) - 'OrgMember', - 'OrgTier', - 'OrganizationUpdate', - 'OrgMemberUpdate', - 'OrgMemberList', - 'OrgMemberDelete', - 'OrgCustodianRef', - 'OrgCustodiansPayload', - 'CustodianUser', - + "OrgMember", + "OrgTier", + "OrganizationUpdate", + "OrgMemberUpdate", + "OrgMemberList", + "OrgMemberDelete", + "OrgCustodianRef", + "OrgCustodiansPayload", + "CustodianUser", # Team models - 'Team', - 'TeamMember', - 'TeamCreate', - 'TeamUpdate', - 'TeamMemberRequest', - 'TeamMemberList', - + "Team", + "TeamMember", + "TeamCreate", + "TeamUpdate", + "TeamMemberRequest", + "TeamMemberList", # Project models - 'Project', - 'ProjectDataFlow', - 'ProjectCreate', - 'ProjectUpdate', - 'ProjectFlowIdentifier', - 'ProjectFlowList', - + "Project", + "ProjectDataFlow", + "ProjectCreate", + "ProjectUpdate", + "ProjectFlowIdentifier", + "ProjectFlowList", # Notification models - 'Notification', - 'NotificationType', - 'NotificationChannelSetting', - 'NotificationSetting', - 'NotificationCount', - 'NotificationChannelSettingCreate', - 'NotificationChannelSettingUpdate', - 'NotificationSettingCreate', - 'NotificationSettingUpdate', - + "Notification", + "NotificationType", + "NotificationChannelSetting", + "NotificationSetting", + "NotificationCount", + "NotificationChannelSettingCreate", + "NotificationChannelSettingUpdate", + "NotificationSettingCreate", + "NotificationSettingUpdate", # Metrics models - 'AccountMetrics', - 'DashboardMetrics', - 'ResourceMetricDaily', - 'ResourceMetricsByRun', - 'MetricsResponse', - 'MetricsByRunResponse', - + "AccountMetrics", + "DashboardMetrics", + "ResourceMetricDaily", + "ResourceMetricsByRun", + "MetricsResponse", + "MetricsByRunResponse", # Code containers - 'CodeContainer', 'CodeContainerCreate', 'CodeContainerUpdate', - + "CodeContainer", + "CodeContainerCreate", + "CodeContainerUpdate", # Transforms - 'Transform', 'TransformCreate', 'TransformUpdate', - + "Transform", + "TransformCreate", + "TransformUpdate", # Attribute transforms - 'AttributeTransform', 'AttributeTransformCreate', 'AttributeTransformUpdate', - + "AttributeTransform", + "AttributeTransformCreate", + "AttributeTransformUpdate", # Async tasks - 'AsyncTask', 'AsyncTaskCreate', 'AsyncTaskResult', 'DownloadLink', - + "AsyncTask", + "AsyncTaskCreate", + "AsyncTaskResult", + "DownloadLink", # Approval requests - 'ApprovalRequest', 'ApprovalDecision', - + "ApprovalRequest", + "ApprovalDecision", # Runtimes - 'Runtime', 'RuntimeCreate', 'RuntimeUpdate', - + "Runtime", + "RuntimeCreate", + "RuntimeUpdate", # Marketplace - 'MarketplaceDomainCreate', - 'MarketplaceDomainsItemCreate', - 'CustodiansPayload', - 'MarketplaceDomain', 'MarketplaceDomainsItem', - + "MarketplaceDomainCreate", + "MarketplaceDomainsItemCreate", + "CustodiansPayload", + "MarketplaceDomain", + "MarketplaceDomainsItem", # Org auth configs - 'AuthConfig', 'AuthConfigPayload', - + "AuthConfig", + "AuthConfigPayload", # GenAI - 'GenAiConfigPayload', - 'GenAiConfigCreatePayload', - 'GenAiOrgSettingPayload', - 'GenAiConfig', 'GenAiOrgSetting', 'ActiveConfigView', - + "GenAiConfigPayload", + "GenAiConfigCreatePayload", + "GenAiOrgSettingPayload", + "GenAiConfig", + "GenAiOrgSetting", + "ActiveConfigView", # Self-signup - 'SelfSignupRequest', 'BlockedDomain', - + "SelfSignupRequest", + "BlockedDomain", # Doc containers / Data schemas - 'DocContainer', 'DataSchema', - + "DocContainer", + "DataSchema", # Webhooks - 'WebhookSendOptions', 'WebhookResponse', + "WebhookSendOptions", + "WebhookResponse", ] diff --git a/nexla_sdk/models/access/__init__.py b/nexla_sdk/models/access/__init__.py index 34928cc..c671a7f 100644 --- a/nexla_sdk/models/access/__init__.py +++ b/nexla_sdk/models/access/__init__.py @@ -1,33 +1,36 @@ """Access control models.""" -from nexla_sdk.models.access.enums import ( - AccessorType +from nexla_sdk.models.access.enums import AccessorType +from nexla_sdk.models.access.requests import ( + AccessorRequest, + AccessorRequestList, + AccessorsRequest, + OrgAccessorRequest, + TeamAccessorRequest, + UserAccessorRequest, ) from nexla_sdk.models.access.responses import ( - UserAccessorResponse, TeamAccessorResponse, OrgAccessorResponse, - AccessorResponse, AccessorResponseList -) -from nexla_sdk.models.access.requests import ( - UserAccessorRequest, TeamAccessorRequest, OrgAccessorRequest, - AccessorRequest, AccessorsRequest, AccessorRequestList + AccessorResponse, + AccessorResponseList, + OrgAccessorResponse, + TeamAccessorResponse, + UserAccessorResponse, ) __all__ = [ # Enums - 'AccessorType', - + "AccessorType", # Responses - 'UserAccessorResponse', - 'TeamAccessorResponse', - 'OrgAccessorResponse', - 'AccessorResponse', - 'AccessorResponseList', - + "UserAccessorResponse", + "TeamAccessorResponse", + "OrgAccessorResponse", + "AccessorResponse", + "AccessorResponseList", # Requests - 'UserAccessorRequest', - 'TeamAccessorRequest', - 'OrgAccessorRequest', - 'AccessorRequest', - 'AccessorsRequest', - 'AccessorRequestList', -] \ No newline at end of file + "UserAccessorRequest", + "TeamAccessorRequest", + "OrgAccessorRequest", + "AccessorRequest", + "AccessorsRequest", + "AccessorRequestList", +] diff --git a/nexla_sdk/models/access/enums.py b/nexla_sdk/models/access/enums.py index 44cc6af..cb3335f 100644 --- a/nexla_sdk/models/access/enums.py +++ b/nexla_sdk/models/access/enums.py @@ -3,6 +3,7 @@ class AccessorType(str, Enum): """Types of accessors.""" + USER = "USER" TEAM = "TEAM" - ORG = "ORG" + ORG = "ORG" diff --git a/nexla_sdk/models/access/requests.py b/nexla_sdk/models/access/requests.py index 404ea9d..e961791 100644 --- a/nexla_sdk/models/access/requests.py +++ b/nexla_sdk/models/access/requests.py @@ -1,21 +1,27 @@ -from typing import List, Optional, Union, Literal +from typing import List, Literal, Optional, Union + from pydantic import Field + +from nexla_sdk.models.access.enums import AccessorType from nexla_sdk.models.base import BaseModel from nexla_sdk.models.enums import AccessRole -from nexla_sdk.models.access.enums import AccessorType class UserAccessorRequest(BaseModel): """Request model for USER type accessor.""" + type: Literal[AccessorType.USER] = AccessorType.USER id: Optional[int] = Field(None, description="Unique ID of the user") email: Optional[str] = Field(None, description="Email of the user") - org_id: Optional[int] = Field(None, description="Organization ID for cross-org access") + org_id: Optional[int] = Field( + None, description="Organization ID for cross-org access" + ) access_roles: List[AccessRole] = Field(description="List of access roles") class TeamAccessorRequest(BaseModel): """Request model for TEAM type accessor.""" + type: Literal[AccessorType.TEAM] = AccessorType.TEAM id: Optional[int] = Field(None, description="Unique ID of the team") name: Optional[str] = Field(None, description="Name of the team") @@ -24,10 +30,15 @@ class TeamAccessorRequest(BaseModel): class OrgAccessorRequest(BaseModel): """Request model for ORG type accessor.""" + type: Literal[AccessorType.ORG] = AccessorType.ORG id: Optional[int] = Field(None, description="Unique ID of the organization") - client_identifier: Optional[str] = Field(None, description="Client identifier for the organization") - email_domain: Optional[str] = Field(None, description="Email domain for the organization") + client_identifier: Optional[str] = Field( + None, description="Client identifier for the organization" + ) + email_domain: Optional[str] = Field( + None, description="Email domain for the organization" + ) access_roles: List[AccessRole] = Field(description="List of access roles") @@ -37,8 +48,9 @@ class OrgAccessorRequest(BaseModel): class AccessorsRequest(BaseModel): """Request model for accessor operations.""" + accessors: List[AccessorRequest] = Field(description="List of accessor requests") # Type aliases for easier usage -AccessorRequestList = List[AccessorRequest] \ No newline at end of file +AccessorRequestList = List[AccessorRequest] diff --git a/nexla_sdk/models/access/responses.py b/nexla_sdk/models/access/responses.py index 3da0b3e..64916ba 100644 --- a/nexla_sdk/models/access/responses.py +++ b/nexla_sdk/models/access/responses.py @@ -1,17 +1,22 @@ -from typing import List, Optional, Union, Literal from datetime import datetime +from typing import List, Literal, Optional, Union + from pydantic import Field + +from nexla_sdk.models.access.enums import AccessorType from nexla_sdk.models.base import BaseModel from nexla_sdk.models.enums import AccessRole -from nexla_sdk.models.access.enums import AccessorType class UserAccessorResponse(BaseModel): """Response model for USER type accessor.""" + type: Literal[AccessorType.USER] = AccessorType.USER id: Optional[int] = Field(None, description="Unique ID of the user") email: Optional[str] = Field(None, description="Email of the user") - org_id: Optional[int] = Field(None, description="Organization ID for cross-org access") + org_id: Optional[int] = Field( + None, description="Organization ID for cross-org access" + ) access_roles: List[AccessRole] = Field(description="List of access roles") created_at: Optional[datetime] = Field(None, description="Creation timestamp") updated_at: Optional[datetime] = Field(None, description="Last update timestamp") @@ -19,6 +24,7 @@ class UserAccessorResponse(BaseModel): class TeamAccessorResponse(BaseModel): """Response model for TEAM type accessor.""" + type: Literal[AccessorType.TEAM] = AccessorType.TEAM id: Optional[int] = Field(None, description="Unique ID of the team") name: Optional[str] = Field(None, description="Name of the team") @@ -29,17 +35,24 @@ class TeamAccessorResponse(BaseModel): class OrgAccessorResponse(BaseModel): """Response model for ORG type accessor.""" + type: Literal[AccessorType.ORG] = AccessorType.ORG id: Optional[int] = Field(None, description="Unique ID of the organization") - client_identifier: Optional[str] = Field(None, description="Client identifier for the organization") - email_domain: Optional[str] = Field(None, description="Email domain for the organization") + client_identifier: Optional[str] = Field( + None, description="Client identifier for the organization" + ) + email_domain: Optional[str] = Field( + None, description="Email domain for the organization" + ) access_roles: List[AccessRole] = Field(description="List of access roles") created_at: Optional[datetime] = Field(None, description="Creation timestamp") updated_at: Optional[datetime] = Field(None, description="Last update timestamp") # Union type for any accessor response -AccessorResponse = Union[UserAccessorResponse, TeamAccessorResponse, OrgAccessorResponse] +AccessorResponse = Union[ + UserAccessorResponse, TeamAccessorResponse, OrgAccessorResponse +] # Type aliases for easier usage diff --git a/nexla_sdk/models/approval_requests/__init__.py b/nexla_sdk/models/approval_requests/__init__.py index 4ef91c2..ccf8050 100644 --- a/nexla_sdk/models/approval_requests/__init__.py +++ b/nexla_sdk/models/approval_requests/__init__.py @@ -1,8 +1,7 @@ -from .responses import ApprovalRequest from .requests import ApprovalDecision +from .responses import ApprovalRequest __all__ = [ - 'ApprovalRequest', - 'ApprovalDecision', + "ApprovalRequest", + "ApprovalDecision", ] - diff --git a/nexla_sdk/models/approval_requests/requests.py b/nexla_sdk/models/approval_requests/requests.py index d10b96d..d6a254d 100644 --- a/nexla_sdk/models/approval_requests/requests.py +++ b/nexla_sdk/models/approval_requests/requests.py @@ -6,4 +6,3 @@ class ApprovalDecision(BaseModel): approved: bool reason: Optional[str] = None - diff --git a/nexla_sdk/models/approval_requests/responses.py b/nexla_sdk/models/approval_requests/responses.py index 0a2a7ce..0ef7ecf 100644 --- a/nexla_sdk/models/approval_requests/responses.py +++ b/nexla_sdk/models/approval_requests/responses.py @@ -14,4 +14,3 @@ class ApprovalRequest(BaseModel): reason: Optional[str] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None - diff --git a/nexla_sdk/models/async_tasks/__init__.py b/nexla_sdk/models/async_tasks/__init__.py index a56729b..d88b660 100644 --- a/nexla_sdk/models/async_tasks/__init__.py +++ b/nexla_sdk/models/async_tasks/__init__.py @@ -1,10 +1,9 @@ -from .responses import AsyncTask, AsyncTaskResult, DownloadLink from .requests import AsyncTaskCreate +from .responses import AsyncTask, AsyncTaskResult, DownloadLink __all__ = [ - 'AsyncTask', - 'AsyncTaskResult', - 'DownloadLink', - 'AsyncTaskCreate', + "AsyncTask", + "AsyncTaskResult", + "DownloadLink", + "AsyncTaskCreate", ] - diff --git a/nexla_sdk/models/async_tasks/requests.py b/nexla_sdk/models/async_tasks/requests.py index 82787e2..5efce1c 100644 --- a/nexla_sdk/models/async_tasks/requests.py +++ b/nexla_sdk/models/async_tasks/requests.py @@ -11,6 +11,7 @@ class AsyncTaskCreate(BaseModel): priority: Optional task priority arguments: Arguments for the task """ + type: str priority: Optional[int] = None arguments: Dict[str, Any] diff --git a/nexla_sdk/models/async_tasks/responses.py b/nexla_sdk/models/async_tasks/responses.py index c3a9a2f..51fdebf 100644 --- a/nexla_sdk/models/async_tasks/responses.py +++ b/nexla_sdk/models/async_tasks/responses.py @@ -24,4 +24,3 @@ class AsyncTaskResult(BaseModel): class DownloadLink(BaseModel): url: str expires_at: Optional[datetime] = None - diff --git a/nexla_sdk/models/attribute_transforms/__init__.py b/nexla_sdk/models/attribute_transforms/__init__.py index cb7fafb..6230dd1 100644 --- a/nexla_sdk/models/attribute_transforms/__init__.py +++ b/nexla_sdk/models/attribute_transforms/__init__.py @@ -1,9 +1,8 @@ -from .responses import AttributeTransform from .requests import AttributeTransformCreate, AttributeTransformUpdate +from .responses import AttributeTransform __all__ = [ - 'AttributeTransform', - 'AttributeTransformCreate', - 'AttributeTransformUpdate', + "AttributeTransform", + "AttributeTransformCreate", + "AttributeTransformUpdate", ] - diff --git a/nexla_sdk/models/attribute_transforms/requests.py b/nexla_sdk/models/attribute_transforms/requests.py index c030107..b65bfb1 100644 --- a/nexla_sdk/models/attribute_transforms/requests.py +++ b/nexla_sdk/models/attribute_transforms/requests.py @@ -31,4 +31,3 @@ class AttributeTransformUpdate(BaseModel): custom_config: Optional[Dict[str, Any]] = None data_credentials_id: Optional[int] = None runtime_data_credentials_id: Optional[int] = None - diff --git a/nexla_sdk/models/attribute_transforms/responses.py b/nexla_sdk/models/attribute_transforms/responses.py index d069e95..64a7074 100644 --- a/nexla_sdk/models/attribute_transforms/responses.py +++ b/nexla_sdk/models/attribute_transforms/responses.py @@ -27,4 +27,3 @@ class AttributeTransform(BaseModel): updated_at: Optional[datetime] = None created_at: Optional[datetime] = None tags: Optional[List[str]] = None - diff --git a/nexla_sdk/models/base.py b/nexla_sdk/models/base.py index ace320d..eac124a 100644 --- a/nexla_sdk/models/base.py +++ b/nexla_sdk/models/base.py @@ -1,14 +1,16 @@ -from typing import TypeVar, Any, Dict import json -from pydantic import BaseModel as PydanticBaseModel, ConfigDict +from typing import Any, Dict, TypeVar -T = TypeVar('T', bound='BaseModel') +from pydantic import BaseModel as PydanticBaseModel +from pydantic import ConfigDict + +T = TypeVar("T", bound="BaseModel") class BaseModel(PydanticBaseModel): """ Base model class with Pydantic functionality and Nexla API compatibility. - + Features: - Automatically ignores unknown fields from API responses - Supports both camelCase and snake_case field names @@ -17,7 +19,7 @@ class BaseModel(PydanticBaseModel): - Validates data types automatically - Easy logging and printing support """ - + model_config = ConfigDict( # Ignore unknown fields from API responses extra="allow", @@ -34,47 +36,49 @@ class BaseModel(PydanticBaseModel): # Validate default values validate_default=True, # Allow both snake_case and camelCase field names - from_attributes=True + from_attributes=True, ) - + def to_dict(self, exclude_none: bool = True) -> Dict[str, Any]: """ Convert model to dictionary. - + Args: exclude_none: Whether to exclude None values - + Returns: Dictionary representation """ return self.model_dump(exclude_none=exclude_none) - + def to_json(self, exclude_none: bool = True, indent: int = 2) -> str: """ Convert model to JSON string. - + Args: exclude_none: Whether to exclude None values indent: JSON indentation level - + Returns: JSON string representation """ - return json.dumps(self.to_dict(exclude_none=exclude_none), indent=indent, default=str) - + return json.dumps( + self.to_dict(exclude_none=exclude_none), indent=indent, default=str + ) + def __str__(self) -> str: """ String representation of the model. - + Returns: Formatted string showing model name and key fields """ # Get model name model_name = self.__class__.__name__ - + # Get key fields for display (limit to avoid too much output) data = self.to_dict(exclude_none=True) - + # Show first few key fields key_fields = [] for key, value in list(data.items())[:5]: # Show first 5 fields @@ -82,19 +86,19 @@ def __str__(self) -> str: key_fields.append(f"{key}='{value}'") else: key_fields.append(f"{key}={value}") - + field_str = ", ".join(key_fields) - + # Add "..." if there are more fields if len(data) > 5: field_str += ", ..." - + return f"{model_name}({field_str})" - + def __repr__(self) -> str: """ Detailed string representation of the model. - + Returns: Detailed string representation """ diff --git a/nexla_sdk/models/code_containers/__init__.py b/nexla_sdk/models/code_containers/__init__.py index 3f5e273..138ed41 100644 --- a/nexla_sdk/models/code_containers/__init__.py +++ b/nexla_sdk/models/code_containers/__init__.py @@ -1,9 +1,8 @@ -from .responses import CodeContainer from .requests import CodeContainerCreate, CodeContainerUpdate +from .responses import CodeContainer __all__ = [ - 'CodeContainer', - 'CodeContainerCreate', - 'CodeContainerUpdate', + "CodeContainer", + "CodeContainerCreate", + "CodeContainerUpdate", ] - diff --git a/nexla_sdk/models/code_containers/requests.py b/nexla_sdk/models/code_containers/requests.py index ab2f410..482f3d7 100644 --- a/nexla_sdk/models/code_containers/requests.py +++ b/nexla_sdk/models/code_containers/requests.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional from nexla_sdk.models.base import BaseModel + from .responses import CodeOperation @@ -37,4 +38,3 @@ class CodeContainerUpdate(BaseModel): data_credentials_id: Optional[int] = None runtime_data_credentials_id: Optional[int] = None ai_function_type: Optional[str] = None - diff --git a/nexla_sdk/models/code_containers/responses.py b/nexla_sdk/models/code_containers/responses.py index c72bf6d..770c2c3 100644 --- a/nexla_sdk/models/code_containers/responses.py +++ b/nexla_sdk/models/code_containers/responses.py @@ -36,4 +36,3 @@ class CodeContainer(BaseModel): updated_at: Optional[datetime] = None created_at: Optional[datetime] = None tags: Optional[List[str]] = None - diff --git a/nexla_sdk/models/common.py b/nexla_sdk/models/common.py index bfe2b9c..23ec243 100644 --- a/nexla_sdk/models/common.py +++ b/nexla_sdk/models/common.py @@ -1,10 +1,12 @@ -from typing import List, Optional, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional + from nexla_sdk.models.base import BaseModel class Owner(BaseModel): """User who owns a resource.""" + id: int full_name: str email: str @@ -13,6 +15,7 @@ class Owner(BaseModel): class Organization(BaseModel): """Organization details.""" + id: int name: str email_domain: Optional[str] = None @@ -30,6 +33,7 @@ class Organization(BaseModel): class Connector(BaseModel): """Connector information.""" + id: int type: str connection_type: str @@ -40,6 +44,7 @@ class Connector(BaseModel): class LogEntry(BaseModel): """Audit log entry.""" + id: int item_type: str item_id: int @@ -60,6 +65,7 @@ class LogEntry(BaseModel): class FlowNode(BaseModel): """Flow node in a data pipeline.""" + id: int origin_node_id: int parent_node_id: Optional[int] = None @@ -72,4 +78,4 @@ class FlowNode(BaseModel): ingestion_mode: Optional[str] = None name: Optional[str] = None description: Optional[str] = None - children: Optional[List['FlowNode']] = None \ No newline at end of file + children: Optional[List["FlowNode"]] = None diff --git a/nexla_sdk/models/credentials/__init__.py b/nexla_sdk/models/credentials/__init__.py index 30004a6..1e87da7 100644 --- a/nexla_sdk/models/credentials/__init__.py +++ b/nexla_sdk/models/credentials/__init__.py @@ -1,25 +1,27 @@ -from nexla_sdk.models.credentials.enums import ( - CredentialType, VerifiedStatus +from nexla_sdk.models.credentials.enums import CredentialType, VerifiedStatus +from nexla_sdk.models.credentials.requests import ( + CredentialCreate, + CredentialUpdate, + ProbeSampleRequest, + ProbeTreeRequest, ) from nexla_sdk.models.credentials.responses import ( - Credential, ProbeTreeResponse, ProbeSampleResponse -) -from nexla_sdk.models.credentials.requests import ( - CredentialCreate, CredentialUpdate, - ProbeTreeRequest, ProbeSampleRequest + Credential, + ProbeSampleResponse, + ProbeTreeResponse, ) __all__ = [ # Enums - 'CredentialType', - 'VerifiedStatus', + "CredentialType", + "VerifiedStatus", # Responses - 'Credential', - 'ProbeTreeResponse', - 'ProbeSampleResponse', + "Credential", + "ProbeTreeResponse", + "ProbeSampleResponse", # Requests - 'CredentialCreate', - 'CredentialUpdate', - 'ProbeTreeRequest', - 'ProbeSampleRequest', -] \ No newline at end of file + "CredentialCreate", + "CredentialUpdate", + "ProbeTreeRequest", + "ProbeSampleRequest", +] diff --git a/nexla_sdk/models/credentials/enums.py b/nexla_sdk/models/credentials/enums.py index 8381c60..97b2e4f 100644 --- a/nexla_sdk/models/credentials/enums.py +++ b/nexla_sdk/models/credentials/enums.py @@ -3,6 +3,7 @@ class CredentialType(str, Enum): """Supported credential types.""" + AS400 = "as400" AWS_ATHENA = "aws_athena" AZURE_BLB = "azure_blb" @@ -59,6 +60,7 @@ class CredentialType(str, Enum): class VerifiedStatus(str, Enum): """Credential verification status.""" + VERIFIED = "VERIFIED" UNVERIFIED = "UNVERIFIED" - FAILED = "FAILED" \ No newline at end of file + FAILED = "FAILED" diff --git a/nexla_sdk/models/credentials/requests.py b/nexla_sdk/models/credentials/requests.py index d9c79b1..ab095ac 100644 --- a/nexla_sdk/models/credentials/requests.py +++ b/nexla_sdk/models/credentials/requests.py @@ -1,9 +1,11 @@ -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional + from nexla_sdk.models.base import BaseModel class CredentialCreate(BaseModel): """Request model for creating a credential.""" + name: str credentials_type: str description: Optional[str] = None @@ -19,6 +21,7 @@ class CredentialCreate(BaseModel): class CredentialUpdate(BaseModel): """Request model for updating a credential.""" + name: Optional[str] = None description: Optional[str] = None credentials: Optional[Dict[str, Any]] = None @@ -26,6 +29,7 @@ class CredentialUpdate(BaseModel): class ProbeTreeRequest(BaseModel): """Request for probing storage structure.""" + depth: int path: Optional[str] = None # For file systems database: Optional[str] = None # For databases @@ -34,5 +38,6 @@ class ProbeTreeRequest(BaseModel): class ProbeSampleRequest(BaseModel): """Request for previewing connector content.""" + # For file connectors path: Optional[str] = None diff --git a/nexla_sdk/models/credentials/responses.py b/nexla_sdk/models/credentials/responses.py index d7c9909..06c115f 100644 --- a/nexla_sdk/models/credentials/responses.py +++ b/nexla_sdk/models/credentials/responses.py @@ -1,12 +1,15 @@ -from typing import List, Optional, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional + from pydantic import Field, field_validator + from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.common import Owner, Organization, Connector +from nexla_sdk.models.common import Connector, Organization, Owner class Credential(BaseModel): """Data credential response model.""" + id: int name: str credentials_type: str @@ -15,7 +18,7 @@ class Credential(BaseModel): access_roles: Optional[List[str]] = None verified_status: Optional[str] = None connector: Optional[Connector] = None - + description: Optional[str] = None credentials_version: Optional[str] = None api_keys: Optional[List[Dict[str, Any]]] = None @@ -30,8 +33,8 @@ class Credential(BaseModel): created_at: Optional[datetime] = None updated_at: Optional[datetime] = None managed: bool = False - - @field_validator('access_roles', mode='before') + + @field_validator("access_roles", mode="before") @classmethod def validate_access_roles(cls, v): """Handle access_roles with None values.""" @@ -40,8 +43,8 @@ def validate_access_roles(cls, v): if isinstance(v, list): return [role for role in v if role is not None] return v - - @field_validator('tags', mode='before') + + @field_validator("tags", mode="before") @classmethod def validate_tags(cls, v): """Handle None tags.""" @@ -52,6 +55,7 @@ def validate_tags(cls, v): class ProbeTreeResponse(BaseModel): """Response from credential probe tree operation.""" + status: str message: str connection_type: str @@ -60,6 +64,7 @@ class ProbeTreeResponse(BaseModel): class ProbeSampleResponse(BaseModel): """Response from credential probe sample operation.""" + status: str message: str connection_type: str diff --git a/nexla_sdk/models/data_schemas/__init__.py b/nexla_sdk/models/data_schemas/__init__.py index 65101c6..ad39b39 100644 --- a/nexla_sdk/models/data_schemas/__init__.py +++ b/nexla_sdk/models/data_schemas/__init__.py @@ -1,6 +1,5 @@ from .responses import DataSchema __all__ = [ - 'DataSchema', + "DataSchema", ] - diff --git a/nexla_sdk/models/data_schemas/responses.py b/nexla_sdk/models/data_schemas/responses.py index 0e238ae..9758934 100644 --- a/nexla_sdk/models/data_schemas/responses.py +++ b/nexla_sdk/models/data_schemas/responses.py @@ -6,4 +6,3 @@ class DataSchema(BaseModel): id: int name: Optional[str] = None - diff --git a/nexla_sdk/models/destinations/__init__.py b/nexla_sdk/models/destinations/__init__.py index f861322..ebba0f3 100644 --- a/nexla_sdk/models/destinations/__init__.py +++ b/nexla_sdk/models/destinations/__init__.py @@ -1,24 +1,30 @@ from nexla_sdk.models.destinations.enums import ( - DestinationStatus, DestinationType, DestinationFormat -) -from nexla_sdk.models.destinations.responses import ( - Destination, DataSetInfo, DataMapInfo + DestinationFormat, + DestinationStatus, + DestinationType, ) from nexla_sdk.models.destinations.requests import ( - DestinationCreate, DestinationUpdate, DestinationCopyOptions + DestinationCopyOptions, + DestinationCreate, + DestinationUpdate, +) +from nexla_sdk.models.destinations.responses import ( + DataMapInfo, + DataSetInfo, + Destination, ) __all__ = [ # Enums - 'DestinationStatus', - 'DestinationType', - 'DestinationFormat', + "DestinationStatus", + "DestinationType", + "DestinationFormat", # Responses - 'Destination', - 'DataSetInfo', - 'DataMapInfo', + "Destination", + "DataSetInfo", + "DataMapInfo", # Requests - 'DestinationCreate', - 'DestinationUpdate', - 'DestinationCopyOptions', -] \ No newline at end of file + "DestinationCreate", + "DestinationUpdate", + "DestinationCopyOptions", +] diff --git a/nexla_sdk/models/destinations/enums.py b/nexla_sdk/models/destinations/enums.py index fa4693c..7b9e0f7 100644 --- a/nexla_sdk/models/destinations/enums.py +++ b/nexla_sdk/models/destinations/enums.py @@ -3,6 +3,7 @@ class DestinationStatus(str, Enum): """Destination status values.""" + ACTIVE = "ACTIVE" PAUSED = "PAUSED" DRAFT = "DRAFT" @@ -12,55 +13,86 @@ class DestinationStatus(str, Enum): class DestinationType(str, Enum): """Supported sink types.""" + # File Systems S3 = "s3" GCS = "gcs" AZURE_BLB = "azure_blb" + AZURE_DATA_LAKE = "azure_data_lake" FTP = "ftp" DROPBOX = "dropbox" BOX = "box" GDRIVE = "gdrive" SHAREPOINT = "sharepoint" - + MIN_IO_S3 = "min_io_s3" + WEBDAV = "webdav" + # Databases MYSQL = "mysql" POSTGRES = "postgres" + SUPABASE = "supabase" SQLSERVER = "sqlserver" ORACLE = "oracle" + ORACLE_AUTONOMOUS = "oracle_autonomous" REDSHIFT = "redshift" SNOWFLAKE = "snowflake" + SNOWFLAKE_DCR = "snowflake_dcr" BIGQUERY = "bigquery" DATABRICKS = "databricks" - + AS400 = "as400" + AWS_ATHENA = "aws_athena" + AZURE_SYNAPSE = "azure_synapse" + CLOUDSQL_MYSQL = "cloudsql_mysql" + CLOUDSQL_POSTGRES = "cloudsql_postgres" + CLOUDSQL_SQLSERVER = "cloudsql_sqlserver" + DB2 = "db2" + FIREBOLT = "firebolt" + GCP_ALLOYDB = "gcp_alloydb" + GCP_SPANNER = "gcp_spanner" + HANA_JDBC = "hana_jdbc" + HIVE = "hive" + NETSUITE_JDBC = "netsuite_jdbc" + SYBASE = "sybase" + TERADATA = "teradata" + + # Delta Lake / Iceberg + DELTA_LAKE_AZURE_BLB = "delta_lake_azure_blb" + DELTA_LAKE_AZURE_DATA_LAKE = "delta_lake_azure_data_lake" + DELTA_LAKE_S3 = "delta_lake_s3" + S3_ICEBERG = "s3_iceberg" + # NoSQL MONGO = "mongo" DYNAMODB = "dynamodb" FIREBASE = "firebase" - - # Streaming + + # Streaming / Messaging KAFKA = "kafka" CONFLUENT_KAFKA = "confluent_kafka" GOOGLE_PUBSUB = "google_pubsub" - + JMS = "jms" + TIBCO = "tibco" + # APIs REST = "rest" - + SOAP = "soap" + # Special EMAIL = "email" DATA_MAP = "data_map" - + NEXLA_MONITOR = "nexla_monitor" + # Vector Databases PINECONE = "pinecone" - - # Add all other types from the spec... class DestinationFormat(str, Enum): """Output format for destinations.""" + JSON = "json" CSV = "csv" PARQUET = "parquet" AVRO = "avro" XML = "xml" DELIMITED = "delimited" - FIXED_WIDTH = "fixed_width" \ No newline at end of file + FIXED_WIDTH = "fixed_width" diff --git a/nexla_sdk/models/destinations/requests.py b/nexla_sdk/models/destinations/requests.py index 94ee0cb..aa7f5d3 100644 --- a/nexla_sdk/models/destinations/requests.py +++ b/nexla_sdk/models/destinations/requests.py @@ -1,15 +1,17 @@ -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional + from nexla_sdk.models.base import BaseModel class DestinationCreate(BaseModel): """Request model for creating a destination.""" + name: str sink_type: str data_credentials_id: int data_set_id: int description: Optional[str] = None - + # In case of Core Sinks only sink_config: Optional[Dict] = None @@ -20,6 +22,7 @@ class DestinationCreate(BaseModel): class DestinationUpdate(BaseModel): """Request model for updating a destination.""" + name: Optional[str] = None description: Optional[str] = None sink_config: Optional[Dict[str, Any]] = None @@ -29,7 +32,8 @@ class DestinationUpdate(BaseModel): class DestinationCopyOptions(BaseModel): """Options for copying a destination.""" + reuse_data_credentials: bool = False copy_access_controls: bool = False owner_id: Optional[int] = None - org_id: Optional[int] = None \ No newline at end of file + org_id: Optional[int] = None diff --git a/nexla_sdk/models/destinations/responses.py b/nexla_sdk/models/destinations/responses.py index 727862d..481b183 100644 --- a/nexla_sdk/models/destinations/responses.py +++ b/nexla_sdk/models/destinations/responses.py @@ -1,14 +1,17 @@ -from typing import List, Optional, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.common import Owner, Organization, Connector +from nexla_sdk.models.common import Connector, Organization, Owner from nexla_sdk.models.credentials.responses import Credential from nexla_sdk.models.destinations.enums import DestinationFormat class DataSetInfo(BaseModel): """Basic dataset information for destination.""" + id: int name: str description: Optional[str] = None @@ -21,6 +24,7 @@ class DataSetInfo(BaseModel): class DataMapInfo(BaseModel): """Basic data map information for destination.""" + id: int owner_id: int org_id: int @@ -33,6 +37,7 @@ class DataMapInfo(BaseModel): class Destination(BaseModel): """Destination (data sink) response model.""" + id: int name: str status: str @@ -43,7 +48,7 @@ class Destination(BaseModel): access_roles: Optional[List[str]] = None managed: Optional[bool] = None connector: Optional[Connector] = None - + description: Optional[str] = None data_set_id: Optional[int] = None data_map_id: Optional[int] = None diff --git a/nexla_sdk/models/doc_containers/__init__.py b/nexla_sdk/models/doc_containers/__init__.py index 2d1e50b..a0f9f0f 100644 --- a/nexla_sdk/models/doc_containers/__init__.py +++ b/nexla_sdk/models/doc_containers/__init__.py @@ -1,6 +1,5 @@ from .responses import DocContainer __all__ = [ - 'DocContainer', + "DocContainer", ] - diff --git a/nexla_sdk/models/doc_containers/responses.py b/nexla_sdk/models/doc_containers/responses.py index 02e6c28..6b78a8d 100644 --- a/nexla_sdk/models/doc_containers/responses.py +++ b/nexla_sdk/models/doc_containers/responses.py @@ -6,4 +6,3 @@ class DocContainer(BaseModel): id: int name: Optional[str] = None - diff --git a/nexla_sdk/models/enums.py b/nexla_sdk/models/enums.py index d9d7c4e..f5a9a33 100644 --- a/nexla_sdk/models/enums.py +++ b/nexla_sdk/models/enums.py @@ -3,6 +3,7 @@ class AccessRole(str, Enum): """Access roles for resources.""" + OWNER = "owner" ADMIN = "admin" OPERATOR = "operator" @@ -11,6 +12,7 @@ class AccessRole(str, Enum): class ResourceStatus(str, Enum): """Common resource status values.""" + ACTIVE = "ACTIVE" PAUSED = "PAUSED" DRAFT = "DRAFT" @@ -22,6 +24,7 @@ class ResourceStatus(str, Enum): class ResourceType(str, Enum): """Resource types in Nexla.""" + ORG = "ORG" USER = "USER" TEAM = "TEAM" @@ -42,6 +45,7 @@ class ResourceType(str, Enum): class NotificationLevel(str, Enum): """Notification levels.""" + DEBUG = "DEBUG" INFO = "INFO" WARN = "WARN" @@ -52,6 +56,7 @@ class NotificationLevel(str, Enum): class NotificationChannel(str, Enum): """Notification delivery channels.""" + APP = "APP" EMAIL = "EMAIL" SMS = "SMS" @@ -61,6 +66,7 @@ class NotificationChannel(str, Enum): class UserTier(str, Enum): """User account tiers.""" + FREE = "FREE" TRIAL = "TRIAL" PAID = "PAID" @@ -69,6 +75,7 @@ class UserTier(str, Enum): class UserStatus(str, Enum): """User account status.""" + ACTIVE = "ACTIVE" DEACTIVATED = "DEACTIVATED" SOURCE_COUNT_CAPPED = "SOURCE_COUNT_CAPPED" @@ -78,12 +85,14 @@ class UserStatus(str, Enum): class OrgMembershipStatus(str, Enum): """Organization membership status.""" + ACTIVE = "ACTIVE" DEACTIVATED = "DEACTIVATED" class ConnectorCategory(str, Enum): """Connector categories.""" + FILE = "file" DATABASE = "database" NOSQL = "nosql" diff --git a/nexla_sdk/models/flows/__init__.py b/nexla_sdk/models/flows/__init__.py index 4e35881..6660b30 100644 --- a/nexla_sdk/models/flows/__init__.py +++ b/nexla_sdk/models/flows/__init__.py @@ -1,24 +1,31 @@ +from nexla_sdk.models.flows.requests import FlowCopyOptions from nexla_sdk.models.flows.responses import ( - FlowResponse, FlowMetrics, FlowElements, - FlowLogEntry, FlowLogsMeta, FlowLogsResponse, - FlowMetricData, FlowMetricsMeta, FlowMetricsData, FlowMetricsApiResponse, - DocsRecommendation + DocsRecommendation, + FlowElements, + FlowLogEntry, + FlowLogsMeta, + FlowLogsResponse, + FlowMetricData, + FlowMetrics, + FlowMetricsApiResponse, + FlowMetricsData, + FlowMetricsMeta, + FlowResponse, ) -from nexla_sdk.models.flows.requests import FlowCopyOptions __all__ = [ # Responses - 'FlowResponse', - 'FlowMetrics', - 'FlowElements', - 'FlowLogEntry', - 'FlowLogsMeta', - 'FlowLogsResponse', - 'FlowMetricData', - 'FlowMetricsMeta', - 'FlowMetricsData', - 'FlowMetricsApiResponse', - 'DocsRecommendation', + "FlowResponse", + "FlowMetrics", + "FlowElements", + "FlowLogEntry", + "FlowLogsMeta", + "FlowLogsResponse", + "FlowMetricData", + "FlowMetricsMeta", + "FlowMetricsData", + "FlowMetricsApiResponse", + "DocsRecommendation", # Requests - 'FlowCopyOptions', -] \ No newline at end of file + "FlowCopyOptions", +] diff --git a/nexla_sdk/models/flows/requests.py b/nexla_sdk/models/flows/requests.py index fdd2505..395225a 100644 --- a/nexla_sdk/models/flows/requests.py +++ b/nexla_sdk/models/flows/requests.py @@ -1,11 +1,13 @@ from typing import Optional + from nexla_sdk.models.base import BaseModel class FlowCopyOptions(BaseModel): """Options for copying a flow.""" + reuse_data_credentials: bool = False copy_access_controls: bool = False copy_dependent_data_flows: bool = False owner_id: Optional[int] = None - org_id: Optional[int] = None \ No newline at end of file + org_id: Optional[int] = None diff --git a/nexla_sdk/models/flows/responses.py b/nexla_sdk/models/flows/responses.py index ab6647d..57761a3 100644 --- a/nexla_sdk/models/flows/responses.py +++ b/nexla_sdk/models/flows/responses.py @@ -1,16 +1,19 @@ -from typing import List, Optional, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel from nexla_sdk.models.common import FlowNode -from nexla_sdk.models.sources.responses import Source -from nexla_sdk.models.nexsets.responses import Nexset -from nexla_sdk.models.destinations.responses import Destination from nexla_sdk.models.credentials.responses import Credential +from nexla_sdk.models.destinations.responses import Destination +from nexla_sdk.models.nexsets.responses import Nexset +from nexla_sdk.models.sources.responses import Source class FlowMetrics(BaseModel): """Flow metrics information.""" + origin_node_id: int records: int size: int @@ -21,6 +24,7 @@ class FlowMetrics(BaseModel): class FlowLogEntry(BaseModel): """A single flow execution log entry.""" + timestamp: Optional[datetime] = None level: Optional[str] = None message: Optional[str] = None @@ -32,6 +36,7 @@ class FlowLogEntry(BaseModel): class FlowLogsMeta(BaseModel): """Metadata for flow logs pagination.""" + current_page: Optional[int] = Field(default=None, alias="currentPage") page_count: Optional[int] = Field(default=None, alias="pageCount") total_count: Optional[int] = Field(default=None, alias="totalCount") @@ -46,6 +51,7 @@ class FlowLogsResponse(BaseModel): logs: List of log entries. meta: Pagination metadata. """ + status: Optional[int] = None message: Optional[str] = None logs: List[FlowLogEntry] = Field(default_factory=list) @@ -54,6 +60,7 @@ class FlowLogsResponse(BaseModel): class FlowMetricData(BaseModel): """Flow metric data for a resource.""" + records: Optional[int] = None size: Optional[int] = None errors: Optional[int] = None @@ -63,6 +70,7 @@ class FlowMetricData(BaseModel): class FlowMetricsMeta(BaseModel): """Metadata for flow metrics pagination.""" + current_page: Optional[int] = Field(default=None, alias="currentPage") page_count: Optional[int] = Field(default=None, alias="pageCount") total_count: Optional[int] = Field(default=None, alias="totalCount") @@ -70,6 +78,7 @@ class FlowMetricsMeta(BaseModel): class FlowMetricsData(BaseModel): """Flow metrics data container.""" + data: Optional[Dict[str, Any]] = None meta: Optional[FlowMetricsMeta] = None @@ -82,6 +91,7 @@ class FlowMetricsApiResponse(BaseModel): message: Status message ("Ok" for success). metrics: Metrics data including resource-keyed data and pagination. """ + status: Optional[int] = None message: Optional[str] = None metrics: Optional[FlowMetricsData] = None @@ -94,12 +104,14 @@ class DocsRecommendation(BaseModel): recommendation: The AI-generated documentation suggestion. status: Status of the recommendation request. """ + recommendation: Optional[str] = None status: Optional[str] = None class FlowElements(BaseModel): """Flow elements containing all resources.""" + code_containers: List[Dict[str, Any]] = Field(default_factory=list) data_sources: List[Source] = Field(default_factory=list) data_sets: List[Nexset] = Field(default_factory=list) @@ -113,6 +125,7 @@ class FlowElements(BaseModel): class FlowResponse(BaseModel): """Flow response model.""" + flows: List[FlowNode] # Include flow elements when not flows_only code_containers: Optional[List[Dict[str, Any]]] = None @@ -124,4 +137,4 @@ class FlowResponse(BaseModel): orgs: Optional[List[Dict[str, Any]]] = None users: Optional[List[Dict[str, Any]]] = None projects: Optional[List[Dict[str, Any]]] = None - metrics: Optional[List[FlowMetrics]] = None \ No newline at end of file + metrics: Optional[List[FlowMetrics]] = None diff --git a/nexla_sdk/models/genai/__init__.py b/nexla_sdk/models/genai/__init__.py index 9fd9baf..95360ec 100644 --- a/nexla_sdk/models/genai/__init__.py +++ b/nexla_sdk/models/genai/__init__.py @@ -1,13 +1,15 @@ -from .responses import GenAiConfig, GenAiOrgSetting, ActiveConfigView from .requests import ( - GenAiConfigPayload, GenAiConfigCreatePayload, GenAiOrgSettingPayload, + GenAiConfigCreatePayload, + GenAiConfigPayload, + GenAiOrgSettingPayload, ) +from .responses import ActiveConfigView, GenAiConfig, GenAiOrgSetting __all__ = [ - 'GenAiConfig', - 'GenAiOrgSetting', - 'ActiveConfigView', - 'GenAiConfigPayload', - 'GenAiConfigCreatePayload', - 'GenAiOrgSettingPayload', + "GenAiConfig", + "GenAiOrgSetting", + "ActiveConfigView", + "GenAiConfigPayload", + "GenAiConfigCreatePayload", + "GenAiOrgSettingPayload", ] diff --git a/nexla_sdk/models/genai/requests.py b/nexla_sdk/models/genai/requests.py index 8cbf7d4..71130f2 100644 --- a/nexla_sdk/models/genai/requests.py +++ b/nexla_sdk/models/genai/requests.py @@ -1,4 +1,4 @@ -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional from nexla_sdk.models.base import BaseModel @@ -25,4 +25,3 @@ class GenAiOrgSettingPayload(BaseModel): org_id: Optional[int] = None gen_ai_config_id: int gen_ai_usage: str # all | gen_docs | check_code - diff --git a/nexla_sdk/models/genai/responses.py b/nexla_sdk/models/genai/responses.py index 2e7d89d..b0f9259 100644 --- a/nexla_sdk/models/genai/responses.py +++ b/nexla_sdk/models/genai/responses.py @@ -26,4 +26,3 @@ class GenAiOrgSetting(BaseModel): class ActiveConfigView(BaseModel): gen_ai_usage: Optional[str] = None active_config: Optional[Dict[str, Any]] = None - diff --git a/nexla_sdk/models/lookups/__init__.py b/nexla_sdk/models/lookups/__init__.py index fda9a7e..1fb7ab0 100644 --- a/nexla_sdk/models/lookups/__init__.py +++ b/nexla_sdk/models/lookups/__init__.py @@ -1,13 +1,15 @@ -from nexla_sdk.models.lookups.responses import Lookup from nexla_sdk.models.lookups.requests import ( - LookupCreate, LookupUpdate, LookupEntriesUpsert + LookupCreate, + LookupEntriesUpsert, + LookupUpdate, ) +from nexla_sdk.models.lookups.responses import Lookup __all__ = [ # Responses - 'Lookup', + "Lookup", # Requests - 'LookupCreate', - 'LookupUpdate', - 'LookupEntriesUpsert', -] \ No newline at end of file + "LookupCreate", + "LookupUpdate", + "LookupEntriesUpsert", +] diff --git a/nexla_sdk/models/lookups/requests.py b/nexla_sdk/models/lookups/requests.py index 6d09e2c..a7bacac 100644 --- a/nexla_sdk/models/lookups/requests.py +++ b/nexla_sdk/models/lookups/requests.py @@ -1,10 +1,13 @@ -from typing import Optional, Dict, Any, List +from typing import Any, Dict, List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel class LookupCreate(BaseModel): """Request model for creating a lookup.""" + name: str data_type: str map_primary_key: str @@ -17,6 +20,7 @@ class LookupCreate(BaseModel): class LookupUpdate(BaseModel): """Request model for updating a lookup.""" + name: Optional[str] = None description: Optional[str] = None map_primary_key: Optional[str] = None @@ -27,4 +31,5 @@ class LookupUpdate(BaseModel): class LookupEntriesUpsert(BaseModel): """Request model for upserting lookup entries.""" - entries: List[Dict[str, Any]] \ No newline at end of file + + entries: List[Dict[str, Any]] diff --git a/nexla_sdk/models/lookups/responses.py b/nexla_sdk/models/lookups/responses.py index 9985f5f..49c75de 100644 --- a/nexla_sdk/models/lookups/responses.py +++ b/nexla_sdk/models/lookups/responses.py @@ -1,12 +1,15 @@ -from typing import List, Optional, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.common import Owner, Organization +from nexla_sdk.models.common import Organization, Owner class Lookup(BaseModel): """Lookup (data map) response model.""" + id: int name: str description: str @@ -19,7 +22,7 @@ class Lookup(BaseModel): data_type: str emit_data_default: bool use_versioning: bool - + data_format: Optional[str] = None data_sink_id: Optional[int] = None data_defaults: Dict[str, Any] = Field(default_factory=dict) @@ -28,4 +31,4 @@ class Lookup(BaseModel): map_entry_schema: Optional[Dict[str, Any]] = None tags: List[str] = Field(default_factory=list) created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None \ No newline at end of file + updated_at: Optional[datetime] = None diff --git a/nexla_sdk/models/marketplace/__init__.py b/nexla_sdk/models/marketplace/__init__.py index efd3b6e..8c0dfeb 100644 --- a/nexla_sdk/models/marketplace/__init__.py +++ b/nexla_sdk/models/marketplace/__init__.py @@ -1,13 +1,16 @@ -from .responses import MarketplaceDomain, MarketplaceDomainsItem from .requests import ( - MarketplaceDomainCreate, MarketplaceDomainsItemCreate, CustodiansPayload, CustodianRef, + CustodianRef, + CustodiansPayload, + MarketplaceDomainCreate, + MarketplaceDomainsItemCreate, ) +from .responses import MarketplaceDomain, MarketplaceDomainsItem __all__ = [ - 'MarketplaceDomain', - 'MarketplaceDomainsItem', - 'MarketplaceDomainCreate', - 'MarketplaceDomainsItemCreate', - 'CustodiansPayload', - 'CustodianRef', + "MarketplaceDomain", + "MarketplaceDomainsItem", + "MarketplaceDomainCreate", + "MarketplaceDomainsItemCreate", + "CustodiansPayload", + "CustodianRef", ] diff --git a/nexla_sdk/models/marketplace/requests.py b/nexla_sdk/models/marketplace/requests.py index 9139520..5027e17 100644 --- a/nexla_sdk/models/marketplace/requests.py +++ b/nexla_sdk/models/marketplace/requests.py @@ -5,6 +5,7 @@ class CustodianRef(BaseModel): """Reference to a user for custodians payload (by id or email).""" + id: Optional[int] = None email: Optional[str] = None @@ -26,4 +27,3 @@ class MarketplaceDomainsItemCreate(BaseModel): name: str description: Optional[str] = None data_set_id: int - diff --git a/nexla_sdk/models/metrics/__init__.py b/nexla_sdk/models/metrics/__init__.py index a24b62f..34561d9 100644 --- a/nexla_sdk/models/metrics/__init__.py +++ b/nexla_sdk/models/metrics/__init__.py @@ -1,7 +1,11 @@ from .enums import ResourceType, UserMetricResourceType from .responses import ( - AccountMetrics, DashboardMetrics, MetricsResponse, - MetricsByRunResponse, ResourceMetricDaily, ResourceMetricsByRun + AccountMetrics, + DashboardMetrics, + MetricsByRunResponse, + MetricsResponse, + ResourceMetricDaily, + ResourceMetricsByRun, ) __all__ = [ @@ -10,9 +14,9 @@ "UserMetricResourceType", # Response models "AccountMetrics", - "DashboardMetrics", + "DashboardMetrics", "MetricsResponse", "MetricsByRunResponse", "ResourceMetricDaily", "ResourceMetricsByRun", -] \ No newline at end of file +] diff --git a/nexla_sdk/models/metrics/enums.py b/nexla_sdk/models/metrics/enums.py index 33a542c..fc0064a 100644 --- a/nexla_sdk/models/metrics/enums.py +++ b/nexla_sdk/models/metrics/enums.py @@ -3,14 +3,16 @@ class ResourceType(str, Enum): """Valid resource types for metrics endpoints.""" + # For resource metrics endpoints (/{resource_type}/{resource_id}/metrics) DATA_SOURCES = "data_sources" - DATA_SINKS = "data_sinks" + DATA_SINKS = "data_sinks" DATA_SETS = "data_sets" class UserMetricResourceType(str, Enum): """Valid resource types for user metrics endpoints.""" + # For user metrics endpoints (/users/{user_id}/metrics) SOURCE = "SOURCE" - SINK = "SINK" \ No newline at end of file + SINK = "SINK" diff --git a/nexla_sdk/models/metrics/responses.py b/nexla_sdk/models/metrics/responses.py index f5feee6..96494e5 100644 --- a/nexla_sdk/models/metrics/responses.py +++ b/nexla_sdk/models/metrics/responses.py @@ -1,15 +1,18 @@ -from typing import List, Optional, Dict, Any +from typing import Any, Dict, List, Optional + from nexla_sdk.models.base import BaseModel class AccountMetrics(BaseModel): """Account utilization metrics.""" + status: int metrics: List[Dict[str, Any]] class DashboardMetricSet(BaseModel): """Dashboard metric set for a resource.""" + records: int size: int errors: int @@ -18,12 +21,14 @@ class DashboardMetricSet(BaseModel): class DashboardMetrics(BaseModel): """24-hour dashboard metrics.""" + status: int metrics: Dict[str, Any] class ResourceMetricDaily(BaseModel): """Daily resource metrics.""" + time: str # Date in YYYY-MM-DD format records: int size: int @@ -32,6 +37,7 @@ class ResourceMetricDaily(BaseModel): class ResourceMetricsByRun(BaseModel): """Resource metrics grouped by run.""" + runId: Optional[int] = None lastWritten: Optional[int] = None dataSetId: int @@ -42,11 +48,13 @@ class ResourceMetricsByRun(BaseModel): class MetricsResponse(BaseModel): """Generic metrics response.""" + status: int metrics: List[Any] # Can be different types class MetricsByRunResponse(BaseModel): """Metrics by run response with pagination.""" + status: int metrics: Dict[str, Any] # Contains data and meta diff --git a/nexla_sdk/models/nexsets/__init__.py b/nexla_sdk/models/nexsets/__init__.py index d12f3dc..07db4b7 100644 --- a/nexla_sdk/models/nexsets/__init__.py +++ b/nexla_sdk/models/nexsets/__init__.py @@ -1,24 +1,22 @@ -from nexla_sdk.models.nexsets.enums import ( - NexsetStatus, TransformType, OutputType -) -from nexla_sdk.models.nexsets.responses import ( - Nexset, NexsetSample, DataSinkSimplified -) +from nexla_sdk.models.nexsets.enums import NexsetStatus, OutputType, TransformType from nexla_sdk.models.nexsets.requests import ( - NexsetCreate, NexsetUpdate, NexsetCopyOptions + NexsetCopyOptions, + NexsetCreate, + NexsetUpdate, ) +from nexla_sdk.models.nexsets.responses import DataSinkSimplified, Nexset, NexsetSample __all__ = [ # Enums - 'NexsetStatus', - 'TransformType', - 'OutputType', + "NexsetStatus", + "TransformType", + "OutputType", # Responses - 'Nexset', - 'NexsetSample', - 'DataSinkSimplified', + "Nexset", + "NexsetSample", + "DataSinkSimplified", # Requests - 'NexsetCreate', - 'NexsetUpdate', - 'NexsetCopyOptions', -] \ No newline at end of file + "NexsetCreate", + "NexsetUpdate", + "NexsetCopyOptions", +] diff --git a/nexla_sdk/models/nexsets/enums.py b/nexla_sdk/models/nexsets/enums.py index 45f6f53..bd50207 100644 --- a/nexla_sdk/models/nexsets/enums.py +++ b/nexla_sdk/models/nexsets/enums.py @@ -3,6 +3,7 @@ class NexsetStatus(str, Enum): """Nexset status values.""" + ACTIVE = "ACTIVE" PAUSED = "PAUSED" DRAFT = "DRAFT" @@ -13,6 +14,7 @@ class NexsetStatus(str, Enum): class TransformType(str, Enum): """Transform types.""" + JOLT_STANDARD = "jolt_standard" JOLT_CUSTOM = "jolt_custom" PYTHON = "python" @@ -22,6 +24,7 @@ class TransformType(str, Enum): class OutputType(str, Enum): """Transform output types.""" + RECORD = "record" ATTRIBUTE = "attribute" - CUSTOM = "custom" \ No newline at end of file + CUSTOM = "custom" diff --git a/nexla_sdk/models/nexsets/requests.py b/nexla_sdk/models/nexsets/requests.py index e937dfe..a911d53 100644 --- a/nexla_sdk/models/nexsets/requests.py +++ b/nexla_sdk/models/nexsets/requests.py @@ -1,19 +1,23 @@ """Request models for nexsets.""" -from typing import Optional, Dict, Any, List, Union + +from typing import Any, Dict, List, Optional, Union + from pydantic import Field + from nexla_sdk.models.base import BaseModel class NexsetCreate(BaseModel): """Request model for creating a nexset.""" + name: str parent_data_set_id: int has_custom_transform: bool - + # One of these must be provided based on has_custom_transform transform: Optional[Dict[str, Any]] = None transform_id: Optional[int] = None - + description: Optional[str] = None output_schema_annotations: Optional[Dict[str, Any]] = None output_schema_validation_enabled: bool = False @@ -25,6 +29,7 @@ class NexsetCreate(BaseModel): class NexsetUpdate(BaseModel): """Request model for updating a nexset.""" + name: Optional[str] = None description: Optional[str] = None has_custom_transform: Optional[bool] = None @@ -40,6 +45,7 @@ class NexsetUpdate(BaseModel): class NexsetCopyOptions(BaseModel): """Options for copying a nexset.""" + copy_access_controls: bool = False owner_id: Optional[int] = None org_id: Optional[int] = None diff --git a/nexla_sdk/models/nexsets/responses.py b/nexla_sdk/models/nexsets/responses.py index 44c0162..3affaf3 100644 --- a/nexla_sdk/models/nexsets/responses.py +++ b/nexla_sdk/models/nexsets/responses.py @@ -1,24 +1,28 @@ -from typing import List, Optional, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional + from pydantic import Field, model_validator + from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.common import Owner, Organization -from nexla_sdk.models.sources.responses import DataSetBrief, Source +from nexla_sdk.models.common import Organization, Owner from nexla_sdk.models.destinations.enums import DestinationType +from nexla_sdk.models.sources.responses import DataSetBrief, Source class DataSinkSimplified(BaseModel): """Simplified data sink information.""" + id: int - owner_id: int - org_id: int + owner_id: Optional[int] = None + org_id: Optional[int] = None name: str status: Optional[str] = None - sink_type: Optional[DestinationType ] = Field(default=None, alias="sinkType") + sink_type: Optional[DestinationType] = Field(default=None, alias="sinkType") class Nexset(BaseModel): """Nexset (data set) response model.""" + id: int name: Optional[str] = None description: Optional[str] = None @@ -27,7 +31,7 @@ class Nexset(BaseModel): org: Optional[Organization] = None access_roles: Optional[List[str]] = None flow_type: Optional[str] = Field(default=None, alias="flowType") - + data_source_id: Optional[int] = None data_source: Optional[Source] = None parent_data_sets: List[DataSetBrief] = Field(default_factory=list) @@ -42,21 +46,21 @@ class Nexset(BaseModel): class NexsetSample(BaseModel): """Nexset sample record.""" + raw_message: Dict[str, Any] = Field(alias="rawMessage") - nexla_metadata: Optional[Dict[str, Any]] = Field(default=None, alias="nexlaMetaData") - - @model_validator(mode='before') + nexla_metadata: Optional[Dict[str, Any]] = Field( + default=None, alias="nexlaMetaData" + ) + + @model_validator(mode="before") @classmethod def handle_formats(cls, data): """Handle both formats - with and without metadata.""" if isinstance(data, dict): # If rawMessage exists, use it; otherwise treat whole dict as raw_message - if 'rawMessage' in data: + if "rawMessage" in data: return data - elif 'raw_message' not in data: + elif "raw_message" not in data: # Direct record format - entire dict is the raw message - return { - 'raw_message': data, - 'nexla_metadata': None - } - return data \ No newline at end of file + return {"raw_message": data, "nexla_metadata": None} + return data diff --git a/nexla_sdk/models/notifications/__init__.py b/nexla_sdk/models/notifications/__init__.py index 26bb1de..9d24357 100644 --- a/nexla_sdk/models/notifications/__init__.py +++ b/nexla_sdk/models/notifications/__init__.py @@ -1,22 +1,27 @@ -from nexla_sdk.models.notifications.responses import ( - Notification, NotificationType, NotificationChannelSetting, - NotificationSetting, NotificationCount -) from nexla_sdk.models.notifications.requests import ( - NotificationChannelSettingCreate, NotificationChannelSettingUpdate, - NotificationSettingCreate, NotificationSettingUpdate + NotificationChannelSettingCreate, + NotificationChannelSettingUpdate, + NotificationSettingCreate, + NotificationSettingUpdate, +) +from nexla_sdk.models.notifications.responses import ( + Notification, + NotificationChannelSetting, + NotificationCount, + NotificationSetting, + NotificationType, ) __all__ = [ # Responses - 'Notification', - 'NotificationType', - 'NotificationChannelSetting', - 'NotificationSetting', - 'NotificationCount', + "Notification", + "NotificationType", + "NotificationChannelSetting", + "NotificationSetting", + "NotificationCount", # Requests - 'NotificationChannelSettingCreate', - 'NotificationChannelSettingUpdate', - 'NotificationSettingCreate', - 'NotificationSettingUpdate', + "NotificationChannelSettingCreate", + "NotificationChannelSettingUpdate", + "NotificationSettingCreate", + "NotificationSettingUpdate", ] diff --git a/nexla_sdk/models/notifications/requests.py b/nexla_sdk/models/notifications/requests.py index 71f0185..a97eb1a 100644 --- a/nexla_sdk/models/notifications/requests.py +++ b/nexla_sdk/models/notifications/requests.py @@ -1,22 +1,27 @@ -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel class NotificationChannelSettingCreate(BaseModel): """Request model for creating notification channel setting.""" + channel: str # APP, EMAIL, SMS, SLACK, WEBHOOKS config: Dict[str, Any] class NotificationChannelSettingUpdate(BaseModel): """Request model for updating notification channel setting.""" + channel: Optional[str] = None config: Optional[Dict[str, Any]] = None class NotificationSettingCreate(BaseModel): """Request model for creating notification setting.""" + channel: str notification_type_id: int status: Optional[str] = None # PAUSED, ACTIVE @@ -28,6 +33,7 @@ class NotificationSettingCreate(BaseModel): class NotificationSettingUpdate(BaseModel): """Request model for updating notification setting.""" + channel: Optional[str] = None status: Optional[str] = None config: Optional[Dict[str, Any]] = None diff --git a/nexla_sdk/models/notifications/responses.py b/nexla_sdk/models/notifications/responses.py index 9439000..5b46a53 100644 --- a/nexla_sdk/models/notifications/responses.py +++ b/nexla_sdk/models/notifications/responses.py @@ -1,22 +1,25 @@ -from typing import List, Optional, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.common import Owner, Organization +from nexla_sdk.models.common import Organization, Owner class Notification(BaseModel): """Notification response model.""" + id: int owner: Owner org: Organization access_roles: List[str] level: str - resource_id: int + resource_id: Optional[int] = None resource_type: str message_id: int message: str - + read_at: Optional[datetime] = None created_at: Optional[datetime] = None updated_at: Optional[datetime] = None @@ -24,6 +27,7 @@ class Notification(BaseModel): class NotificationType(BaseModel): """Notification type information.""" + id: int name: str description: str @@ -36,6 +40,7 @@ class NotificationType(BaseModel): class NotificationChannelSetting(BaseModel): """Notification channel configuration.""" + id: int owner_id: int org_id: int @@ -45,6 +50,7 @@ class NotificationChannelSetting(BaseModel): class NotificationSetting(BaseModel): """Notification setting configuration.""" + id: int org_id: int owner_id: int @@ -59,11 +65,12 @@ class NotificationSetting(BaseModel): category: str event_type: str resource_type: str - + config: Dict[str, Any] = Field(default_factory=dict) priority: Optional[int] = None class NotificationCount(BaseModel): """Notification count response.""" + count: int diff --git a/nexla_sdk/models/org_auth_configs/__init__.py b/nexla_sdk/models/org_auth_configs/__init__.py index e74dd27..fdbe397 100644 --- a/nexla_sdk/models/org_auth_configs/__init__.py +++ b/nexla_sdk/models/org_auth_configs/__init__.py @@ -1,8 +1,7 @@ -from .responses import AuthConfig from .requests import AuthConfigPayload +from .responses import AuthConfig __all__ = [ - 'AuthConfig', - 'AuthConfigPayload', + "AuthConfig", + "AuthConfigPayload", ] - diff --git a/nexla_sdk/models/org_auth_configs/requests.py b/nexla_sdk/models/org_auth_configs/requests.py index b9a6c8d..2a0692d 100644 --- a/nexla_sdk/models/org_auth_configs/requests.py +++ b/nexla_sdk/models/org_auth_configs/requests.py @@ -28,4 +28,3 @@ class AuthConfigPayload(BaseModel): oidc_keys_url_key: Optional[str] = None oidc_id_claims: Optional[Dict[str, Any]] = None oidc_access_claims: Optional[Dict[str, Any]] = None - diff --git a/nexla_sdk/models/org_auth_configs/responses.py b/nexla_sdk/models/org_auth_configs/responses.py index a7a51f7..a9c2524 100644 --- a/nexla_sdk/models/org_auth_configs/responses.py +++ b/nexla_sdk/models/org_auth_configs/responses.py @@ -33,4 +33,3 @@ class AuthConfig(BaseModel): client_config: Optional[Dict[str, Any]] = None updated_at: Optional[datetime] = None created_at: Optional[datetime] = None - diff --git a/nexla_sdk/models/organizations/__init__.py b/nexla_sdk/models/organizations/__init__.py index 24b49f7..cbc9b1b 100644 --- a/nexla_sdk/models/organizations/__init__.py +++ b/nexla_sdk/models/organizations/__init__.py @@ -1,37 +1,42 @@ -from nexla_sdk.models.organizations.responses import ( - Organization, OrgMember, OrgTier, AccountSummary, CustodianUser +from nexla_sdk.models.organizations.custodians import ( + OrgCustodianRef, + OrgCustodiansPayload, ) from nexla_sdk.models.organizations.requests import ( OrganizationCreate, OrganizationUpdate, + OrgMemberActivateDeactivateRequest, OrgMemberCreateRequest, - OrgMemberUpdate, - OrgMemberList, - OrgMemberDeleteRequest, OrgMemberDelete, - OrgMemberActivateDeactivateRequest + OrgMemberDeleteRequest, + OrgMemberList, + OrgMemberUpdate, ) -from nexla_sdk.models.organizations.custodians import ( - OrgCustodianRef, OrgCustodiansPayload, +from nexla_sdk.models.organizations.responses import ( + AccountSummary, + CustodianUser, + Organization, + OrgMember, + OrgTier, ) __all__ = [ # Responses - 'Organization', - 'OrgMember', - 'OrgTier', - 'AccountSummary', - 'CustodianUser', + "Organization", + "OrgMember", + "OrgTier", + "AccountSummary", + "CustodianUser", # Requests - 'OrganizationCreate', - 'OrganizationUpdate', - 'OrgMemberCreateRequest', - 'OrgMemberUpdate', - 'OrgMemberList', - 'OrgMemberDeleteRequest', - 'OrgMemberDelete', - 'OrgMemberActivateDeactivateRequest', + "OrganizationCreate", + "OrganizationUpdate", + "OrgMemberCreateRequest", + "OrgMemberUpdate", + "OrgMemberList", + "OrgMemberDeleteRequest", + "OrgMemberDelete", + "OrgMemberActivateDeactivateRequest", # Custodians - 'OrgCustodianRef', - 'OrgCustodiansPayload', + "OrgCustodianRef", + "OrgCustodiansPayload", ] diff --git a/nexla_sdk/models/organizations/custodians.py b/nexla_sdk/models/organizations/custodians.py index 3053bd4..7a3bf46 100644 --- a/nexla_sdk/models/organizations/custodians.py +++ b/nexla_sdk/models/organizations/custodians.py @@ -1,15 +1,16 @@ -from typing import Optional, List +from typing import List, Optional from nexla_sdk.models.base import BaseModel class OrgCustodianRef(BaseModel): """Reference to a user for organization custodians (by id or email).""" + id: Optional[int] = None email: Optional[str] = None class OrgCustodiansPayload(BaseModel): """Payload for organization custodians endpoints.""" - custodians: List[OrgCustodianRef] + custodians: List[OrgCustodianRef] diff --git a/nexla_sdk/models/organizations/requests.py b/nexla_sdk/models/organizations/requests.py index c0ab169..54919d5 100644 --- a/nexla_sdk/models/organizations/requests.py +++ b/nexla_sdk/models/organizations/requests.py @@ -1,15 +1,18 @@ -from typing import Optional, List, Dict, Any +from typing import Any, Dict, List, Optional + from nexla_sdk.models.base import BaseModel class OrgOwnerRequest(BaseModel): """Request model for specifying an org owner.""" + full_name: str email: str class OrgMemberCreateRequest(BaseModel): """Request model for creating an org member.""" + full_name: str email: str admin: bool = False @@ -17,6 +20,7 @@ class OrgMemberCreateRequest(BaseModel): class OrganizationCreate(BaseModel): """Request model for creating an organization.""" + name: str email_domain: str owner: Optional[OrgOwnerRequest] = None @@ -31,6 +35,7 @@ class OrganizationCreate(BaseModel): class OrganizationUpdate(BaseModel): """Request model for updating an organization.""" + name: Optional[str] = None description: Optional[str] = None owner: Optional[OrgOwnerRequest] = None @@ -43,6 +48,7 @@ class OrganizationUpdate(BaseModel): class OrgMemberUpdate(BaseModel): """Request model for updating org member.""" + id: Optional[int] = None email: Optional[str] = None full_name: Optional[str] = None @@ -52,11 +58,13 @@ class OrgMemberUpdate(BaseModel): class OrgMemberList(BaseModel): """Request model for updating org members.""" + members: List[OrgMemberUpdate] class OrgMemberDeleteRequest(BaseModel): """Request model for deleting a single org member.""" + id: Optional[int] = None email: Optional[str] = None delegate_owner_id: Optional[int] = None @@ -64,9 +72,11 @@ class OrgMemberDeleteRequest(BaseModel): class OrgMemberDelete(BaseModel): """Request model for deleting org members.""" + members: List[OrgMemberDeleteRequest] class OrgMemberActivateDeactivateRequest(BaseModel): """Request model for activating/deactivating org members.""" - members: List[Dict[str, Any]] \ No newline at end of file + + members: List[Dict[str, Any]] diff --git a/nexla_sdk/models/organizations/responses.py b/nexla_sdk/models/organizations/responses.py index 7b27248..a0a4722 100644 --- a/nexla_sdk/models/organizations/responses.py +++ b/nexla_sdk/models/organizations/responses.py @@ -1,12 +1,15 @@ -from typing import List, Optional, Dict from datetime import datetime +from typing import Dict, List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel from nexla_sdk.models.users.responses import User class OrgTier(BaseModel): """Organization tier information.""" + id: int name: str display_name: str @@ -18,6 +21,7 @@ class OrgTier(BaseModel): class Organization(BaseModel): """Organization response model.""" + id: int name: str email_domain: Optional[str] = None @@ -37,7 +41,7 @@ class Organization(BaseModel): default_cluster_id: Optional[int] = None billing_owner: Optional[User] = None admins: List[User] = Field(default_factory=list) - org_tier: Optional[OrgTier] = Field(default=None, alias='account_tier') + org_tier: Optional[OrgTier] = Field(default=None, alias="account_tier") account_tier_display_name: Optional[str] = None account_tier_name: Optional[str] = None email_domain_verified_at: Optional[datetime] = None @@ -48,10 +52,11 @@ class Organization(BaseModel): class OrgMember(BaseModel): """Organization member information.""" + id: int full_name: str email: str - is_admin: bool = Field(..., alias='is_admin?') + is_admin: bool = Field(..., alias="is_admin?") access_role: Optional[List[str]] = None org_membership_status: str user_status: str @@ -59,6 +64,7 @@ class OrgMember(BaseModel): class AccountSummary(BaseModel): """Organization account summary statistics.""" + org_id: int data_sources: Dict[str, int] data_sets: Dict[str, Dict[str, int]] @@ -67,6 +73,7 @@ class AccountSummary(BaseModel): class CustodianUser(BaseModel): """Simplified user view for organization custodians endpoints.""" + id: int email: Optional[str] = None full_name: Optional[str] = None diff --git a/nexla_sdk/models/projects/__init__.py b/nexla_sdk/models/projects/__init__.py index e908d78..45fa15d 100644 --- a/nexla_sdk/models/projects/__init__.py +++ b/nexla_sdk/models/projects/__init__.py @@ -1,15 +1,18 @@ -from nexla_sdk.models.projects.responses import Project, ProjectDataFlow from nexla_sdk.models.projects.requests import ( - ProjectCreate, ProjectUpdate, ProjectFlowIdentifier, ProjectFlowList + ProjectCreate, + ProjectFlowIdentifier, + ProjectFlowList, + ProjectUpdate, ) +from nexla_sdk.models.projects.responses import Project, ProjectDataFlow __all__ = [ # Responses - 'Project', - 'ProjectDataFlow', + "Project", + "ProjectDataFlow", # Requests - 'ProjectCreate', - 'ProjectUpdate', - 'ProjectFlowIdentifier', - 'ProjectFlowList', -] \ No newline at end of file + "ProjectCreate", + "ProjectUpdate", + "ProjectFlowIdentifier", + "ProjectFlowList", +] diff --git a/nexla_sdk/models/projects/requests.py b/nexla_sdk/models/projects/requests.py index dba5122..47ae5ba 100644 --- a/nexla_sdk/models/projects/requests.py +++ b/nexla_sdk/models/projects/requests.py @@ -1,16 +1,20 @@ -from typing import Optional, List +from typing import List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel class ProjectFlowIdentifier(BaseModel): """Flow identifier for project.""" + data_source_id: Optional[int] = None data_set_id: Optional[int] = None class ProjectCreate(BaseModel): """Request model for creating a project.""" + name: str description: Optional[str] = None data_flows: List[ProjectFlowIdentifier] = Field(default_factory=list) @@ -18,6 +22,7 @@ class ProjectCreate(BaseModel): class ProjectUpdate(BaseModel): """Request model for updating a project.""" + name: Optional[str] = None description: Optional[str] = None data_flows: Optional[List[ProjectFlowIdentifier]] = None @@ -25,5 +30,6 @@ class ProjectUpdate(BaseModel): class ProjectFlowList(BaseModel): """Request model for managing project flows.""" + data_flows: Optional[List[ProjectFlowIdentifier]] = None flows: Optional[List[int]] = None # Alternative using flow node IDs diff --git a/nexla_sdk/models/projects/responses.py b/nexla_sdk/models/projects/responses.py index 7e4e122..417419a 100644 --- a/nexla_sdk/models/projects/responses.py +++ b/nexla_sdk/models/projects/responses.py @@ -1,12 +1,15 @@ -from typing import List, Optional from datetime import datetime +from typing import List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.common import Owner, Organization +from nexla_sdk.models.common import Organization, Owner class ProjectDataFlow(BaseModel): """Project data flow information.""" + id: int project_id: int data_source_id: Optional[int] = None @@ -20,13 +23,14 @@ class ProjectDataFlow(BaseModel): class Project(BaseModel): """Project response model.""" + id: int owner: Owner org: Organization name: str description: str access_roles: List[str] - + # Optional fields data_flows: List[ProjectDataFlow] = Field(default_factory=list) flows: List[ProjectDataFlow] = Field(default_factory=list) diff --git a/nexla_sdk/models/runtimes/__init__.py b/nexla_sdk/models/runtimes/__init__.py index 5771ec8..340880d 100644 --- a/nexla_sdk/models/runtimes/__init__.py +++ b/nexla_sdk/models/runtimes/__init__.py @@ -1,9 +1,8 @@ -from .responses import Runtime from .requests import RuntimeCreate, RuntimeUpdate +from .responses import Runtime __all__ = [ - 'Runtime', - 'RuntimeCreate', - 'RuntimeUpdate', + "Runtime", + "RuntimeCreate", + "RuntimeUpdate", ] - diff --git a/nexla_sdk/models/runtimes/requests.py b/nexla_sdk/models/runtimes/requests.py index e1f6167..336a7d6 100644 --- a/nexla_sdk/models/runtimes/requests.py +++ b/nexla_sdk/models/runtimes/requests.py @@ -1,10 +1,11 @@ -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional from nexla_sdk.models.base import BaseModel class RuntimeCreate(BaseModel): """Create payload for Custom Runtime matching OpenAPI RuntimePayload.""" + name: str description: Optional[str] = None active: Optional[bool] = None @@ -15,6 +16,7 @@ class RuntimeCreate(BaseModel): class RuntimeUpdate(BaseModel): """Update payload for Custom Runtime matching OpenAPI RuntimePayload.""" + name: Optional[str] = None description: Optional[str] = None active: Optional[bool] = None diff --git a/nexla_sdk/models/runtimes/responses.py b/nexla_sdk/models/runtimes/responses.py index 93a7446..c0074c3 100644 --- a/nexla_sdk/models/runtimes/responses.py +++ b/nexla_sdk/models/runtimes/responses.py @@ -1,11 +1,12 @@ from datetime import datetime -from typing import Optional, Dict, Any +from typing import Any, Dict, Optional from nexla_sdk.models.base import BaseModel class Runtime(BaseModel): """Response model for Custom Runtime aligned with OpenAPI Runtime schema.""" + id: int name: str description: Optional[str] = None diff --git a/nexla_sdk/models/self_signup/__init__.py b/nexla_sdk/models/self_signup/__init__.py index 3981514..bfbf369 100644 --- a/nexla_sdk/models/self_signup/__init__.py +++ b/nexla_sdk/models/self_signup/__init__.py @@ -1,7 +1,6 @@ -from .responses import SelfSignupRequest, BlockedDomain +from .responses import BlockedDomain, SelfSignupRequest __all__ = [ - 'SelfSignupRequest', - 'BlockedDomain', + "SelfSignupRequest", + "BlockedDomain", ] - diff --git a/nexla_sdk/models/self_signup/responses.py b/nexla_sdk/models/self_signup/responses.py index f1e954a..9bb4a39 100644 --- a/nexla_sdk/models/self_signup/responses.py +++ b/nexla_sdk/models/self_signup/responses.py @@ -17,4 +17,3 @@ class SelfSignupRequest(BaseModel): class BlockedDomain(BaseModel): id: int domain: str - diff --git a/nexla_sdk/models/sources/__init__.py b/nexla_sdk/models/sources/__init__.py index 0121975..6df9d18 100644 --- a/nexla_sdk/models/sources/__init__.py +++ b/nexla_sdk/models/sources/__init__.py @@ -1,25 +1,28 @@ from nexla_sdk.models.sources.enums import ( - SourceStatus, SourceType, IngestMethod, FlowType -) -from nexla_sdk.models.sources.responses import ( - Source, DataSetBrief, RunInfo + FlowType, + IngestMethod, + SourceStatus, + SourceType, ) from nexla_sdk.models.sources.requests import ( - SourceCreate, SourceUpdate, SourceCopyOptions + SourceCopyOptions, + SourceCreate, + SourceUpdate, ) +from nexla_sdk.models.sources.responses import DataSetBrief, RunInfo, Source __all__ = [ # Enums - 'SourceStatus', - 'SourceType', - 'IngestMethod', - 'FlowType', + "SourceStatus", + "SourceType", + "IngestMethod", + "FlowType", # Responses - 'Source', - 'DataSetBrief', - 'RunInfo', + "Source", + "DataSetBrief", + "RunInfo", # Requests - 'SourceCreate', - 'SourceUpdate', - 'SourceCopyOptions', -] \ No newline at end of file + "SourceCreate", + "SourceUpdate", + "SourceCopyOptions", +] diff --git a/nexla_sdk/models/sources/enums.py b/nexla_sdk/models/sources/enums.py index 929d75a..49b6284 100644 --- a/nexla_sdk/models/sources/enums.py +++ b/nexla_sdk/models/sources/enums.py @@ -1,9 +1,11 @@ """Enums for sources.""" + from enum import Enum class SourceStatus(str, Enum): """Source status values.""" + ACTIVE = "ACTIVE" PAUSED = "PAUSED" DRAFT = "DRAFT" @@ -13,6 +15,7 @@ class SourceStatus(str, Enum): class SourceType(str, Enum): """Supported source types.""" + # File Systems S3 = "s3" GCS = "gcs" @@ -22,7 +25,7 @@ class SourceType(str, Enum): BOX = "box" GDRIVE = "gdrive" SHAREPOINT = "sharepoint" - + # Databases MYSQL = "mysql" POSTGRES = "postgres" @@ -32,32 +35,33 @@ class SourceType(str, Enum): SNOWFLAKE = "snowflake" BIGQUERY = "bigquery" DATABRICKS = "databricks" - + # NoSQL MONGO = "mongo" DYNAMODB = "dynamodb" FIREBASE = "firebase" - + # Streaming KAFKA = "kafka" CONFLUENT_KAFKA = "confluent_kafka" GOOGLE_PUBSUB = "google_pubsub" - + # APIs REST = "rest" SOAP = "soap" NEXLA_REST = "nexla_rest" - + # Special FILE_UPLOAD = "file_upload" EMAIL = "email" NEXLA_MONITOR = "nexla_monitor" - + # Add all other types from the spec... class IngestMethod(str, Enum): """Data ingestion methods.""" + BATCH = "BATCH" STREAMING = "STREAMING" REAL_TIME = "REAL_TIME" @@ -67,6 +71,7 @@ class IngestMethod(str, Enum): class FlowType(str, Enum): """Flow processing types.""" + BATCH = "batch" STREAMING = "streaming" - REAL_TIME = "real_time" \ No newline at end of file + REAL_TIME = "real_time" diff --git a/nexla_sdk/models/sources/requests.py b/nexla_sdk/models/sources/requests.py index c014d1e..5ffb2ac 100644 --- a/nexla_sdk/models/sources/requests.py +++ b/nexla_sdk/models/sources/requests.py @@ -1,10 +1,13 @@ """Request models for sources.""" -from typing import Optional, Dict, Any + +from typing import Any, Dict, Optional + from nexla_sdk.models.base import BaseModel class SourceCreate(BaseModel): """Request model for creating a source.""" + name: str source_type: str data_credentials_id: Optional[int] = None @@ -14,13 +17,14 @@ class SourceCreate(BaseModel): source_config: Optional[Dict] = None # For Templatized APIs - vendor_endpoint_id: Optional[int] = None + vendor_endpoint_id: Optional[int] = None ingest_method: Optional[str] = None template_config: Optional[Dict] = None class SourceUpdate(BaseModel): """Request model for updating a source.""" + name: Optional[str] = None description: Optional[str] = None source_config: Optional[Dict[str, Any]] = None @@ -29,6 +33,7 @@ class SourceUpdate(BaseModel): class SourceCopyOptions(BaseModel): """Options for copying a source.""" + reuse_data_credentials: bool = False copy_access_controls: bool = False owner_id: Optional[int] = None diff --git a/nexla_sdk/models/sources/responses.py b/nexla_sdk/models/sources/responses.py index d562b10..f3c415b 100644 --- a/nexla_sdk/models/sources/responses.py +++ b/nexla_sdk/models/sources/responses.py @@ -1,13 +1,16 @@ -from typing import List, Optional, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional + from pydantic import Field, field_validator + from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.common import Owner, Organization, Connector +from nexla_sdk.models.common import Connector, Organization, Owner from nexla_sdk.models.credentials.responses import Credential class DataSetBrief(BaseModel): """Brief dataset information.""" + id: int owner_id: int org_id: int @@ -20,12 +23,14 @@ class DataSetBrief(BaseModel): class RunInfo(BaseModel): """Run information.""" + id: int created_at: datetime class Source(BaseModel): """Data source response model.""" + id: int name: str status: str @@ -37,7 +42,7 @@ class Source(BaseModel): managed: Optional[bool] = None auto_generated: Optional[bool] = None connector: Optional[Connector] = None - + description: Optional[str] = None ingest_method: Optional[str] = None source_format: Optional[str] = None @@ -57,16 +62,16 @@ class Source(BaseModel): tags: List[str] = Field(default_factory=list) created_at: Optional[datetime] = None updated_at: Optional[datetime] = None - - @field_validator('data_sets', mode='before') + + @field_validator("data_sets", mode="before") @classmethod def validate_data_sets(cls, v): """Handle None data_sets.""" if v is None: return [] return v - - @field_validator('tags', mode='before') + + @field_validator("tags", mode="before") @classmethod def validate_tags(cls, v): """Handle None tags.""" diff --git a/nexla_sdk/models/teams/__init__.py b/nexla_sdk/models/teams/__init__.py index 6d9f462..292beb6 100644 --- a/nexla_sdk/models/teams/__init__.py +++ b/nexla_sdk/models/teams/__init__.py @@ -1,15 +1,18 @@ -from nexla_sdk.models.teams.responses import Team, TeamMember from nexla_sdk.models.teams.requests import ( - TeamCreate, TeamUpdate, TeamMemberRequest, TeamMemberList + TeamCreate, + TeamMemberList, + TeamMemberRequest, + TeamUpdate, ) +from nexla_sdk.models.teams.responses import Team, TeamMember __all__ = [ # Responses - 'Team', - 'TeamMember', + "Team", + "TeamMember", # Requests - 'TeamCreate', - 'TeamUpdate', - 'TeamMemberRequest', - 'TeamMemberList', + "TeamCreate", + "TeamUpdate", + "TeamMemberRequest", + "TeamMemberList", ] diff --git a/nexla_sdk/models/teams/requests.py b/nexla_sdk/models/teams/requests.py index 0100c46..3afa1bc 100644 --- a/nexla_sdk/models/teams/requests.py +++ b/nexla_sdk/models/teams/requests.py @@ -1,10 +1,13 @@ -from typing import Optional, List +from typing import List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel class TeamMemberRequest(BaseModel): """Request model for team member.""" + # Can identify by ID or email id: Optional[int] = None email: Optional[str] = None @@ -13,6 +16,7 @@ class TeamMemberRequest(BaseModel): class TeamCreate(BaseModel): """Request model for creating a team.""" + name: str description: Optional[str] = None members: List[TeamMemberRequest] = Field(default_factory=list) @@ -20,6 +24,7 @@ class TeamCreate(BaseModel): class TeamUpdate(BaseModel): """Request model for updating a team.""" + name: Optional[str] = None description: Optional[str] = None members: Optional[List[TeamMemberRequest]] = None @@ -27,4 +32,5 @@ class TeamUpdate(BaseModel): class TeamMemberList(BaseModel): """Request model for team member operations.""" + members: List[TeamMemberRequest] diff --git a/nexla_sdk/models/teams/responses.py b/nexla_sdk/models/teams/responses.py index 11df6ce..9807a2e 100644 --- a/nexla_sdk/models/teams/responses.py +++ b/nexla_sdk/models/teams/responses.py @@ -1,12 +1,15 @@ -from typing import List, Optional from datetime import datetime +from typing import List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.common import Owner, Organization +from nexla_sdk.models.common import Organization, Owner class TeamMember(BaseModel): """Team member information.""" + id: int email: str admin: bool @@ -14,6 +17,7 @@ class TeamMember(BaseModel): class Team(BaseModel): """Team response model.""" + id: int name: str description: str @@ -22,7 +26,7 @@ class Team(BaseModel): member: bool members: List[TeamMember] access_roles: List[str] - + tags: List[str] = Field(default_factory=list) created_at: Optional[datetime] = None updated_at: Optional[datetime] = None diff --git a/nexla_sdk/models/transforms/__init__.py b/nexla_sdk/models/transforms/__init__.py index 420c800..f3a5705 100644 --- a/nexla_sdk/models/transforms/__init__.py +++ b/nexla_sdk/models/transforms/__init__.py @@ -1,8 +1,9 @@ -from .responses import Transform, TransformCodeOp from .requests import TransformCreate, TransformUpdate +from .responses import Transform, TransformCodeOp __all__ = [ - 'Transform', 'TransformCodeOp', - 'TransformCreate', - 'TransformUpdate', + "Transform", + "TransformCodeOp", + "TransformCreate", + "TransformUpdate", ] diff --git a/nexla_sdk/models/transforms/requests.py b/nexla_sdk/models/transforms/requests.py index bc4ce79..0129e66 100644 --- a/nexla_sdk/models/transforms/requests.py +++ b/nexla_sdk/models/transforms/requests.py @@ -1,6 +1,7 @@ from typing import Any, Dict, List, Optional from nexla_sdk.models.base import BaseModel + from .responses import TransformCodeOp @@ -32,4 +33,3 @@ class TransformUpdate(BaseModel): custom_config: Optional[Dict[str, Any]] = None data_credentials_id: Optional[int] = None runtime_data_credentials_id: Optional[int] = None - diff --git a/nexla_sdk/models/transforms/responses.py b/nexla_sdk/models/transforms/responses.py index f1daa0b..3a36cc1 100644 --- a/nexla_sdk/models/transforms/responses.py +++ b/nexla_sdk/models/transforms/responses.py @@ -32,4 +32,3 @@ class Transform(BaseModel): updated_at: Optional[datetime] = None created_at: Optional[datetime] = None tags: Optional[List[str]] = None - diff --git a/nexla_sdk/models/users/__init__.py b/nexla_sdk/models/users/__init__.py index 1256ab2..d92b1bf 100644 --- a/nexla_sdk/models/users/__init__.py +++ b/nexla_sdk/models/users/__init__.py @@ -1,20 +1,22 @@ +from nexla_sdk.models.users.requests import UserCreate, UserUpdate from nexla_sdk.models.users.responses import ( - User, UserExpanded, UserSettings, - DefaultOrg, OrgMembership, AccountSummary -) -from nexla_sdk.models.users.requests import ( - UserCreate, UserUpdate + AccountSummary, + DefaultOrg, + OrgMembership, + User, + UserExpanded, + UserSettings, ) __all__ = [ # Responses - 'User', - 'UserExpanded', - 'UserSettings', - 'DefaultOrg', - 'OrgMembership', - 'AccountSummary', + "User", + "UserExpanded", + "UserSettings", + "DefaultOrg", + "OrgMembership", + "AccountSummary", # Requests - 'UserCreate', - 'UserUpdate', -] \ No newline at end of file + "UserCreate", + "UserUpdate", +] diff --git a/nexla_sdk/models/users/requests.py b/nexla_sdk/models/users/requests.py index b41946e..a061e20 100644 --- a/nexla_sdk/models/users/requests.py +++ b/nexla_sdk/models/users/requests.py @@ -1,10 +1,12 @@ -from typing import Optional, Union, List, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional, Union + from nexla_sdk.models.base import BaseModel class UserCreate(BaseModel): """Request model for creating a user.""" + full_name: str email: str default_org_id: Optional[int] = None @@ -18,6 +20,7 @@ class UserCreate(BaseModel): class UserUpdate(BaseModel): """Request model for updating a user.""" + name: Optional[str] = None email: Optional[str] = None status: Optional[str] = None @@ -27,4 +30,4 @@ class UserUpdate(BaseModel): password_confirmation: Optional[str] = None password_current: Optional[str] = None tos_signed_at: Optional[datetime] = None - admin: Optional[Union[str, bool, List[Dict[str, Any]]]] = None \ No newline at end of file + admin: Optional[Union[str, bool, List[Dict[str, Any]]]] = None diff --git a/nexla_sdk/models/users/responses.py b/nexla_sdk/models/users/responses.py index 79d603d..5ace3cd 100644 --- a/nexla_sdk/models/users/responses.py +++ b/nexla_sdk/models/users/responses.py @@ -1,17 +1,21 @@ -from typing import List, Optional, Dict, Any from datetime import datetime +from typing import Any, Dict, List, Optional + from pydantic import Field + from nexla_sdk.models.base import BaseModel class DefaultOrg(BaseModel): """User's default organization.""" + id: int name: str class OrgMembership(BaseModel): """Organization membership details.""" + id: int name: str is_admin: Optional[bool] = Field(default=None, alias="isAdmin") @@ -21,6 +25,7 @@ class OrgMembership(BaseModel): class User(BaseModel): """User response model.""" + id: int email: str full_name: str @@ -32,7 +37,7 @@ class User(BaseModel): account_locked: bool org_memberships: List[OrgMembership] api_key: Optional[str] = None - + email_verified_at: Optional[datetime] = None tos_signed_at: Optional[datetime] = None created_at: Optional[datetime] = None @@ -41,6 +46,7 @@ class User(BaseModel): class AccountSummary(BaseModel): """User account summary.""" + data_sources: Dict[str, Dict[str, int]] data_sets: Dict[str, Dict[str, int]] data_sinks: Dict[str, Dict[str, int]] @@ -49,11 +55,13 @@ class AccountSummary(BaseModel): class UserExpanded(User): """User with expanded account summary.""" + account_summary: Optional[AccountSummary] = None class UserSettings(BaseModel): """User settings.""" + id: str owner: Dict[str, Any] org: Dict[str, Any] diff --git a/nexla_sdk/models/webhooks/__init__.py b/nexla_sdk/models/webhooks/__init__.py index ab442aa..3185e8f 100644 --- a/nexla_sdk/models/webhooks/__init__.py +++ b/nexla_sdk/models/webhooks/__init__.py @@ -1,4 +1,5 @@ """Webhook models.""" + from .requests import WebhookSendOptions from .responses import WebhookResponse diff --git a/nexla_sdk/models/webhooks/requests.py b/nexla_sdk/models/webhooks/requests.py index 489d9e9..58a6e67 100644 --- a/nexla_sdk/models/webhooks/requests.py +++ b/nexla_sdk/models/webhooks/requests.py @@ -1,5 +1,7 @@ """Webhook request models.""" + from typing import Optional + from nexla_sdk.models.base import BaseModel diff --git a/nexla_sdk/models/webhooks/responses.py b/nexla_sdk/models/webhooks/responses.py index 3f21c6e..fc77f83 100644 --- a/nexla_sdk/models/webhooks/responses.py +++ b/nexla_sdk/models/webhooks/responses.py @@ -1,5 +1,7 @@ """Webhook response models.""" + from typing import Optional + from nexla_sdk.models.base import BaseModel diff --git a/nexla_sdk/resources/__init__.py b/nexla_sdk/resources/__init__.py index e983c95..effd879 100644 --- a/nexla_sdk/resources/__init__.py +++ b/nexla_sdk/resources/__init__.py @@ -1,53 +1,53 @@ +from nexla_sdk.resources.approval_requests import ApprovalRequestsResource +from nexla_sdk.resources.async_tasks import AsyncTasksResource +from nexla_sdk.resources.attribute_transforms import AttributeTransformsResource from nexla_sdk.resources.base_resource import BaseResource +from nexla_sdk.resources.code_containers import CodeContainersResource from nexla_sdk.resources.credentials import CredentialsResource -from nexla_sdk.resources.flows import FlowsResource -from nexla_sdk.resources.sources import SourcesResource +from nexla_sdk.resources.data_schemas import DataSchemasResource from nexla_sdk.resources.destinations import DestinationsResource -from nexla_sdk.resources.nexsets import NexsetsResource +from nexla_sdk.resources.doc_containers import DocContainersResource +from nexla_sdk.resources.flows import FlowsResource +from nexla_sdk.resources.genai import GenAIResource from nexla_sdk.resources.lookups import LookupsResource -from nexla_sdk.resources.users import UsersResource +from nexla_sdk.resources.marketplace import MarketplaceResource +from nexla_sdk.resources.metrics import MetricsResource +from nexla_sdk.resources.nexsets import NexsetsResource +from nexla_sdk.resources.notifications import NotificationsResource +from nexla_sdk.resources.org_auth_configs import OrgAuthConfigsResource from nexla_sdk.resources.organizations import OrganizationsResource -from nexla_sdk.resources.teams import TeamsResource from nexla_sdk.resources.projects import ProjectsResource -from nexla_sdk.resources.notifications import NotificationsResource -from nexla_sdk.resources.metrics import MetricsResource -from nexla_sdk.resources.code_containers import CodeContainersResource -from nexla_sdk.resources.transforms import TransformsResource -from nexla_sdk.resources.attribute_transforms import AttributeTransformsResource -from nexla_sdk.resources.async_tasks import AsyncTasksResource -from nexla_sdk.resources.approval_requests import ApprovalRequestsResource from nexla_sdk.resources.runtimes import RuntimesResource -from nexla_sdk.resources.marketplace import MarketplaceResource -from nexla_sdk.resources.org_auth_configs import OrgAuthConfigsResource -from nexla_sdk.resources.genai import GenAIResource from nexla_sdk.resources.self_signup import SelfSignupResource -from nexla_sdk.resources.doc_containers import DocContainersResource -from nexla_sdk.resources.data_schemas import DataSchemasResource +from nexla_sdk.resources.sources import SourcesResource +from nexla_sdk.resources.teams import TeamsResource +from nexla_sdk.resources.transforms import TransformsResource +from nexla_sdk.resources.users import UsersResource __all__ = [ - 'BaseResource', - 'CredentialsResource', - 'FlowsResource', - 'SourcesResource', - 'DestinationsResource', - 'NexsetsResource', - 'LookupsResource', - 'UsersResource', - 'OrganizationsResource', - 'TeamsResource', - 'ProjectsResource', - 'NotificationsResource', - 'MetricsResource', - 'CodeContainersResource', - 'TransformsResource', - 'AttributeTransformsResource', - 'AsyncTasksResource', - 'ApprovalRequestsResource', - 'RuntimesResource', - 'MarketplaceResource', - 'OrgAuthConfigsResource', - 'GenAIResource', - 'SelfSignupResource', - 'DocContainersResource', - 'DataSchemasResource', + "BaseResource", + "CredentialsResource", + "FlowsResource", + "SourcesResource", + "DestinationsResource", + "NexsetsResource", + "LookupsResource", + "UsersResource", + "OrganizationsResource", + "TeamsResource", + "ProjectsResource", + "NotificationsResource", + "MetricsResource", + "CodeContainersResource", + "TransformsResource", + "AttributeTransformsResource", + "AsyncTasksResource", + "ApprovalRequestsResource", + "RuntimesResource", + "MarketplaceResource", + "OrgAuthConfigsResource", + "GenAIResource", + "SelfSignupResource", + "DocContainersResource", + "DataSchemasResource", ] diff --git a/nexla_sdk/resources/approval_requests.py b/nexla_sdk/resources/approval_requests.py index 51085a7..92623f1 100644 --- a/nexla_sdk/resources/approval_requests.py +++ b/nexla_sdk/resources/approval_requests.py @@ -1,6 +1,7 @@ from typing import List -from nexla_sdk.resources.base_resource import BaseResource + from nexla_sdk.models.approval_requests.responses import ApprovalRequest +from nexla_sdk.resources.base_resource import BaseResource class ApprovalRequestsResource(BaseResource): @@ -13,21 +14,21 @@ def __init__(self, client): def list_pending(self) -> List[ApprovalRequest]: path = f"{self._path}/pending" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return self._parse_response(response) def list_requested(self) -> List[ApprovalRequest]: path = f"{self._path}/requested" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return self._parse_response(response) def approve(self, request_id: int) -> ApprovalRequest: path = f"{self._path}/{request_id}/approve" - response = self._make_request('PUT', path) + response = self._make_request("PUT", path) return self._parse_response(response) def reject(self, request_id: int, reason: str = "") -> ApprovalRequest: path = f"{self._path}/{request_id}/reject" body = {"reason": reason} if reason else {} - response = self._make_request('DELETE', path, json=body) + response = self._make_request("DELETE", path, json=body) return self._parse_response(response) diff --git a/nexla_sdk/resources/async_tasks.py b/nexla_sdk/resources/async_tasks.py index 62118cf..0b171a7 100644 --- a/nexla_sdk/resources/async_tasks.py +++ b/nexla_sdk/resources/async_tasks.py @@ -1,7 +1,8 @@ -from typing import List, Dict, Any, Optional, Union -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.async_tasks.responses import AsyncTask, DownloadLink +from typing import Any, Dict, List, Optional, Union + from nexla_sdk.models.async_tasks.requests import AsyncTaskCreate +from nexla_sdk.models.async_tasks.responses import AsyncTask, DownloadLink +from nexla_sdk.resources.base_resource import BaseResource class AsyncTasksResource(BaseResource): @@ -14,61 +15,61 @@ def __init__(self, client): def list(self) -> List[AsyncTask]: """List asynchronous tasks.""" - response = self._make_request('GET', self._path) + response = self._make_request("GET", self._path) return self._parse_response(response) def create(self, payload: AsyncTaskCreate) -> AsyncTask: """Create/start an asynchronous task.""" serialized = self._serialize_data(payload) - response = self._make_request('POST', self._path, json=serialized) + response = self._make_request("POST", self._path, json=serialized) return self._parse_response(response) def list_of_type(self, task_type: str) -> List[AsyncTask]: path = f"{self._path}/of_type/{task_type}" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return self._parse_response(response) def list_by_status(self, status: str) -> List[AsyncTask]: path = f"{self._path}/by_status/{status}" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return self._parse_response(response) def types(self) -> List[str]: path = f"{self._path}/types" - return self._make_request('GET', path) + return self._make_request("GET", path) def explain_arguments(self, task_type: str) -> Dict[str, Any]: path = f"{self._path}/explain_arguments/{task_type}" - return self._make_request('GET', path) + return self._make_request("GET", path) def get(self, task_id: int) -> AsyncTask: path = f"{self._path}/{task_id}" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return self._parse_response(response) def delete(self, task_id: int) -> Dict[str, Any]: path = f"{self._path}/{task_id}" - return self._make_request('DELETE', path) + return self._make_request("DELETE", path) def rerun(self, task_id: int) -> AsyncTask: path = f"{self._path}/{task_id}/rerun" - response = self._make_request('POST', path) + response = self._make_request("POST", path) return self._parse_response(response) def result(self, task_id: int) -> Optional[Dict[str, Any]]: path = f"{self._path}/{task_id}/result" - return self._make_request('GET', path) + return self._make_request("GET", path) def download_link(self, task_id: int) -> Union[str, DownloadLink]: path = f"{self._path}/{task_id}/download_link" - response = self._make_request('GET', path) + response = self._make_request("GET", path) # Some servers may return a plain URL string; others an object if isinstance(response, str): return response - if isinstance(response, dict) and 'url' in response: + if isinstance(response, dict) and "url" in response: return DownloadLink.model_validate(response) return response # type: ignore[return-value] def acknowledge(self, task_id: int) -> Dict[str, Any]: path = f"{self._path}/{task_id}/acknowledge" - return self._make_request('POST', path) + return self._make_request("POST", path) diff --git a/nexla_sdk/resources/attribute_transforms.py b/nexla_sdk/resources/attribute_transforms.py index 6ab301b..dc2f0c5 100644 --- a/nexla_sdk/resources/attribute_transforms.py +++ b/nexla_sdk/resources/attribute_transforms.py @@ -1,9 +1,11 @@ -from typing import List, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.attribute_transforms.responses import AttributeTransform +from typing import Any, Dict, List + from nexla_sdk.models.attribute_transforms.requests import ( - AttributeTransformCreate, AttributeTransformUpdate, + AttributeTransformCreate, + AttributeTransformUpdate, ) +from nexla_sdk.models.attribute_transforms.responses import AttributeTransform +from nexla_sdk.resources.base_resource import BaseResource class AttributeTransformsResource(BaseResource): @@ -32,7 +34,9 @@ def list(self, **kwargs) -> List[AttributeTransform]: """ return super().list(**kwargs) - def get(self, attribute_transform_id: int, expand: bool = False) -> AttributeTransform: + def get( + self, attribute_transform_id: int, expand: bool = False + ) -> AttributeTransform: """Get an attribute transform by ID.""" return super().get(attribute_transform_id, expand) @@ -40,7 +44,9 @@ def create(self, data: AttributeTransformCreate) -> AttributeTransform: """Create a new attribute transform.""" return super().create(data) - def update(self, attribute_transform_id: int, data: AttributeTransformUpdate) -> AttributeTransform: + def update( + self, attribute_transform_id: int, data: AttributeTransformUpdate + ) -> AttributeTransform: """Update an attribute transform by ID.""" return super().update(attribute_transform_id, data) @@ -51,5 +57,5 @@ def delete(self, attribute_transform_id: int) -> Dict[str, Any]: def list_public(self) -> List[AttributeTransform]: """List publicly shared attribute transforms.""" path = f"{self._path}/public" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return self._parse_response(response) diff --git a/nexla_sdk/resources/base_resource.py b/nexla_sdk/resources/base_resource.py index 549cb7d..a97cd48 100644 --- a/nexla_sdk/resources/base_resource.py +++ b/nexla_sdk/resources/base_resource.py @@ -1,34 +1,38 @@ -from typing import Dict, Any, Optional, List, TypeVar, Type, Union -from nexla_sdk.utils.pagination import Paginator +from typing import Any, Dict, List, Optional, Type, TypeVar, Union + from nexla_sdk.exceptions import NexlaError from nexla_sdk.models.access import ( + AccessorRequestList, AccessorResponse, - AccessorRequestList, AccessorResponseList + AccessorResponseList, ) +from nexla_sdk.utils.pagination import Paginator -T = TypeVar('T') +T = TypeVar("T") class BaseResource: """Base class for all Nexla resources.""" - + def __init__(self, client): """ Initialize resource. - + Args: client: Nexla client instance """ self.client = client self._path = "" # Override in subclasses self._model_class = None # Override in subclasses - - def _make_request(self, - method: str, - path: str, - resource_id: Optional[str] = None, - operation: Optional[str] = None, - **kwargs) -> Any: + + def _make_request( + self, + method: str, + path: str, + resource_id: Optional[str] = None, + operation: Optional[str] = None, + **kwargs, + ) -> Any: """Make HTTP request using client with enhanced error context.""" try: return self.client.request(method, path, **kwargs) @@ -37,21 +41,25 @@ def _make_request(self, raise except Exception as e: # Extract resource type from path - resource_type = self._path.strip('/').split('/')[-1] if self._path else "unknown" - + resource_type = ( + self._path.strip("/").split("/")[-1] if self._path else "unknown" + ) + # Build context information context = { "method": method, "path": path, "resource_path": self._path, - "kwargs": {k: v for k, v in kwargs.items() if k not in ['json', 'data']} # Exclude sensitive data + "kwargs": { + k: v for k, v in kwargs.items() if k not in ["json", "data"] + }, # Exclude sensitive data } - - if hasattr(e, 'response') and e.response: - context['api_response'] = e.response - if hasattr(e, 'status_code'): - context['status_code'] = e.status_code - + + if hasattr(e, "response") and e.response: + context["api_response"] = e.response + if hasattr(e, "status_code"): + context["status_code"] = e.status_code + # Re-raise with enhanced context raise NexlaError( message=str(e), @@ -59,55 +67,61 @@ def _make_request(self, resource_type=resource_type, resource_id=resource_id, context=context, - original_error=e + original_error=e, ) from e - + def _serialize_data(self, data: Union[Dict[str, Any], Any]) -> Dict[str, Any]: """ Convert data to dictionary for JSON serialization. - + Args: data: Data to serialize (dict or Pydantic model) - + Returns: Dictionary representation """ if data is None: return {} - + # Check if it's a Pydantic model (has model_dump method) - if hasattr(data, 'model_dump'): + if hasattr(data, "model_dump"): return data.model_dump(exclude_none=True) - + # If it's already a dict, return as-is if isinstance(data, dict): return data - + # For other types, try to convert to dict - if hasattr(data, '__dict__'): + if hasattr(data, "__dict__"): return data.__dict__ - + return data - - def _parse_response(self, response: Any, model_class: Optional[Type[T]] = None) -> Any: + + def _parse_response( + self, response: Any, model_class: Optional[Type[T]] = None + ) -> Any: """Parse response into model objects.""" model_class = model_class or self._model_class - + if not model_class: return response - + if isinstance(response, list): - return [model_class.model_validate(item) if isinstance(item, dict) else item - for item in response] + return [ + model_class.model_validate(item) if isinstance(item, dict) else item + for item in response + ] elif isinstance(response, dict): return model_class.model_validate(response) return response - - def list(self, - page: Optional[int] = None, - per_page: Optional[int] = None, - access_role: Optional[str] = None, - **params) -> List[T]: + + def list( + self, + page: Optional[int] = None, + per_page: Optional[int] = None, + access_role: Optional[str] = None, + **params, + ) -> List[T]: """ List resources with optional filters. @@ -115,19 +129,19 @@ def list(self, - page: Page number (1-based) - per_page: Items per page - access_role: owner, collaborator, operator, admin - + Any resource-specific filters can be passed via keyword arguments (for example, `credentials_type` for credentials, `expand` for users/projects). - + Args: page: Page number (1-based) per_page: Items per page access_role: Filter by access role (owner, collaborator, operator, admin) **params: Resource-specific query parameters - + Returns: List of resources - + Examples: # Basic listing client.sources.list() @@ -140,49 +154,47 @@ def list(self, """ query_params = {} if page is not None: - query_params['page'] = page + query_params["page"] = page if per_page is not None: - query_params['per_page'] = per_page + query_params["per_page"] = per_page if access_role is not None: - query_params['access_role'] = access_role + query_params["access_role"] = access_role query_params.update(params) - - response = self._make_request('GET', self._path, operation="list_resources", params=query_params) + + response = self._make_request( + "GET", self._path, operation="list_resources", params=query_params + ) return self._parse_response(response) - - def paginate(self, - per_page: int = 20, - access_role: Optional[str] = None, - **params) -> Paginator[T]: + + def paginate( + self, per_page: int = 20, access_role: Optional[str] = None, **params + ) -> Paginator[T]: """ Get paginator for iterating through resources. - + Args: per_page: Items per page access_role: Filter by access role **params: Additional query parameters - + Returns: Paginator instance """ return Paginator( - fetch_func=self.list, - page_size=per_page, - access_role=access_role, - **params + fetch_func=self.list, page_size=per_page, access_role=access_role, **params ) - + def get(self, resource_id: int, expand: bool = False) -> T: """ Get single resource by ID. - + Args: resource_id: Resource ID expand: Include expanded references (where supported) - + Returns: Resource instance - + Examples: # Get by ID client.sources.get(123) @@ -191,21 +203,27 @@ def get(self, resource_id: int, expand: bool = False) -> T: client.projects.get(456, expand=True) """ path = f"{self._path}/{resource_id}" - params = {'expand': 1} if expand else {} - - response = self._make_request('GET', path, resource_id=str(resource_id), operation="get_resource", params=params) + params = {"expand": 1} if expand else {} + + response = self._make_request( + "GET", + path, + resource_id=str(resource_id), + operation="get_resource", + params=params, + ) return self._parse_response(response) - + def create(self, data: Union[Dict[str, Any], Any]) -> T: """ Create new resource. - + Args: data: Resource data (Pydantic model or dict) - + Returns: Created resource - + Examples: # Using a typed request model source = client.sources.create(SourceCreate(name="My Source", connector=...)) @@ -214,171 +232,203 @@ def create(self, data: Union[Dict[str, Any], Any]) -> T: client.async_tasks.create(AsyncTaskCreate(type="export", arguments={...})) """ serialized_data = self._serialize_data(data) - response = self._make_request('POST', self._path, operation="create_resource", json=serialized_data) + response = self._make_request( + "POST", self._path, operation="create_resource", json=serialized_data + ) return self._parse_response(response) - + def update(self, resource_id: int, data: Union[Dict[str, Any], Any]) -> T: """ Update resource. - + Args: resource_id: Resource ID data: Updated data (dict or Pydantic model) - + Returns: Updated resource """ path = f"{self._path}/{resource_id}" serialized_data = self._serialize_data(data) - response = self._make_request('PUT', path, resource_id=str(resource_id), operation="update_resource", json=serialized_data) + response = self._make_request( + "PUT", + path, + resource_id=str(resource_id), + operation="update_resource", + json=serialized_data, + ) return self._parse_response(response) - + def delete(self, resource_id: int) -> Dict[str, Any]: """ Delete resource. - + Args: resource_id: Resource ID - + Returns: Response with status """ path = f"{self._path}/{resource_id}" - return self._make_request('DELETE', path, resource_id=str(resource_id), operation="delete_resource") - + return self._make_request( + "DELETE", path, resource_id=str(resource_id), operation="delete_resource" + ) + def activate(self, resource_id: int) -> T: """ Activate resource. - + Args: resource_id: Resource ID - + Returns: Activated resource """ path = f"{self._path}/{resource_id}/activate" - response = self._make_request('PUT', path, resource_id=str(resource_id), operation="activate_resource") + response = self._make_request( + "PUT", path, resource_id=str(resource_id), operation="activate_resource" + ) return self._parse_response(response) - + def pause(self, resource_id: int) -> T: """ Pause resource. - + Args: resource_id: Resource ID - + Returns: Paused resource """ path = f"{self._path}/{resource_id}/pause" - response = self._make_request('PUT', path, resource_id=str(resource_id), operation="pause_resource") + response = self._make_request( + "PUT", path, resource_id=str(resource_id), operation="pause_resource" + ) return self._parse_response(response) - - def copy(self, resource_id: int, options: Optional[Union[Dict[str, Any], Any]] = None) -> T: + + def copy( + self, resource_id: int, options: Optional[Union[Dict[str, Any], Any]] = None + ) -> T: """ Copy resource. - + Args: resource_id: Resource ID options: Copy options (dict or Pydantic model) - + Returns: Copied resource """ path = f"{self._path}/{resource_id}/copy" serialized_options = self._serialize_data(options) if options else {} - response = self._make_request('POST', path, json=serialized_options) + response = self._make_request("POST", path, json=serialized_options) return self._parse_response(response) - + def get_audit_log(self, resource_id: int) -> List[Dict[str, Any]]: """ Get audit log for resource. - + Args: resource_id: Resource ID - + Returns: List of audit log entries """ path = f"{self._path}/{resource_id}/audit_log" - return self._make_request('GET', path) - + return self._make_request("GET", path) + def get_accessors(self, resource_id: int) -> AccessorResponseList: """ Get access control rules for resource. - + Args: resource_id: Resource ID - + Returns: List of access control rules """ path = f"{self._path}/{resource_id}/accessors" - response = self._make_request('GET', path) - + response = self._make_request("GET", path) + # Parse response into AccessorResponse objects if isinstance(response, list): return [AccessorResponse.model_validate(item) for item in response] return [] - - def add_accessors(self, resource_id: int, accessors: AccessorRequestList) -> AccessorResponseList: + + def add_accessors( + self, resource_id: int, accessors: AccessorRequestList + ) -> AccessorResponseList: """ Add access control rules. - + Args: resource_id: Resource ID accessors: List of accessor rules - + Returns: Updated accessor list """ path = f"{self._path}/{resource_id}/accessors" - serialized_accessors = [self._serialize_data(accessor) for accessor in accessors] - response = self._make_request('PUT', path, json={'accessors': serialized_accessors}) - + serialized_accessors = [ + self._serialize_data(accessor) for accessor in accessors + ] + response = self._make_request( + "PUT", path, json={"accessors": serialized_accessors} + ) + # Parse response into AccessorResponse objects if isinstance(response, list): return [AccessorResponse.model_validate(item) for item in response] return [] - - def replace_accessors(self, resource_id: int, accessors: AccessorRequestList) -> AccessorResponseList: + + def replace_accessors( + self, resource_id: int, accessors: AccessorRequestList + ) -> AccessorResponseList: """ Replace all access control rules. - + Args: resource_id: Resource ID accessors: List of accessor rules - + Returns: New accessor list """ path = f"{self._path}/{resource_id}/accessors" - serialized_accessors = [self._serialize_data(accessor) for accessor in accessors] - response = self._make_request('POST', path, json={'accessors': serialized_accessors}) - + serialized_accessors = [ + self._serialize_data(accessor) for accessor in accessors + ] + response = self._make_request( + "POST", path, json={"accessors": serialized_accessors} + ) + # Parse response into AccessorResponse objects if isinstance(response, list): return [AccessorResponse.model_validate(item) for item in response] return [] - - def delete_accessors(self, resource_id: int, accessors: Optional[AccessorRequestList] = None) -> AccessorResponseList: + + def delete_accessors( + self, resource_id: int, accessors: Optional[AccessorRequestList] = None + ) -> AccessorResponseList: """ Delete access control rules. - + Args: resource_id: Resource ID accessors: Specific accessors to delete (None = delete all) - + Returns: Remaining accessor list """ path = f"{self._path}/{resource_id}/accessors" data = None if accessors: - serialized_accessors = [self._serialize_data(accessor) for accessor in accessors] - data = {'accessors': serialized_accessors} - response = self._make_request('DELETE', path, json=data) - + serialized_accessors = [ + self._serialize_data(accessor) for accessor in accessors + ] + data = {"accessors": serialized_accessors} + response = self._make_request("DELETE", path, json=data) + # Parse response into AccessorResponse objects if isinstance(response, list): return [AccessorResponse.model_validate(item) for item in response] diff --git a/nexla_sdk/resources/code_containers.py b/nexla_sdk/resources/code_containers.py index 4646544..2fa67af 100644 --- a/nexla_sdk/resources/code_containers.py +++ b/nexla_sdk/resources/code_containers.py @@ -1,7 +1,11 @@ -from typing import List, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource +from typing import Any, Dict, List + +from nexla_sdk.models.code_containers.requests import ( + CodeContainerCreate, + CodeContainerUpdate, +) from nexla_sdk.models.code_containers.responses import CodeContainer -from nexla_sdk.models.code_containers.requests import CodeContainerCreate, CodeContainerUpdate +from nexla_sdk.resources.base_resource import BaseResource class CodeContainersResource(BaseResource): @@ -46,7 +50,9 @@ def create(self, data: CodeContainerCreate) -> CodeContainer: """ return super().create(data) - def update(self, code_container_id: int, data: CodeContainerUpdate) -> CodeContainer: + def update( + self, code_container_id: int, data: CodeContainerUpdate + ) -> CodeContainer: """Update an existing code container. Examples: @@ -65,5 +71,5 @@ def copy(self, code_container_id: int) -> CodeContainer: def list_public(self) -> List[CodeContainer]: """List publicly shared code containers.""" path = f"{self._path}/public" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return self._parse_response(response) diff --git a/nexla_sdk/resources/credentials.py b/nexla_sdk/resources/credentials.py index 87dce43..83bb436 100644 --- a/nexla_sdk/resources/credentials.py +++ b/nexla_sdk/resources/credentials.py @@ -1,33 +1,42 @@ """Credentials resource implementation.""" -from typing import List, Optional, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.credentials.responses import Credential, ProbeTreeResponse, ProbeSampleResponse + +from typing import Any, Dict, List, Optional + from nexla_sdk.models.credentials.requests import ( - CredentialCreate, CredentialUpdate, ProbeTreeRequest, ProbeSampleRequest + CredentialCreate, + CredentialUpdate, + ProbeSampleRequest, + ProbeTreeRequest, ) +from nexla_sdk.models.credentials.responses import ( + Credential, + ProbeSampleResponse, + ProbeTreeResponse, +) +from nexla_sdk.resources.base_resource import BaseResource class CredentialsResource(BaseResource): """Resource for managing data credentials.""" - + def __init__(self, client): super().__init__(client) self._path = "/data_credentials" self._model_class = Credential - - def list(self, - credentials_type: Optional[str] = None, - **kwargs) -> List[Credential]: + + def list( + self, credentials_type: Optional[str] = None, **kwargs + ) -> List[Credential]: """ List credentials with optional filters. - + Args: credentials_type: Filter by credential type (e.g., 's3', 'gcs') page: Page number (via kwargs) per_page: Items per page (via kwargs) access_role: Filter by access role (via kwargs) **kwargs: Additional query parameters - + Returns: List of credentials @@ -43,138 +52,158 @@ def list(self, """ params = kwargs.copy() if credentials_type: - params['credentials_type'] = credentials_type - + params["credentials_type"] = credentials_type + return super().list(**params) - + def get(self, credential_id: int, expand: bool = False) -> Credential: """ Get single credential by ID. - + Args: credential_id: Credential ID expand: Include expanded references - + Returns: Credential instance - + Examples: client.credentials.get(123) """ return super().get(credential_id, expand) - + def create(self, data: CredentialCreate) -> Credential: """ Create new credential. - + Args: data: Credential creation data - + Returns: Created credential - + Examples: new_cred = client.credentials.create( CredentialCreate(name="my-s3", connector_type="s3", config={...}) ) """ return super().create(data) - + def update(self, credential_id: int, data: CredentialUpdate) -> Credential: """ Update credential. - + Args: credential_id: Credential ID data: Updated credential data - + Returns: Updated credential """ return super().update(credential_id, data) - + def delete(self, credential_id: int) -> Dict[str, Any]: """ Delete credential. - + Args: credential_id: Credential ID - + Returns: Response with status """ return super().delete(credential_id) - - def probe(self, credential_id: int, async_mode: bool = False, request_id: Optional[int] = None) -> Dict[str, Any]: + + def probe( + self, + credential_id: int, + async_mode: bool = False, + request_id: Optional[int] = None, + ) -> Dict[str, Any]: """ Test credential validity. - + Args: credential_id: Credential ID - + Returns: Probe response """ path = f"{self._path}/{credential_id}/probe" params = {} if async_mode: - params['async'] = True + params["async"] = True if request_id is not None: - params['request_id'] = request_id - response = self._make_request('GET', path, params=params) - + params["request_id"] = request_id + response = self._make_request("GET", path, params=params) + # Handle cases where the response might be None or contain raw text if response is None: - return {"status": "success", "message": "Credential probe completed successfully"} + return { + "status": "success", + "message": "Credential probe completed successfully", + } elif isinstance(response, dict) and "raw_text" in response: - return {"status": "success", "message": response["raw_text"], "status_code": response.get("status_code")} + return { + "status": "success", + "message": response["raw_text"], + "status_code": response.get("status_code"), + } else: return response - - def probe_tree(self, - credential_id: int, - request: ProbeTreeRequest, - async_mode: bool = False, - request_id: Optional[int] = None) -> ProbeTreeResponse: + + def probe_tree( + self, + credential_id: int, + request: ProbeTreeRequest, + async_mode: bool = False, + request_id: Optional[int] = None, + ) -> ProbeTreeResponse: """ Preview storage structure accessible by credential. - + Args: credential_id: Credential ID request: Probe tree request - + Returns: Storage structure response """ path = f"{self._path}/{credential_id}/probe/tree" params = {} if async_mode: - params['async'] = True + params["async"] = True if request_id is not None: - params['request_id'] = request_id - response = self._make_request('POST', path, json=request.to_dict(), params=params) + params["request_id"] = request_id + response = self._make_request( + "POST", path, json=request.to_dict(), params=params + ) return ProbeTreeResponse(**response) - - def probe_sample(self, - credential_id: int, - request: ProbeSampleRequest, - async_mode: bool = False, - request_id: Optional[int] = None) -> ProbeSampleResponse: + + def probe_sample( + self, + credential_id: int, + request: ProbeSampleRequest, + async_mode: bool = False, + request_id: Optional[int] = None, + ) -> ProbeSampleResponse: """ Preview data content accessible by credential. - + Args: credential_id: Credential ID request: Probe sample request - + Returns: Sample data response """ path = f"{self._path}/{credential_id}/probe/sample" params = {} if async_mode: - params['async'] = True + params["async"] = True if request_id is not None: - params['request_id'] = request_id - response = self._make_request('POST', path, json=request.to_dict(), params=params) + params["request_id"] = request_id + response = self._make_request( + "POST", path, json=request.to_dict(), params=params + ) return ProbeSampleResponse(**response) diff --git a/nexla_sdk/resources/data_schemas.py b/nexla_sdk/resources/data_schemas.py index 02b3848..efdf54f 100644 --- a/nexla_sdk/resources/data_schemas.py +++ b/nexla_sdk/resources/data_schemas.py @@ -1,6 +1,7 @@ from typing import List -from nexla_sdk.resources.base_resource import BaseResource + from nexla_sdk.models.common import LogEntry +from nexla_sdk.resources.base_resource import BaseResource class DataSchemasResource(BaseResource): @@ -13,5 +14,5 @@ def __init__(self, client): def get_audit_log(self, schema_id: int, **params) -> List[LogEntry]: path = f"{self._path}/{schema_id}/audit_log" - response = self._make_request('GET', path, params=params) + response = self._make_request("GET", path, params=params) return [LogEntry.model_validate(item) for item in (response or [])] diff --git a/nexla_sdk/resources/destinations.py b/nexla_sdk/resources/destinations.py index c2e476f..35b8da9 100644 --- a/nexla_sdk/resources/destinations.py +++ b/nexla_sdk/resources/destinations.py @@ -1,27 +1,32 @@ -from typing import List, Optional, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource +from typing import Any, Dict, List, Optional + +from nexla_sdk.models.destinations.requests import ( + DestinationCopyOptions, + DestinationCreate, + DestinationUpdate, +) from nexla_sdk.models.destinations.responses import Destination -from nexla_sdk.models.destinations.requests import DestinationCreate, DestinationUpdate, DestinationCopyOptions +from nexla_sdk.resources.base_resource import BaseResource class DestinationsResource(BaseResource): """Resource for managing destinations (data sinks).""" - + def __init__(self, client): super().__init__(client) self._path = "/data_sinks" self._model_class = Destination - + def list(self, **kwargs) -> List[Destination]: """ List destinations with optional filters. - + Args: page: Page number (via kwargs) per_page: Items per page (via kwargs) access_role: Filter by access role (via kwargs) **kwargs: Additional query parameters - + Returns: List of destinations @@ -29,95 +34,97 @@ def list(self, **kwargs) -> List[Destination]: client.destinations.list(page=1, per_page=20, access_role="owner") """ return super().list(**kwargs) - + def get(self, sink_id: int, expand: bool = False) -> Destination: """ Get single destination by ID. - + Args: sink_id: Destination ID expand: Include expanded references - + Returns: Destination instance - + Examples: client.destinations.get(321) """ return super().get(sink_id, expand) - + def create(self, data: DestinationCreate) -> Destination: """ Create new destination. - + Args: data: Destination creation data - + Returns: Created destination - + Examples: new_sink = client.destinations.create(DestinationCreate(name="My Sink", connector=...)) """ return super().create(data) - + def update(self, sink_id: int, data: DestinationUpdate) -> Destination: """ Update destination. - + Args: sink_id: Destination ID data: Updated destination data - + Returns: Updated destination """ return super().update(sink_id, data) - + def delete(self, sink_id: int) -> Dict[str, Any]: """ Delete destination. - + Args: sink_id: Destination ID - + Returns: Response with status """ return super().delete(sink_id) - + def activate(self, sink_id: int) -> Destination: """ Activate destination. - + Args: sink_id: Destination ID - + Returns: Activated destination """ return super().activate(sink_id) - + def pause(self, sink_id: int) -> Destination: """ Pause destination. - + Args: sink_id: Destination ID - + Returns: Paused destination """ return super().pause(sink_id) - - def copy(self, sink_id: int, options: Optional[DestinationCopyOptions] = None) -> Destination: + + def copy( + self, sink_id: int, options: Optional[DestinationCopyOptions] = None + ) -> Destination: """ Copy a destination. - + Args: sink_id: Destination ID options: Copy options - + Returns: Copied destination """ diff --git a/nexla_sdk/resources/doc_containers.py b/nexla_sdk/resources/doc_containers.py index d75b109..ff2df5d 100644 --- a/nexla_sdk/resources/doc_containers.py +++ b/nexla_sdk/resources/doc_containers.py @@ -1,6 +1,7 @@ from typing import List -from nexla_sdk.resources.base_resource import BaseResource + from nexla_sdk.models.common import LogEntry +from nexla_sdk.resources.base_resource import BaseResource class DocContainersResource(BaseResource): @@ -13,7 +14,7 @@ def __init__(self, client): def get_audit_log(self, doc_container_id: int, **params) -> List[LogEntry]: path = f"{self._path}/{doc_container_id}/audit_log" - response = self._make_request('GET', path, params=params) + response = self._make_request("GET", path, params=params) return [LogEntry.model_validate(item) for item in (response or [])] # Accessors via BaseResource methods are compatible diff --git a/nexla_sdk/resources/flows.py b/nexla_sdk/resources/flows.py index 50bfe02..4c6291d 100644 --- a/nexla_sdk/resources/flows.py +++ b/nexla_sdk/resources/flows.py @@ -1,24 +1,30 @@ -from typing import List, Optional, Dict, Any, Union -from nexla_sdk.resources.base_resource import BaseResource +from typing import Any, Dict, List, Optional, Union + +from nexla_sdk.models.flows.requests import FlowCopyOptions from nexla_sdk.models.flows.responses import ( - FlowResponse, FlowLogsResponse, FlowMetricsApiResponse, DocsRecommendation + DocsRecommendation, + FlowLogsResponse, + FlowMetricsApiResponse, + FlowResponse, ) -from nexla_sdk.models.flows.requests import FlowCopyOptions +from nexla_sdk.resources.base_resource import BaseResource class FlowsResource(BaseResource): """Resource for managing data flows.""" - + def __init__(self, client): super().__init__(client) self._path = "/flows" self._model_class = FlowResponse - - def list(self, - flows_only: bool = False, - include_run_metrics: bool = False, - access_role: Optional[str] = None, - **kwargs) -> List[FlowResponse]: + + def list( + self, + flows_only: bool = False, + include_run_metrics: bool = False, + access_role: Optional[str] = None, + **kwargs, + ) -> List[FlowResponse]: """ List flows with optional filters. @@ -40,79 +46,89 @@ def list(self, """ params = kwargs.copy() if flows_only: - params['flows_only'] = 1 + params["flows_only"] = 1 if include_run_metrics: - params['include_run_metrics'] = 1 + params["include_run_metrics"] = 1 if access_role: - params['access_role'] = access_role + params["access_role"] = access_role - response = self._make_request('GET', self._path, params=params) + response = self._make_request("GET", self._path, params=params) # API returns a single FlowResponse object for list return [self._parse_response(response)] - - def get(self, flow_id: int, flows_only: bool = False) -> FlowResponse: + + def get( + self, flow_id: int, flows_only: bool = False, include_run_metrics: bool = False + ) -> FlowResponse: """ Get flow by ID. - + Args: flow_id: Flow ID flows_only: Only return flow structure without resource details - + include_run_metrics: Include run metrics in response + Returns: Flow response """ path = f"{self._path}/{flow_id}" - params = {'flows_only': 1} if flows_only else {} - response = self._make_request('GET', path, params=params) + params = {} + if flows_only: + params["flows_only"] = 1 + if include_run_metrics: + params["include_run_metrics"] = 1 + response = self._make_request("GET", path, params=params) return self._parse_response(response) - - def get_by_resource(self, - resource_type: str, - resource_id: int, - flows_only: bool = False) -> FlowResponse: + + def get_by_resource( + self, resource_type: str, resource_id: int, flows_only: bool = False + ) -> FlowResponse: """ Get flow by resource ID. - + Args: resource_type: Type of resource (data_sources, data_sets, data_sinks) resource_id: Resource ID flows_only: Only return flow structure - + Returns: Flow response """ path = f"/{resource_type}/{resource_id}/flow" - params = {'flows_only': 1} if flows_only else {} - - response = self._make_request('GET', path, params=params) + params = {"flows_only": 1} if flows_only else {} + + response = self._make_request("GET", path, params=params) return self._parse_response(response) - - def activate(self, flow_id: int, all: bool = False, full_tree: bool = False) -> FlowResponse: + + def activate( + self, flow_id: int, all: bool = False, full_tree: bool = False + ) -> FlowResponse: """ Activate a flow. - + Args: flow_id: Flow ID all: Activate entire flow tree - + Returns: Activated flow """ path = f"{self._path}/{flow_id}/activate" params = {} if all: - params['all'] = 1 + params["all"] = 1 if full_tree: - params['full_tree'] = 1 - - response = self._make_request('PUT', path, params=params) + params["full_tree"] = 1 + + response = self._make_request("PUT", path, params=params) return self._parse_response(response) - - def pause(self, - flow_id: int, - all: bool = False, - full_tree: bool = False, - async_mode: bool = False) -> FlowResponse: + + def pause( + self, + flow_id: int, + all: bool = False, + full_tree: bool = False, + async_mode: bool = False, + ) -> FlowResponse: """ Pause a flow. @@ -128,107 +144,117 @@ def pause(self, path = f"{self._path}/{flow_id}/pause" params = {} if all: - params['all'] = 1 + params["all"] = 1 if full_tree: - params['full_tree'] = 1 + params["full_tree"] = 1 if async_mode: - params['async'] = 1 + params["async"] = 1 - response = self._make_request('PUT', path, params=params) + response = self._make_request("PUT", path, params=params) return self._parse_response(response) - - def copy(self, flow_id: int, options: Optional[FlowCopyOptions] = None) -> FlowResponse: + + def copy( + self, flow_id: int, options: Optional[FlowCopyOptions] = None + ) -> FlowResponse: """ Copy a flow. - + Args: flow_id: Flow ID options: Copy options - + Returns: Copied flow """ return super().copy(flow_id, options) - + def delete(self, flow_id: int) -> Dict[str, Any]: """ Delete flow. - + Args: flow_id: Flow ID - + Returns: Response with status """ return super().delete(flow_id) - - def delete_by_resource(self, resource_type: str, resource_id: int) -> Dict[str, Any]: + + def delete_by_resource( + self, resource_type: str, resource_id: int + ) -> Dict[str, Any]: """ Delete flow by resource ID. - + Args: resource_type: Type of resource resource_id: Resource ID - + Returns: Response status """ path = f"/{resource_type}/{resource_id}/flow" - return self._make_request('DELETE', path) - - def activate_by_resource(self, - resource_type: str, - resource_id: int, - all: bool = False, - full_tree: bool = False) -> FlowResponse: + return self._make_request("DELETE", path) + + def activate_by_resource( + self, + resource_type: str, + resource_id: int, + all: bool = False, + full_tree: bool = False, + ) -> FlowResponse: """ Activate flow by resource ID. - + Args: resource_type: Type of resource resource_id: Resource ID all: Activate entire flow tree - + Returns: Activated flow """ path = f"/{resource_type}/{resource_id}/activate" params = {} if all: - params['all'] = 1 + params["all"] = 1 if full_tree: - params['full_tree'] = 1 - - response = self._make_request('PUT', path, params=params) + params["full_tree"] = 1 + + response = self._make_request("PUT", path, params=params) return self._parse_response(response) - - def pause_by_resource(self, - resource_type: str, - resource_id: int, - all: bool = False, - full_tree: bool = False) -> FlowResponse: + + def pause_by_resource( + self, + resource_type: str, + resource_id: int, + all: bool = False, + full_tree: bool = False, + ) -> FlowResponse: """ Pause flow by resource ID. - + Args: resource_type: Type of resource resource_id: Resource ID all: Pause entire flow tree - + Returns: Paused flow """ path = f"/{resource_type}/{resource_id}/pause" params = {} if all: - params['all'] = 1 + params["all"] = 1 if full_tree: - params['full_tree'] = 1 - - response = self._make_request('PUT', path, params=params) + params["full_tree"] = 1 + + response = self._make_request("PUT", path, params=params) return self._parse_response(response) - def docs_recommendation(self, flow_id: int) -> Union[DocsRecommendation, Dict[str, Any]]: + def docs_recommendation( + self, flow_id: int + ) -> Union[DocsRecommendation, Dict[str, Any]]: """Generate AI suggestion for flow documentation. Args: @@ -239,20 +265,22 @@ def docs_recommendation(self, flow_id: int) -> Union[DocsRecommendation, Dict[st or raw dict if response doesn't match expected schema. """ path = f"{self._path}/{flow_id}/docs/recommendation" - response = self._make_request('POST', path) + response = self._make_request("POST", path) try: return DocsRecommendation.model_validate(response) except Exception: return response - def get_logs(self, - resource_type: str, - resource_id: int, - run_id: int, - from_ts: int, - to_ts: int = None, - page: int = None, - per_page: int = None) -> Union[FlowLogsResponse, Dict[str, Any]]: + def get_logs( + self, + resource_type: str, + resource_id: int, + run_id: int, + from_ts: int, + to_ts: int = None, + page: int = None, + per_page: int = None, + ) -> Union[FlowLogsResponse, Dict[str, Any]]: """Get flow execution logs for a specific run id of a flow. Args: @@ -270,30 +298,32 @@ def get_logs(self, """ path = f"/data_flows/{resource_type}/{resource_id}/logs" params = { - 'run_id': run_id, - 'from': from_ts, + "run_id": run_id, + "from": from_ts, } if to_ts is not None: - params['to'] = to_ts + params["to"] = to_ts if page is not None: - params['page'] = page + params["page"] = page if per_page is not None: - params['per_page'] = per_page - response = self._make_request('GET', path, params=params) + params["per_page"] = per_page + response = self._make_request("GET", path, params=params) try: return FlowLogsResponse.model_validate(response) except Exception: return response - def get_metrics(self, - resource_type: str, - resource_id: int, - from_date: str, - to_date: str = None, - groupby: str = None, - orderby: str = None, - page: int = None, - per_page: int = None) -> Union[FlowMetricsApiResponse, Dict[str, Any]]: + def get_metrics( + self, + resource_type: str, + resource_id: int, + from_date: str, + to_date: str = None, + groupby: str = None, + orderby: str = None, + page: int = None, + per_page: int = None, + ) -> Union[FlowMetricsApiResponse, Dict[str, Any]]: """Get flow metrics for a flow node keyed by resource id. Args: @@ -311,19 +341,19 @@ def get_metrics(self, or raw dict if response doesn't match expected schema. """ path = f"/data_flows/{resource_type}/{resource_id}/metrics" - params = {'from': from_date} + params = {"from": from_date} if to_date: - params['to'] = to_date + params["to"] = to_date if groupby: - params['groupby'] = groupby + params["groupby"] = groupby if orderby: - params['orderby'] = orderby + params["orderby"] = orderby if page is not None: - params['page'] = page + params["page"] = page if per_page is not None: - params['per_page'] = per_page + params["per_page"] = per_page - response = self._make_request('GET', path, params=params) + response = self._make_request("GET", path, params=params) try: return FlowMetricsApiResponse.model_validate(response) except Exception: diff --git a/nexla_sdk/resources/genai.py b/nexla_sdk/resources/genai.py index 376124a..1fd519b 100644 --- a/nexla_sdk/resources/genai.py +++ b/nexla_sdk/resources/genai.py @@ -1,9 +1,16 @@ -from typing import List, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.genai.responses import GenAiConfig, GenAiOrgSetting, ActiveConfigView +from typing import Any, Dict, List + from nexla_sdk.models.genai.requests import ( - GenAiConfigPayload, GenAiConfigCreatePayload, GenAiOrgSettingPayload, + GenAiConfigCreatePayload, + GenAiConfigPayload, + GenAiOrgSettingPayload, ) +from nexla_sdk.models.genai.responses import ( + ActiveConfigView, + GenAiConfig, + GenAiOrgSetting, +) +from nexla_sdk.resources.base_resource import BaseResource class GenAIResource(BaseResource): @@ -16,48 +23,66 @@ def __init__(self, client): # Integration Configs def list_configs(self) -> List[GenAiConfig]: - response = self._make_request('GET', "/gen_ai_integration_configs") + response = self._make_request("GET", "/gen_ai_integration_configs") return [GenAiConfig.model_validate(item) for item in (response or [])] def create_config(self, payload: GenAiConfigCreatePayload) -> GenAiConfig: data = self._serialize_data(payload) - response = self._make_request('POST', "/gen_ai_integration_configs", json=data) + response = self._make_request("POST", "/gen_ai_integration_configs", json=data) return GenAiConfig.model_validate(response) def get_config(self, gen_ai_config_id: int) -> GenAiConfig: - response = self._make_request('GET', f"/gen_ai_integration_configs/{gen_ai_config_id}") + response = self._make_request( + "GET", f"/gen_ai_integration_configs/{gen_ai_config_id}" + ) return GenAiConfig.model_validate(response) - def update_config(self, gen_ai_config_id: int, payload: GenAiConfigPayload) -> GenAiConfig: + def update_config( + self, gen_ai_config_id: int, payload: GenAiConfigPayload + ) -> GenAiConfig: data = self._serialize_data(payload) - response = self._make_request('PUT', f"/gen_ai_integration_configs/{gen_ai_config_id}", json=data) + response = self._make_request( + "PUT", f"/gen_ai_integration_configs/{gen_ai_config_id}", json=data + ) return GenAiConfig.model_validate(response) def delete_config(self, gen_ai_config_id: int) -> Dict[str, Any]: - return self._make_request('DELETE', f"/gen_ai_integration_configs/{gen_ai_config_id}") + return self._make_request( + "DELETE", f"/gen_ai_integration_configs/{gen_ai_config_id}" + ) # Org Settings - def list_org_settings(self, org_id: int = None, all: bool = False) -> List[GenAiOrgSetting]: + def list_org_settings( + self, org_id: int = None, all: bool = False + ) -> List[GenAiOrgSetting]: params = {} if org_id is not None: - params['org_id'] = org_id + params["org_id"] = org_id if all: - params['all'] = True - response = self._make_request('GET', "/gen_ai_org_settings", params=params) + params["all"] = True + response = self._make_request("GET", "/gen_ai_org_settings", params=params) return [GenAiOrgSetting.model_validate(item) for item in (response or [])] def create_org_setting(self, payload: GenAiOrgSettingPayload) -> GenAiOrgSetting: data = self._serialize_data(payload) - response = self._make_request('POST', "/gen_ai_org_settings", json=data) + response = self._make_request("POST", "/gen_ai_org_settings", json=data) return GenAiOrgSetting.model_validate(response) def get_org_setting(self, gen_ai_org_setting_id: int) -> GenAiOrgSetting: - response = self._make_request('GET', f"/gen_ai_org_settings/{gen_ai_org_setting_id}") + response = self._make_request( + "GET", f"/gen_ai_org_settings/{gen_ai_org_setting_id}" + ) return GenAiOrgSetting.model_validate(response) def delete_org_setting(self, gen_ai_org_setting_id: int) -> Dict[str, Any]: - return self._make_request('DELETE', f"/gen_ai_org_settings/{gen_ai_org_setting_id}") + return self._make_request( + "DELETE", f"/gen_ai_org_settings/{gen_ai_org_setting_id}" + ) def show_active_config(self, gen_ai_usage: str) -> ActiveConfigView: - response = self._make_request('GET', "/gen_ai_org_settings/active_config", params={'gen_ai_usage': gen_ai_usage}) + response = self._make_request( + "GET", + "/gen_ai_org_settings/active_config", + params={"gen_ai_usage": gen_ai_usage}, + ) return ActiveConfigView.model_validate(response) diff --git a/nexla_sdk/resources/lookups.py b/nexla_sdk/resources/lookups.py index f47e233..5c8dad0 100644 --- a/nexla_sdk/resources/lookups.py +++ b/nexla_sdk/resources/lookups.py @@ -1,28 +1,34 @@ """Lookups resource implementation.""" -from typing import List, Dict, Any, Union -from nexla_sdk.resources.base_resource import BaseResource + +from typing import Any, Dict, List, Union + +from nexla_sdk.models.lookups.requests import ( + LookupCreate, + LookupEntriesUpsert, + LookupUpdate, +) from nexla_sdk.models.lookups.responses import Lookup -from nexla_sdk.models.lookups.requests import LookupCreate, LookupUpdate, LookupEntriesUpsert +from nexla_sdk.resources.base_resource import BaseResource class LookupsResource(BaseResource): """Resource for managing lookups (data maps).""" - + def __init__(self, client): super().__init__(client) self._path = "/data_maps" self._model_class = Lookup - + def list(self, **kwargs) -> List[Lookup]: """ List lookups with optional filters. - + Args: page: Page number (via kwargs) per_page: Items per page (via kwargs) access_role: Filter by access role (via kwargs) **kwargs: Additional query parameters - + Returns: List of lookups @@ -30,121 +36,121 @@ def list(self, **kwargs) -> List[Lookup]: client.lookups.list(page=1, per_page=50) """ return super().list(**kwargs) - + def get(self, data_map_id: int, expand: bool = False) -> Lookup: """ Get single lookup by ID. - + Args: data_map_id: Lookup ID expand: Include expanded references - + Returns: Lookup instance - + Examples: client.lookups.get(55) """ return super().get(data_map_id, expand) - + def create(self, data: LookupCreate) -> Lookup: """ Create new lookup. - + Args: data: Lookup creation data - + Returns: Created lookup - + Examples: client.lookups.create(LookupCreate(name="status-map", ...)) """ return super().create(data) - + def update(self, data_map_id: int, data: LookupUpdate) -> Lookup: """ Update lookup. - + Args: data_map_id: Lookup ID data: Updated lookup data - + Returns: Updated lookup """ return super().update(data_map_id, data) - + def delete(self, data_map_id: int) -> Dict[str, Any]: """ Delete lookup. - + Args: data_map_id: Lookup ID - + Returns: Response with status """ return super().delete(data_map_id) - def upsert_entries(self, - data_map_id: int, - entries: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def upsert_entries( + self, data_map_id: int, entries: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: """ Upsert entries in a lookup. - + Args: data_map_id: Lookup ID entries: List of entries to upsert - + Returns: Response with entry results """ path = f"{self._path}/{data_map_id}/entries" - + # Create request model request = LookupEntriesUpsert(entries=entries) - - return self._make_request('PUT', path, json=request.to_dict()) - - def get_entries(self, - data_map_id: int, - entry_keys: Union[str, List[str]]) -> List[Dict[str, Any]]: + + return self._make_request("PUT", path, json=request.to_dict()) + + def get_entries( + self, data_map_id: int, entry_keys: Union[str, List[str]] + ) -> List[Dict[str, Any]]: """ Get specific entries from a lookup. - + Args: data_map_id: Lookup ID entry_keys: Single key or list of keys to retrieve - + Returns: List of matching entries """ if isinstance(entry_keys, list): - keys_str = ','.join(str(key) for key in entry_keys) + keys_str = ",".join(str(key) for key in entry_keys) else: keys_str = str(entry_keys) - + path = f"/data_maps/{data_map_id}/entries/{keys_str}" - return self._make_request('GET', path) - - def delete_entries(self, - data_map_id: int, - entry_keys: Union[str, List[str]]) -> Dict[str, Any]: + return self._make_request("GET", path) + + def delete_entries( + self, data_map_id: int, entry_keys: Union[str, List[str]] + ) -> Dict[str, Any]: """ Delete specific entries from a lookup. - + Args: data_map_id: Lookup ID entry_keys: Single key or list of keys to delete - + Returns: Response with deletion results """ if isinstance(entry_keys, list): - keys_str = ','.join(str(key) for key in entry_keys) + keys_str = ",".join(str(key) for key in entry_keys) else: keys_str = str(entry_keys) - + path = f"/data_maps/{data_map_id}/entries/{keys_str}" - return self._make_request('DELETE', path) + return self._make_request("DELETE", path) diff --git a/nexla_sdk/resources/marketplace.py b/nexla_sdk/resources/marketplace.py index 9257f0d..e1e9465 100644 --- a/nexla_sdk/resources/marketplace.py +++ b/nexla_sdk/resources/marketplace.py @@ -1,12 +1,16 @@ -from typing import List, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource +from typing import Any, Dict, List + +from nexla_sdk.models.marketplace.requests import ( + CustodiansPayload, + MarketplaceDomainCreate, + MarketplaceDomainsItemCreate, +) from nexla_sdk.models.marketplace.responses import ( - MarketplaceDomain, MarketplaceDomainsItem, + MarketplaceDomain, + MarketplaceDomainsItem, ) from nexla_sdk.models.organizations.responses import CustodianUser -from nexla_sdk.models.marketplace.requests import ( - MarketplaceDomainCreate, MarketplaceDomainsItemCreate, CustodiansPayload, -) +from nexla_sdk.resources.base_resource import BaseResource class MarketplaceResource(BaseResource): @@ -19,60 +23,84 @@ def __init__(self, client): # Domains def list_domains(self) -> List[MarketplaceDomain]: - response = self._make_request('GET', f"{self._path}/domains") + response = self._make_request("GET", f"{self._path}/domains") return self._parse_response(response, MarketplaceDomain) # type: ignore[arg-type] def create_domains(self, data: MarketplaceDomainCreate) -> List[MarketplaceDomain]: payload = self._serialize_data(data) - response = self._make_request('POST', f"{self._path}/domains", json=payload) + response = self._make_request("POST", f"{self._path}/domains", json=payload) return self._parse_response(response, MarketplaceDomain) # type: ignore[arg-type] def get_domains_for_org(self, org_id: int) -> List[MarketplaceDomain]: - response = self._make_request('GET', f"{self._path}/domains/for_org", params={'org_id': org_id}) + response = self._make_request( + "GET", f"{self._path}/domains/for_org", params={"org_id": org_id} + ) return self._parse_response(response, MarketplaceDomain) # type: ignore[arg-type] def get_domain(self, domain_id: int) -> MarketplaceDomain: - response = self._make_request('GET', f"{self._path}/domains/{domain_id}") + response = self._make_request("GET", f"{self._path}/domains/{domain_id}") return self._parse_response(response, MarketplaceDomain) # type: ignore[arg-type] - def update_domain(self, domain_id: int, data: MarketplaceDomainCreate) -> MarketplaceDomain: + def update_domain( + self, domain_id: int, data: MarketplaceDomainCreate + ) -> MarketplaceDomain: payload = self._serialize_data(data) - response = self._make_request('PUT', f"{self._path}/domains/{domain_id}", json=payload) + response = self._make_request( + "PUT", f"{self._path}/domains/{domain_id}", json=payload + ) return self._parse_response(response, MarketplaceDomain) # type: ignore[arg-type] def create_domain(self, data: MarketplaceDomainCreate) -> MarketplaceDomain: payload = self._serialize_data(data) - response = self._make_request('POST', f"{self._path}/domains", json=payload) + response = self._make_request("POST", f"{self._path}/domains", json=payload) return self._parse_response(response, MarketplaceDomain) # type: ignore[arg-type] def delete_domain(self, domain_id: int) -> Dict[str, Any]: - return self._make_request('DELETE', f"{self._path}/domains/{domain_id}") + return self._make_request("DELETE", f"{self._path}/domains/{domain_id}") # Items def list_domain_items(self, domain_id: int) -> List[MarketplaceDomainsItem]: - response = self._make_request('GET', f"{self._path}/domains/{domain_id}/items") + response = self._make_request("GET", f"{self._path}/domains/{domain_id}/items") return self._parse_response(response, MarketplaceDomainsItem) # type: ignore[arg-type] - def create_domain_item(self, domain_id: int, data: MarketplaceDomainsItemCreate) -> List[MarketplaceDomainsItem]: + def create_domain_item( + self, domain_id: int, data: MarketplaceDomainsItemCreate + ) -> List[MarketplaceDomainsItem]: payload = self._serialize_data(data) - response = self._make_request('POST', f"{self._path}/domains/{domain_id}/items", json=payload) + response = self._make_request( + "POST", f"{self._path}/domains/{domain_id}/items", json=payload + ) return self._parse_response(response, MarketplaceDomainsItem) # type: ignore[arg-type] # Custodians def list_domain_custodians(self, domain_id: int) -> List[CustodianUser]: - response = self._make_request('GET', f"{self._path}/domains/{domain_id}/custodians") + response = self._make_request( + "GET", f"{self._path}/domains/{domain_id}/custodians" + ) return self._parse_response(response, CustodianUser) # type: ignore[arg-type] - def update_domain_custodians(self, domain_id: int, payload: CustodiansPayload) -> List[CustodianUser]: + def update_domain_custodians( + self, domain_id: int, payload: CustodiansPayload + ) -> List[CustodianUser]: data = self._serialize_data(payload) - response = self._make_request('PUT', f"{self._path}/domains/{domain_id}/custodians", json=data) + response = self._make_request( + "PUT", f"{self._path}/domains/{domain_id}/custodians", json=data + ) return self._parse_response(response, CustodianUser) # type: ignore[arg-type] - def add_domain_custodians(self, domain_id: int, payload: CustodiansPayload) -> List[CustodianUser]: + def add_domain_custodians( + self, domain_id: int, payload: CustodiansPayload + ) -> List[CustodianUser]: data = self._serialize_data(payload) - response = self._make_request('POST', f"{self._path}/domains/{domain_id}/custodians", json=data) + response = self._make_request( + "POST", f"{self._path}/domains/{domain_id}/custodians", json=data + ) return self._parse_response(response, CustodianUser) # type: ignore[arg-type] - def remove_domain_custodians(self, domain_id: int, payload: CustodiansPayload) -> Dict[str, Any]: + def remove_domain_custodians( + self, domain_id: int, payload: CustodiansPayload + ) -> Dict[str, Any]: data = self._serialize_data(payload) - return self._make_request('DELETE', f"{self._path}/domains/{domain_id}/custodians", json=data) + return self._make_request( + "DELETE", f"{self._path}/domains/{domain_id}/custodians", json=data + ) diff --git a/nexla_sdk/resources/metrics.py b/nexla_sdk/resources/metrics.py index 3dc0140..1a43b55 100644 --- a/nexla_sdk/resources/metrics.py +++ b/nexla_sdk/resources/metrics.py @@ -1,63 +1,62 @@ -from typing import Optional, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.metrics.responses import ( - MetricsResponse, - MetricsByRunResponse -) +from typing import Any, Dict, Optional + from nexla_sdk.models.metrics.enums import ResourceType +from nexla_sdk.models.metrics.responses import MetricsByRunResponse, MetricsResponse +from nexla_sdk.resources.base_resource import BaseResource class MetricsResource(BaseResource): """ Resource for retrieving metrics. - + Note: This resource already uses strongly-typed Pydantic models for all return types and doesn't follow standard CRUD patterns, so no additional typed overrides are needed. """ - + def __init__(self, client): super().__init__(client) self._path = "" # Metrics endpoints are distributed - - def get_resource_daily_metrics(self, - resource_type: ResourceType, - resource_id: int, - from_date: str, - to_date: Optional[str] = None) -> MetricsResponse: + + def get_resource_daily_metrics( + self, + resource_type: ResourceType, + resource_id: int, + from_date: str, + to_date: Optional[str] = None, + ) -> MetricsResponse: """ Get daily metrics for a resource. - + Args: resource_type: Type of resource (data_sources, data_sets, data_sinks) resource_id: Resource ID from_date: Start date (YYYY-MM-DD) to_date: End date (optional) - + Returns: Daily metrics """ path = f"/{resource_type}/{resource_id}/metrics" - params = { - 'from': from_date, - 'aggregate': 1 - } + params = {"from": from_date, "aggregate": 1} if to_date: - params['to'] = to_date - - response = self._make_request('GET', path, params=params) + params["to"] = to_date + + response = self._make_request("GET", path, params=params) return MetricsResponse(**response) - - def get_resource_metrics_by_run(self, - resource_type: ResourceType, - resource_id: int, - groupby: Optional[str] = None, - orderby: Optional[str] = None, - page: Optional[int] = None, - size: Optional[int] = None) -> MetricsByRunResponse: + + def get_resource_metrics_by_run( + self, + resource_type: ResourceType, + resource_id: int, + groupby: Optional[str] = None, + orderby: Optional[str] = None, + page: Optional[int] = None, + size: Optional[int] = None, + ) -> MetricsByRunResponse: """ Get metrics by run for a resource. - + Args: resource_type: Type of resource resource_id: Resource ID @@ -65,72 +64,76 @@ def get_resource_metrics_by_run(self, orderby: Order by field (runId, lastWritten) page: Page number size: Page size - + Returns: Metrics by run """ path = f"/{resource_type}/{resource_id}/metrics/run_summary" params = {} if groupby: - params['groupby'] = groupby + params["groupby"] = groupby if orderby: - params['orderby'] = orderby + params["orderby"] = orderby if page: - params['page'] = page + params["page"] = page if size: - params['size'] = size - - response = self._make_request('GET', path, params=params) + params["size"] = size + + response = self._make_request("GET", path, params=params) return MetricsByRunResponse(**response) - + def get_rate_limits(self) -> Dict[str, Any]: """ Get current rate limit and usage. - + Returns: Rate limit information """ path = "/limits" - return self._make_request('GET', path) + return self._make_request("GET", path) # Convenience wrappers for flow-level logs/metrics - def get_flow_metrics(self, - resource_type: str, - resource_id: int, - from_date: str, - to_date: str = None, - groupby: str = None, - orderby: str = None, - page: int = None, - per_page: int = None) -> Dict[str, Any]: + def get_flow_metrics( + self, + resource_type: str, + resource_id: int, + from_date: str, + to_date: str = None, + groupby: str = None, + orderby: str = None, + page: int = None, + per_page: int = None, + ) -> Dict[str, Any]: path = f"/data_flows/{resource_type}/{resource_id}/metrics" - params = {'from': from_date} + params = {"from": from_date} if to_date: - params['to'] = to_date + params["to"] = to_date if groupby: - params['groupby'] = groupby + params["groupby"] = groupby if orderby: - params['orderby'] = orderby + params["orderby"] = orderby if page is not None: - params['page'] = page + params["page"] = page if per_page is not None: - params['per_page'] = per_page - return self._make_request('GET', path, params=params) - - def get_flow_logs(self, - resource_type: str, - resource_id: int, - run_id: int, - from_ts: int, - to_ts: int = None, - page: int = None, - per_page: int = None) -> Dict[str, Any]: + params["per_page"] = per_page + return self._make_request("GET", path, params=params) + + def get_flow_logs( + self, + resource_type: str, + resource_id: int, + run_id: int, + from_ts: int, + to_ts: int = None, + page: int = None, + per_page: int = None, + ) -> Dict[str, Any]: path = f"/data_flows/{resource_type}/{resource_id}/logs" - params = {'run_id': run_id, 'from': from_ts} + params = {"run_id": run_id, "from": from_ts} if to_ts is not None: - params['to'] = to_ts + params["to"] = to_ts if page is not None: - params['page'] = page + params["page"] = page if per_page is not None: - params['per_page'] = per_page - return self._make_request('GET', path, params=params) + params["per_page"] = per_page + return self._make_request("GET", path, params=params) diff --git a/nexla_sdk/resources/nexsets.py b/nexla_sdk/resources/nexsets.py index 97df24f..5fcfd31 100644 --- a/nexla_sdk/resources/nexsets.py +++ b/nexla_sdk/resources/nexsets.py @@ -1,27 +1,32 @@ -from typing import List, Optional, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource +from typing import Any, Dict, List, Optional + +from nexla_sdk.models.nexsets.requests import ( + NexsetCopyOptions, + NexsetCreate, + NexsetUpdate, +) from nexla_sdk.models.nexsets.responses import Nexset, NexsetSample -from nexla_sdk.models.nexsets.requests import NexsetCreate, NexsetUpdate, NexsetCopyOptions +from nexla_sdk.resources.base_resource import BaseResource class NexsetsResource(BaseResource): """Resource for managing nexsets (data sets).""" - + def __init__(self, client): super().__init__(client) self._path = "/data_sets" self._model_class = Nexset - + def list(self, **kwargs) -> List[Nexset]: """ List nexsets with optional filters. - + Args: page: Page number (via kwargs) per_page: Items per page (via kwargs) access_role: Filter by access role (via kwargs) **kwargs: Additional query parameters - + Returns: List of nexsets @@ -29,126 +34,124 @@ def list(self, **kwargs) -> List[Nexset]: client.nexsets.list(page=1, per_page=50) """ return super().list(**kwargs) - + def get(self, set_id: int, expand: bool = False) -> Nexset: """ Get single nexset by ID. - + Args: set_id: Nexset ID expand: Include expanded references - + Returns: Nexset instance - + Examples: client.nexsets.get(789) """ return super().get(set_id, expand) - + def create(self, data: NexsetCreate) -> Nexset: """ Create new nexset. - + Args: data: Nexset creation data - + Returns: Created nexset - + Examples: new_set = client.nexsets.create(NexsetCreate(name="My Dataset", ...)) """ return super().create(data) - + def update(self, set_id: int, data: NexsetUpdate) -> Nexset: """ Update nexset. - + Args: set_id: Nexset ID data: Updated nexset data - + Returns: Updated nexset """ return super().update(set_id, data) - + def delete(self, set_id: int) -> Dict[str, Any]: """ Delete nexset. - + Args: set_id: Nexset ID - + Returns: Response with status """ return super().delete(set_id) - + def activate(self, set_id: int) -> Nexset: """ Activate nexset. - + Args: set_id: Nexset ID - + Returns: Activated nexset """ return super().activate(set_id) - + def pause(self, set_id: int) -> Nexset: """ Pause nexset. - + Args: set_id: Nexset ID - + Returns: Paused nexset """ return super().pause(set_id) - def get_samples(self, - set_id: int, - count: int = 10, - include_metadata: bool = False, - live: bool = False) -> List[NexsetSample]: + def get_samples( + self, + set_id: int, + count: int = 10, + include_metadata: bool = False, + live: bool = False, + ) -> List[NexsetSample]: """ Get sample records from a nexset. - + Args: set_id: Nexset ID count: Maximum number of samples include_metadata: Include Nexla metadata live: Fetch live samples from topic - + Returns: List of sample records """ path = f"{self._path}/{set_id}/samples" - params = { - 'count': count, - 'include_metadata': include_metadata, - 'live': live - } - - response = self._make_request('GET', path, params=params) - + params = {"count": count, "include_metadata": include_metadata, "live": live} + + response = self._make_request("GET", path, params=params) + # Handle both response formats if isinstance(response, list): return [NexsetSample(**item) for item in response] return response - + def copy(self, set_id: int, options: Optional[NexsetCopyOptions] = None) -> Nexset: """ Copy a nexset. - + Args: set_id: Nexset ID options: Copy options - + Returns: Copied nexset """ @@ -158,4 +161,4 @@ def copy(self, set_id: int, options: Optional[NexsetCopyOptions] = None) -> Nexs def docs_recommendation(self, set_id: int) -> Dict[str, Any]: """Generate AI suggestion for Nexset documentation.""" path = f"{self._path}/{set_id}/docs/recommendation" - return self._make_request('POST', path) + return self._make_request("POST", path) diff --git a/nexla_sdk/resources/notifications.py b/nexla_sdk/resources/notifications.py index d4e7069..9430aee 100644 --- a/nexla_sdk/resources/notifications.py +++ b/nexla_sdk/resources/notifications.py @@ -1,57 +1,65 @@ -from typing import List, Optional, Dict, Any, Union -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.notifications.responses import ( - Notification, NotificationType, NotificationChannelSetting, - NotificationSetting, NotificationCount -) +from typing import Any, Dict, List, Optional, Union + from nexla_sdk.models.notifications.requests import ( - NotificationChannelSettingCreate, NotificationChannelSettingUpdate, - NotificationSettingCreate, NotificationSettingUpdate + NotificationChannelSettingCreate, + NotificationChannelSettingUpdate, + NotificationSettingCreate, + NotificationSettingUpdate, +) +from nexla_sdk.models.notifications.responses import ( + Notification, + NotificationChannelSetting, + NotificationCount, + NotificationSetting, + NotificationType, ) +from nexla_sdk.resources.base_resource import BaseResource class NotificationsResource(BaseResource): """Resource for managing notifications.""" - + def __init__(self, client): super().__init__(client) self._path = "/notifications" self._model_class = Notification - + def get(self, notification_id: int, expand: bool = False) -> Notification: """ Get single notification by ID. - + Args: notification_id: Notification ID expand: Include expanded references - + Returns: Notification instance """ return super().get(notification_id, expand) - + def delete(self, notification_id: int) -> Dict[str, Any]: """ Delete notification. - + Args: notification_id: Notification ID - + Returns: Response with status """ return super().delete(notification_id) - - def list(self, - read: Optional[int] = None, - level: Optional[str] = None, - from_timestamp: Optional[int] = None, - to_timestamp: Optional[int] = None, - **kwargs) -> List[Notification]: + + def list( + self, + read: Optional[int] = None, + level: Optional[str] = None, + from_timestamp: Optional[int] = None, + to_timestamp: Optional[int] = None, + **kwargs, + ) -> List[Notification]: """ List notifications with optional filters. - + Args: read: Filter by read status (0=unread, 1=read) level: Filter by level (DEBUG, INFO, WARN, ERROR, RECOVERED, RESOLVED) @@ -60,322 +68,325 @@ def list(self, page: Page number (via kwargs) per_page: Items per page (via kwargs) **kwargs: Additional parameters - + Returns: List of notifications - + Examples: client.notifications.list(read=0, level="ERROR", page=1, per_page=50) """ params = kwargs.copy() if read is not None: - params['read'] = read + params["read"] = read if level: - params['level'] = level + params["level"] = level if from_timestamp: - params['from'] = from_timestamp + params["from"] = from_timestamp if to_timestamp: - params['to'] = to_timestamp - + params["to"] = to_timestamp + return super().list(**params) - + def delete_all(self) -> Dict[str, Any]: """ Delete all notifications. - + Returns: Response status """ path = f"{self._path}/all" - return self._make_request('DELETE', path) - + return self._make_request("DELETE", path) + def get_count(self, read: Optional[int] = None) -> NotificationCount: """ Get notification count. - + Args: read: Filter by read status - + Returns: Notification count """ path = f"{self._path}/count" - params = {'read': read} if read is not None else {} - response = self._make_request('GET', path, params=params) + params = {"read": read} if read is not None else {} + response = self._make_request("GET", path, params=params) return NotificationCount(**response) - + def mark_read(self, notification_ids: Union[List[int], str]) -> Dict[str, Any]: """ Mark notifications as read. - + Args: notification_ids: List of IDs or 'all' - + Returns: Response status """ path = f"{self._path}/mark_read" - - if notification_ids == 'all': - params = {'notification_id': 'all'} - return self._make_request('PUT', path, params=params) + + if notification_ids == "all": + params = {"notification_id": "all"} + return self._make_request("PUT", path, params=params) else: - return self._make_request('PUT', path, json=notification_ids) - + return self._make_request("PUT", path, json=notification_ids) + def mark_unread(self, notification_ids: Union[List[int], str]) -> Dict[str, Any]: """ Mark notifications as unread. - + Args: notification_ids: List of IDs or 'all' - + Returns: Response status """ path = f"{self._path}/mark_unread" - - if notification_ids == 'all': - params = {'notification_id': 'all'} - return self._make_request('PUT', path, params=params) + + if notification_ids == "all": + params = {"notification_id": "all"} + return self._make_request("PUT", path, params=params) else: - return self._make_request('PUT', path, json=notification_ids) - + return self._make_request("PUT", path, json=notification_ids) + # Notification Types def get_types(self, status: Optional[str] = None) -> List[NotificationType]: """ Get all notification types. - + Args: status: Filter by status (ACTIVE, PAUSE) - + Returns: List of notification types """ path = "/notification_types" - params = {'status': status} if status else {} - response = self._make_request('GET', path, params=params) + params = {"status": status} if status else {} + response = self._make_request("GET", path, params=params) return [NotificationType(**item) for item in response] - + def get_type(self, event_type: str, resource_type: str) -> NotificationType: """ Get specific notification type. - + Args: event_type: Event type resource_type: Resource type - + Returns: Notification type """ path = "/notification_types/list" - params = { - 'event_type': event_type, - 'resource_type': resource_type - } - response = self._make_request('GET', path, params=params) + params = {"event_type": event_type, "resource_type": resource_type} + response = self._make_request("GET", path, params=params) return NotificationType(**response) - + # Channel Settings def list_channel_settings(self) -> List[NotificationChannelSetting]: """ List notification channel settings. - + Returns: List of channel settings """ path = "/notification_channel_settings" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return [NotificationChannelSetting(**item) for item in response] - - def create_channel_setting(self, data: NotificationChannelSettingCreate) -> NotificationChannelSetting: + + def create_channel_setting( + self, data: NotificationChannelSettingCreate + ) -> NotificationChannelSetting: """ Create notification channel setting. - + Args: data: Channel setting creation data - + Returns: Created channel setting """ path = "/notification_channel_settings" - response = self._make_request('POST', path, json=data.to_dict()) + response = self._make_request("POST", path, json=data.to_dict()) return NotificationChannelSetting(**response) - + def get_channel_setting(self, setting_id: int) -> NotificationChannelSetting: """ Get notification channel setting. - + Args: setting_id: Channel setting ID - + Returns: Channel setting """ path = f"/notification_channel_settings/{setting_id}" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return NotificationChannelSetting(**response) - - def update_channel_setting(self, - setting_id: int, - data: NotificationChannelSettingUpdate) -> NotificationChannelSetting: + + def update_channel_setting( + self, setting_id: int, data: NotificationChannelSettingUpdate + ) -> NotificationChannelSetting: """ Update notification channel setting. - + Args: setting_id: Channel setting ID data: Updated channel setting data - + Returns: Updated channel setting """ path = f"/notification_channel_settings/{setting_id}" - response = self._make_request('PUT', path, json=data.to_dict()) + response = self._make_request("PUT", path, json=data.to_dict()) return NotificationChannelSetting(**response) - + def delete_channel_setting(self, setting_id: int) -> Dict[str, Any]: """ Delete notification channel setting. - + Args: setting_id: Channel setting ID - + Returns: Response status """ path = f"/notification_channel_settings/{setting_id}" - return self._make_request('DELETE', path) - + return self._make_request("DELETE", path) + # Notification Settings - def list_settings(self, - event_type: Optional[str] = None, - resource_type: Optional[str] = None, - status: Optional[str] = None) -> List[NotificationSetting]: + def list_settings( + self, + event_type: Optional[str] = None, + resource_type: Optional[str] = None, + status: Optional[str] = None, + ) -> List[NotificationSetting]: """ List notification settings. - + Args: event_type: Filter by event type resource_type: Filter by resource type status: Filter by status - + Returns: List of notification settings """ path = "/notification_settings" params = {} if event_type: - params['event_type'] = event_type + params["event_type"] = event_type if resource_type: - params['resource_type'] = resource_type + params["resource_type"] = resource_type if status: - params['status'] = status - - response = self._make_request('GET', path, params=params) + params["status"] = status + + response = self._make_request("GET", path, params=params) return [NotificationSetting(**item) for item in response] - + def create_setting(self, data: NotificationSettingCreate) -> NotificationSetting: """ Create notification setting. - + Args: data: Notification setting creation data - + Returns: Created setting """ path = "/notification_settings" - response = self._make_request('POST', path, json=data.to_dict()) + response = self._make_request("POST", path, json=data.to_dict()) return NotificationSetting(**response) - + def get_setting(self, setting_id: int) -> NotificationSetting: """ Get notification setting. - + Args: setting_id: Setting ID - + Returns: Notification setting """ path = f"/notification_settings/{setting_id}" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return NotificationSetting(**response) - - def update_setting(self, - setting_id: int, - data: NotificationSettingUpdate) -> NotificationSetting: + + def update_setting( + self, setting_id: int, data: NotificationSettingUpdate + ) -> NotificationSetting: """ Update notification setting. - + Args: setting_id: Setting ID data: Updated notification setting data - + Returns: Updated setting """ path = f"/notification_settings/{setting_id}" - response = self._make_request('PUT', path, json=data.to_dict()) + response = self._make_request("PUT", path, json=data.to_dict()) return NotificationSetting(**response) - + def delete_setting(self, setting_id: int) -> Dict[str, Any]: """ Delete notification setting. - + Args: setting_id: Setting ID - + Returns: Response status """ path = f"/notification_settings/{setting_id}" - return self._make_request('DELETE', path) - - def get_settings_by_type(self, - notification_type_id: int, - expand: bool = False) -> List[NotificationSetting]: + return self._make_request("DELETE", path) + + def get_settings_by_type( + self, notification_type_id: int, expand: bool = False + ) -> List[NotificationSetting]: """ Get notification settings for a type. - + Args: notification_type_id: Notification type ID expand: Include expanded information - + Returns: List of settings """ path = f"/notification_settings/notification_types/{notification_type_id}" - params = {'expand': expand} if expand else {} - response = self._make_request('GET', path, params=params) + params = {"expand": expand} if expand else {} + response = self._make_request("GET", path, params=params) return [NotificationSetting(**item) for item in response] - - def get_resource_settings(self, - resource_type: str, - resource_id: int, - expand: bool = False, - filter_overridden: bool = False, - notification_type_id: Optional[int] = None) -> List[NotificationSetting]: + + def get_resource_settings( + self, + resource_type: str, + resource_id: int, + expand: bool = False, + filter_overridden: bool = False, + notification_type_id: Optional[int] = None, + ) -> List[NotificationSetting]: """ Get notification settings for a resource. - + Args: resource_type: Resource type resource_id: Resource ID expand: Include expanded information filter_overridden: Filter overridden settings notification_type_id: Filter by type ID - + Returns: List of settings """ path = f"/notification_settings/{resource_type}/{resource_id}" params = {} if expand: - params['expand'] = expand + params["expand"] = expand if filter_overridden: - params['filter_overridden_settings'] = filter_overridden + params["filter_overridden_settings"] = filter_overridden if notification_type_id: - params['notification_type_id'] = notification_type_id - - response = self._make_request('GET', path, params=params) + params["notification_type_id"] = notification_type_id + + response = self._make_request("GET", path, params=params) return [NotificationSetting(**item) for item in response] diff --git a/nexla_sdk/resources/org_auth_configs.py b/nexla_sdk/resources/org_auth_configs.py index b92a708..31fa650 100644 --- a/nexla_sdk/resources/org_auth_configs.py +++ b/nexla_sdk/resources/org_auth_configs.py @@ -1,7 +1,8 @@ -from typing import List, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.org_auth_configs.responses import AuthConfig +from typing import Any, Dict, List + from nexla_sdk.models.org_auth_configs.requests import AuthConfigPayload +from nexla_sdk.models.org_auth_configs.responses import AuthConfig +from nexla_sdk.resources.base_resource import BaseResource class OrgAuthConfigsResource(BaseResource): @@ -14,31 +15,33 @@ def __init__(self, client): def list(self) -> List[AuthConfig]: """List authentication configurations for the current organization.""" - response = self._make_request('GET', self._path) + response = self._make_request("GET", self._path) return self._parse_response(response) def list_all(self) -> List[AuthConfig]: """List all authentication configurations (admin only).""" - response = self._make_request('GET', f"{self._path}/all") + response = self._make_request("GET", f"{self._path}/all") return self._parse_response(response) def get(self, auth_config_id: int) -> AuthConfig: """Get a specific authentication configuration by ID.""" - response = self._make_request('GET', f"{self._path}/{auth_config_id}") + response = self._make_request("GET", f"{self._path}/{auth_config_id}") return self._parse_response(response) def create(self, payload: AuthConfigPayload) -> AuthConfig: """Create a new authentication configuration.""" data = self._serialize_data(payload) - response = self._make_request('POST', self._path, json=data) + response = self._make_request("POST", self._path, json=data) return self._parse_response(response) def update(self, auth_config_id: int, payload: AuthConfigPayload) -> AuthConfig: """Update an existing authentication configuration.""" data = self._serialize_data(payload) - response = self._make_request('PUT', f"{self._path}/{auth_config_id}", json=data) + response = self._make_request( + "PUT", f"{self._path}/{auth_config_id}", json=data + ) return self._parse_response(response) def delete(self, auth_config_id: int) -> Dict[str, Any]: """Delete an authentication configuration by ID.""" - return self._make_request('DELETE', f"{self._path}/{auth_config_id}") + return self._make_request("DELETE", f"{self._path}/{auth_config_id}") diff --git a/nexla_sdk/resources/organizations.py b/nexla_sdk/resources/organizations.py index ae9991f..6ea79cc 100644 --- a/nexla_sdk/resources/organizations.py +++ b/nexla_sdk/resources/organizations.py @@ -1,15 +1,21 @@ -from typing import List, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource +from typing import Any, Dict, List + from nexla_sdk.models.common import LogEntry -from nexla_sdk.models.organizations.responses import Organization, OrgMember, AccountSummary, CustodianUser +from nexla_sdk.models.organizations.custodians import OrgCustodiansPayload from nexla_sdk.models.organizations.requests import ( OrganizationCreate, OrganizationUpdate, - OrgMemberList, + OrgMemberActivateDeactivateRequest, OrgMemberDelete, - OrgMemberActivateDeactivateRequest + OrgMemberList, ) -from nexla_sdk.models.organizations.custodians import OrgCustodiansPayload +from nexla_sdk.models.organizations.responses import ( + AccountSummary, + CustodianUser, + Organization, + OrgMember, +) +from nexla_sdk.resources.base_resource import BaseResource class OrganizationsResource(BaseResource): @@ -23,16 +29,16 @@ def __init__(self, client): def list(self, **kwargs) -> List[Organization]: """ List organizations with optional filters. - + Args: page: Page number (via kwargs) per_page: Items per page (via kwargs) access_role: Filter by access role (via kwargs) **kwargs: Additional query parameters - + Returns: List of organizations - + Examples: client.organizations.list(page=1, per_page=25) """ @@ -41,11 +47,11 @@ def list(self, **kwargs) -> List[Organization]: def get(self, org_id: int, expand: bool = False) -> Organization: """ Get single organization by ID. - + Args: org_id: Organization ID expand: Include expanded references - + Returns: Organization instance """ @@ -54,10 +60,10 @@ def get(self, org_id: int, expand: bool = False) -> Organization: def create(self, data: OrganizationCreate) -> Organization: """ Create a new organization. Note: This is an admin-only operation. - + Args: data: Organization creation data - + Returns: Created organization """ @@ -66,11 +72,11 @@ def create(self, data: OrganizationCreate) -> Organization: def update(self, org_id: int, data: OrganizationUpdate) -> Organization: """ Update organization. - + Args: org_id: Organization ID data: Updated organization data - + Returns: Updated organization """ @@ -79,10 +85,10 @@ def update(self, org_id: int, data: OrganizationUpdate) -> Organization: def delete(self, org_id: int) -> Dict[str, Any]: """ Delete organization. - + Args: org_id: Organization ID - + Returns: Response with status """ @@ -91,212 +97,225 @@ def delete(self, org_id: int) -> Dict[str, Any]: def get_members(self, org_id: int) -> List[OrgMember]: """ Get all members in organization. - + Args: org_id: Organization ID - + Returns: List of organization members """ path = f"{self._path}/{org_id}/members" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return [OrgMember(**member) for member in response] def update_members(self, org_id: int, members: OrgMemberList) -> List[OrgMember]: """ Add or update members in organization. - + Args: org_id: Organization ID members: Members to add/update - + Returns: Updated member list """ path = f"{self._path}/{org_id}/members" - response = self._make_request('PUT', path, json=members.to_dict()) + response = self._make_request("PUT", path, json=members.to_dict()) return [OrgMember(**member) for member in response] def replace_members(self, org_id: int, members: OrgMemberList) -> List[OrgMember]: """ Replace all members in organization. - + Args: org_id: Organization ID members: New member list - + Returns: New member list """ path = f"{self._path}/{org_id}/members" - response = self._make_request('POST', path, json=members.to_dict()) + response = self._make_request("POST", path, json=members.to_dict()) return [OrgMember(**member) for member in response] def delete_members(self, org_id: int, members: OrgMemberDelete) -> Dict[str, Any]: """ Remove members from organization. - + Args: org_id: Organization ID members: Members to remove - + Returns: Response status """ path = f"{self._path}/{org_id}/members" - return self._make_request('DELETE', path, json=members.to_dict()) + return self._make_request("DELETE", path, json=members.to_dict()) - def deactivate_members(self, org_id: int, members: OrgMemberActivateDeactivateRequest) -> List[OrgMember]: + def deactivate_members( + self, org_id: int, members: OrgMemberActivateDeactivateRequest + ) -> List[OrgMember]: """ Deactivate members in an organization. - + Args: org_id: Organization ID members: Members to deactivate - + Returns: Updated list of members """ path = f"{self._path}/{org_id}/members/deactivate" - response = self._make_request('PUT', path, json=members.to_dict()) + response = self._make_request("PUT", path, json=members.to_dict()) return [OrgMember(**member) for member in response] - def activate_members(self, org_id: int, members: OrgMemberActivateDeactivateRequest) -> List[OrgMember]: + def activate_members( + self, org_id: int, members: OrgMemberActivateDeactivateRequest + ) -> List[OrgMember]: """ Activate members in an organization. - + Args: org_id: Organization ID members: Members to activate - + Returns: Updated list of members """ path = f"{self._path}/{org_id}/members/activate" - response = self._make_request('PUT', path, json=members.to_dict()) + response = self._make_request("PUT", path, json=members.to_dict()) return [OrgMember(**member) for member in response] def get_account_summary(self, org_id: int) -> AccountSummary: """ Get account summary statistics for an organization. - + Args: org_id: Organization ID - + Returns: Account summary """ path = f"{self._path}/{org_id}/account_summary" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return AccountSummary.model_validate(response) def get_current_account_summary(self) -> AccountSummary: """ Get account summary for the current organization based on auth token. - + Returns: Account summary """ path = f"{self._path}/account_summary" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return AccountSummary.model_validate(response) - def get_org_flow_account_metrics(self, org_id: int, from_date: str, to_date: str = None) -> Dict[str, Any]: + def get_org_flow_account_metrics( + self, org_id: int, from_date: str, to_date: str = None + ) -> Dict[str, Any]: """Get total account metrics for an organization (flows).""" path = f"{self._path}/{org_id}/flows/account_metrics" - params = {'from': from_date} + params = {"from": from_date} if to_date: - params['to'] = to_date - return self._make_request('GET', path, params=params) + params["to"] = to_date + return self._make_request("GET", path, params=params) def get_audit_log(self, org_id: int, **params) -> List[LogEntry]: """ Get audit log for an organization. - + Args: org_id: Organization ID **params: Additional query parameters (e.g., page, per_page) - + Returns: List of audit log entries """ path = f"{self._path}/{org_id}/audit_log" - response = self._make_request('GET', path, params=params) + response = self._make_request("GET", path, params=params) return [LogEntry.model_validate(item) for item in response] - def get_resource_audit_log(self, org_id: int, resource_type: str, **params) -> List[LogEntry]: + def get_resource_audit_log( + self, org_id: int, resource_type: str, **params + ) -> List[LogEntry]: """ Get audit log for a specific resource type within an organization. - + Args: org_id: Organization ID resource_type: The type of resource (e.g., 'data_source', 'data_sink') **params: Additional query parameters - + Returns: List of audit log entries """ path = f"{self._path}/{org_id}/{resource_type}/audit_log" - response = self._make_request('GET', path, params=params) + response = self._make_request("GET", path, params=params) return [LogEntry.model_validate(item) for item in response] - + def get_auth_settings(self, org_id: int) -> List[Dict[str, Any]]: """ Get authentication settings for organization. - + Args: org_id: Organization ID - + Returns: List of auth settings """ path = f"{self._path}/{org_id}/auth_settings" - return self._make_request('GET', path) + return self._make_request("GET", path) - def update_auth_setting(self, - org_id: int, - auth_setting_id: int, - enabled: bool) -> Dict[str, Any]: + def update_auth_setting( + self, org_id: int, auth_setting_id: int, enabled: bool + ) -> Dict[str, Any]: """ Enable/disable authentication configuration. - + Args: org_id: Organization ID auth_setting_id: Auth setting ID enabled: Whether to enable - + Returns: Updated auth setting """ path = f"{self._path}/{org_id}/auth_settings/{auth_setting_id}" - data = {'enabled': enabled} - return self._make_request('PUT', path, json=data) + data = {"enabled": enabled} + return self._make_request("PUT", path, json=data) # Org custodians def get_custodians(self, org_id: int) -> List[CustodianUser]: path = f"{self._path}/{org_id}/custodians" - response = self._make_request('GET', path) + response = self._make_request("GET", path) if isinstance(response, list): return [CustodianUser.model_validate(item) for item in response] return [] - def update_custodians(self, org_id: int, payload: OrgCustodiansPayload) -> List[CustodianUser]: + def update_custodians( + self, org_id: int, payload: OrgCustodiansPayload + ) -> List[CustodianUser]: path = f"{self._path}/{org_id}/custodians" data = self._serialize_data(payload) - response = self._make_request('PUT', path, json=data) + response = self._make_request("PUT", path, json=data) if isinstance(response, list): return [CustodianUser.model_validate(item) for item in response] return [] - def add_custodians(self, org_id: int, payload: OrgCustodiansPayload) -> List[CustodianUser]: + def add_custodians( + self, org_id: int, payload: OrgCustodiansPayload + ) -> List[CustodianUser]: path = f"{self._path}/{org_id}/custodians" data = self._serialize_data(payload) - response = self._make_request('POST', path, json=data) + response = self._make_request("POST", path, json=data) if isinstance(response, list): return [CustodianUser.model_validate(item) for item in response] return [] - def remove_custodians(self, org_id: int, payload: OrgCustodiansPayload) -> Dict[str, Any]: + def remove_custodians( + self, org_id: int, payload: OrgCustodiansPayload + ) -> Dict[str, Any]: path = f"{self._path}/{org_id}/custodians" data = self._serialize_data(payload) - return self._make_request('DELETE', path, json=data) + return self._make_request("DELETE", path, json=data) diff --git a/nexla_sdk/resources/projects.py b/nexla_sdk/resources/projects.py index c70179a..1961f66 100644 --- a/nexla_sdk/resources/projects.py +++ b/nexla_sdk/resources/projects.py @@ -1,91 +1,96 @@ -from typing import List, Optional, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.projects.responses import Project, ProjectDataFlow -from nexla_sdk.models.projects.requests import ProjectCreate, ProjectUpdate, ProjectFlowList +from typing import Any, Dict, List, Optional + from nexla_sdk.models.flows.responses import FlowResponse +from nexla_sdk.models.projects.requests import ( + ProjectCreate, + ProjectFlowList, + ProjectUpdate, +) +from nexla_sdk.models.projects.responses import Project, ProjectDataFlow +from nexla_sdk.resources.base_resource import BaseResource class ProjectsResource(BaseResource): """Resource for managing projects.""" - + def __init__(self, client): super().__init__(client) self._path = "/projects" self._model_class = Project - + def list(self, expand: bool = False, **kwargs) -> List[Project]: """ List projects with optional filters. - + Args: expand: Include flows in the response page: Page number (via kwargs) per_page: Items per page (via kwargs) access_role: Filter by access role (via kwargs) **kwargs: Additional query parameters - + Returns: List of projects - + Examples: client.projects.list(page=1, per_page=10) client.projects.list(expand=True) """ if expand: - kwargs['expand'] = 'true' + kwargs["expand"] = "true" return super().list(**kwargs) - + def get(self, project_id: int, expand: bool = False) -> Project: """ Get single project by ID. - + Args: project_id: Project ID expand: Include expanded references - + Returns: Project instance - + Examples: client.projects.get(12) """ return super().get(project_id, expand) - + def create(self, data: ProjectCreate) -> Project: """ Create new project. - + Args: data: Project creation data - + Returns: Created project - + Examples: client.projects.create(ProjectCreate(name="My Project")) """ return super().create(data) - + def update(self, project_id: int, data: ProjectUpdate) -> Project: """ Update project. - + Args: project_id: Project ID data: Updated project data - + Returns: Updated project """ return super().update(project_id, data) - + def delete(self, project_id: int) -> Dict[str, Any]: """ Delete project. - + Args: project_id: Project ID - + Returns: Response with status """ @@ -94,71 +99,77 @@ def delete(self, project_id: int) -> Dict[str, Any]: def get_flows(self, project_id: int) -> FlowResponse: """ Get flows in project. - + Args: project_id: Project ID - + Returns: Flow response """ path = f"{self._path}/{project_id}/flows" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return FlowResponse(**response) - - def add_flows(self, project_id: int, flows: ProjectFlowList) -> List[ProjectDataFlow]: + + def add_flows( + self, project_id: int, flows: ProjectFlowList + ) -> List[ProjectDataFlow]: """ Add flows to project. - + Args: project_id: Project ID flows: Flows to add - + Returns: List of added project flows """ path = f"{self._path}/{project_id}/flows" payload = self._serialize_data(flows) - response = self._make_request('PUT', path, json=payload) + response = self._make_request("PUT", path, json=payload) # API returns a list of project data flows for add operation return [ProjectDataFlow.model_validate(item) for item in response] - - def replace_flows(self, project_id: int, flows: ProjectFlowList) -> List[ProjectDataFlow]: + + def replace_flows( + self, project_id: int, flows: ProjectFlowList + ) -> List[ProjectDataFlow]: """ Replace all flows in project. - + Args: project_id: Project ID flows: New flow list - + Returns: List of project flows after replacement """ path = f"{self._path}/{project_id}/flows" payload = self._serialize_data(flows) - response = self._make_request('POST', path, json=payload) + response = self._make_request("POST", path, json=payload) # API returns a list of project data flows for replace operation return [ProjectDataFlow.model_validate(item) for item in response] - - def remove_flows(self, - project_id: int, - flows: Optional[ProjectFlowList] = None) -> List[ProjectDataFlow]: + + def remove_flows( + self, project_id: int, flows: Optional[ProjectFlowList] = None + ) -> List[ProjectDataFlow]: """ Remove flows from project. - + Args: project_id: Project ID flows: Flows to remove (None = remove all) - + Returns: Remaining project flows """ path = f"{self._path}/{project_id}/flows" data = self._serialize_data(flows) if flows else None - response = self._make_request('DELETE', path, json=data) + response = self._make_request("DELETE", path, json=data) # API returns remaining flows list return [ProjectDataFlow.model_validate(item) for item in response] - def add_data_flows(self, project_id: int, flows: ProjectFlowList) -> List[ProjectDataFlow]: + def add_data_flows( + self, project_id: int, flows: ProjectFlowList + ) -> List[ProjectDataFlow]: """ Backward-compatible alias for adding flows to a project. @@ -166,7 +177,9 @@ def add_data_flows(self, project_id: int, flows: ProjectFlowList) -> List[Projec """ return self.add_flows(project_id, flows) - def replace_data_flows(self, project_id: int, flows: ProjectFlowList) -> List[ProjectDataFlow]: + def replace_data_flows( + self, project_id: int, flows: ProjectFlowList + ) -> List[ProjectDataFlow]: """ Backward-compatible alias for replacing all flows in a project. @@ -174,9 +187,9 @@ def replace_data_flows(self, project_id: int, flows: ProjectFlowList) -> List[Pr """ return self.replace_flows(project_id, flows) - def remove_data_flows(self, - project_id: int, - flows: Optional[ProjectFlowList] = None) -> List[ProjectDataFlow]: + def remove_data_flows( + self, project_id: int, flows: Optional[ProjectFlowList] = None + ) -> List[ProjectDataFlow]: """ Backward-compatible alias for removing flows from a project. @@ -184,7 +197,9 @@ def remove_data_flows(self, """ return self.remove_flows(project_id, flows) - def search_flows(self, project_id: int, filters: List[Dict[str, Any]]) -> FlowResponse: + def search_flows( + self, project_id: int, filters: List[Dict[str, Any]] + ) -> FlowResponse: """ Search flows in a project using filter criteria. @@ -197,5 +212,5 @@ def search_flows(self, project_id: int, filters: List[Dict[str, Any]]) -> FlowRe """ path = f"{self._path}/{project_id}/flows/search" payload = {"filters": filters} - response = self._make_request('POST', path, json=payload) + response = self._make_request("POST", path, json=payload) return FlowResponse(**response) diff --git a/nexla_sdk/resources/runtimes.py b/nexla_sdk/resources/runtimes.py index c4366e7..a68410f 100644 --- a/nexla_sdk/resources/runtimes.py +++ b/nexla_sdk/resources/runtimes.py @@ -1,7 +1,8 @@ -from typing import List, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.runtimes.responses import Runtime +from typing import Any, Dict, List + from nexla_sdk.models.runtimes.requests import RuntimeCreate, RuntimeUpdate +from nexla_sdk.models.runtimes.responses import Runtime +from nexla_sdk.resources.base_resource import BaseResource class RuntimesResource(BaseResource): @@ -14,41 +15,41 @@ def __init__(self, client): def list(self) -> List[Runtime]: """List custom runtimes.""" - response = self._make_request('GET', self._path) + response = self._make_request("GET", self._path) return self._parse_response(response) def create(self, data: RuntimeCreate) -> Runtime: """Create a new custom runtime.""" payload = self._serialize_data(data) - response = self._make_request('POST', self._path, json=payload) + response = self._make_request("POST", self._path, json=payload) return self._parse_response(response) def get(self, runtime_id: int) -> Runtime: """Get a custom runtime by ID.""" path = f"{self._path}/{runtime_id}" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return self._parse_response(response) def update(self, runtime_id: int, data: RuntimeUpdate) -> Runtime: """Update a custom runtime by ID.""" path = f"{self._path}/{runtime_id}" payload = self._serialize_data(data) - response = self._make_request('PUT', path, json=payload) + response = self._make_request("PUT", path, json=payload) return self._parse_response(response) def delete(self, runtime_id: int) -> Dict[str, Any]: """Delete a custom runtime by ID.""" path = f"{self._path}/{runtime_id}" - return self._make_request('DELETE', path) + return self._make_request("DELETE", path) def activate(self, runtime_id: int) -> Runtime: """Activate a custom runtime.""" path = f"{self._path}/{runtime_id}/activate" - response = self._make_request('PUT', path) + response = self._make_request("PUT", path) return self._parse_response(response) def pause(self, runtime_id: int) -> Runtime: """Pause a custom runtime.""" path = f"{self._path}/{runtime_id}/pause" - response = self._make_request('PUT', path) + response = self._make_request("PUT", path) return self._parse_response(response) diff --git a/nexla_sdk/resources/self_signup.py b/nexla_sdk/resources/self_signup.py index f2ff82f..b5fd778 100644 --- a/nexla_sdk/resources/self_signup.py +++ b/nexla_sdk/resources/self_signup.py @@ -1,6 +1,7 @@ -from typing import Dict, Any, List +from typing import Any, Dict, List + +from nexla_sdk.models.self_signup.responses import BlockedDomain, SelfSignupRequest from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.self_signup.responses import SelfSignupRequest, BlockedDomain class SelfSignupResource(BaseResource): @@ -13,31 +14,39 @@ def __init__(self, client): # Public signup def signup(self, payload: Dict[str, Any]) -> Dict[str, Any]: - return self._make_request('POST', "/signup", json=payload) + return self._make_request("POST", "/signup", json=payload) def verify_email(self, token: str) -> Dict[str, Any]: - return self._make_request('GET', "/signup/verify_email", params={'token': token}) + return self._make_request( + "GET", "/signup/verify_email", params={"token": token} + ) # Admin APIs def list_requests(self) -> List[SelfSignupRequest]: - response = self._make_request('GET', "/self_signup_requests") + response = self._make_request("GET", "/self_signup_requests") return [SelfSignupRequest.model_validate(item) for item in (response or [])] def approve_request(self, request_id: str) -> SelfSignupRequest: - response = self._make_request('PUT', f"/self_signup_requests/{request_id}/approve") + response = self._make_request( + "PUT", f"/self_signup_requests/{request_id}/approve" + ) return SelfSignupRequest.model_validate(response) def list_blocked_domains(self) -> List[BlockedDomain]: - response = self._make_request('GET', "/self_signup_blocked_domains") + response = self._make_request("GET", "/self_signup_blocked_domains") return [BlockedDomain.model_validate(item) for item in (response or [])] def add_blocked_domain(self, domain: str) -> BlockedDomain: - response = self._make_request('POST', "/self_signup_blocked_domains", json={'domain': domain}) + response = self._make_request( + "POST", "/self_signup_blocked_domains", json={"domain": domain} + ) return BlockedDomain.model_validate(response) def update_blocked_domain(self, domain_id: str, domain: str) -> BlockedDomain: - response = self._make_request('PUT', f"/self_signup_blocked_domains/{domain_id}", json={'domain': domain}) + response = self._make_request( + "PUT", f"/self_signup_blocked_domains/{domain_id}", json={"domain": domain} + ) return BlockedDomain.model_validate(response) def delete_blocked_domain(self, domain_id: str) -> Dict[str, Any]: - return self._make_request('DELETE', f"/self_signup_blocked_domains/{domain_id}") + return self._make_request("DELETE", f"/self_signup_blocked_domains/{domain_id}") diff --git a/nexla_sdk/resources/sources.py b/nexla_sdk/resources/sources.py index ccc394e..bf4f8ea 100644 --- a/nexla_sdk/resources/sources.py +++ b/nexla_sdk/resources/sources.py @@ -1,27 +1,32 @@ -from typing import List, Optional, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource +from typing import Any, Dict, List, Optional + +from nexla_sdk.models.sources.requests import ( + SourceCopyOptions, + SourceCreate, + SourceUpdate, +) from nexla_sdk.models.sources.responses import Source -from nexla_sdk.models.sources.requests import SourceCreate, SourceUpdate, SourceCopyOptions +from nexla_sdk.resources.base_resource import BaseResource class SourcesResource(BaseResource): """Resource for managing data sources.""" - + def __init__(self, client): super().__init__(client) self._path = "/data_sources" self._model_class = Source - + def list(self, **kwargs) -> List[Source]: """ List sources with optional filters. - + Args: page: Page number (via kwargs) per_page: Items per page (via kwargs) access_role: Filter by access role (via kwargs) **kwargs: Additional query parameters - + Returns: List of sources @@ -33,95 +38,97 @@ def list(self, **kwargs) -> List[Source]: client.sources.list(page=1, per_page=20, access_role="owner") """ return super().list(**kwargs) - + def get(self, source_id: int, expand: bool = False) -> Source: """ Get single source by ID. - + Args: source_id: Source ID expand: Include expanded references - + Returns: Source instance - + Examples: client.sources.get(123) """ return super().get(source_id, expand) - + def create(self, data: SourceCreate) -> Source: """ Create new source. - + Args: data: Source creation data - + Returns: Created source - + Examples: new_source = client.sources.create(SourceCreate(name="My Source", connector=...)) """ return super().create(data) - + def update(self, source_id: int, data: SourceUpdate) -> Source: """ Update source. - + Args: source_id: Source ID data: Updated source data - + Returns: Updated source """ return super().update(source_id, data) - + def delete(self, source_id: int) -> Dict[str, Any]: """ Delete source. - + Args: source_id: Source ID - + Returns: Response with status """ return super().delete(source_id) - + def activate(self, source_id: int) -> Source: """ Activate source. - + Args: source_id: Source ID - + Returns: Activated source """ return super().activate(source_id) - + def pause(self, source_id: int) -> Source: """ Pause source. - + Args: source_id: Source ID - + Returns: Paused source """ return super().pause(source_id) - - def copy(self, source_id: int, options: Optional[SourceCopyOptions] = None) -> Source: + + def copy( + self, source_id: int, options: Optional[SourceCopyOptions] = None + ) -> Source: """ Copy a source. - + Args: source_id: Source ID options: Copy options - + Returns: Copied source """ diff --git a/nexla_sdk/resources/teams.py b/nexla_sdk/resources/teams.py index 5691424..162828b 100644 --- a/nexla_sdk/resources/teams.py +++ b/nexla_sdk/resources/teams.py @@ -1,27 +1,28 @@ -from typing import List, Optional, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource +from typing import Any, Dict, List, Optional + +from nexla_sdk.models.teams.requests import TeamCreate, TeamMemberList, TeamUpdate from nexla_sdk.models.teams.responses import Team, TeamMember -from nexla_sdk.models.teams.requests import TeamCreate, TeamUpdate, TeamMemberList +from nexla_sdk.resources.base_resource import BaseResource class TeamsResource(BaseResource): """Resource for managing teams.""" - + def __init__(self, client): super().__init__(client) self._path = "/teams" self._model_class = Team - + def list(self, **kwargs) -> List[Team]: """ List teams with optional filters. - + Args: page: Page number (via kwargs) per_page: Items per page (via kwargs) access_role: Filter by access role (via kwargs) **kwargs: Additional query parameters - + Returns: List of teams @@ -29,58 +30,58 @@ def list(self, **kwargs) -> List[Team]: client.teams.list(page=2, per_page=50) """ return super().list(**kwargs) - + def get(self, team_id: int, expand: bool = False) -> Team: """ Get single team by ID. - + Args: team_id: Team ID expand: Include expanded references - + Returns: Team instance - + Examples: client.teams.get(101) """ return super().get(team_id, expand) - + def create(self, data: TeamCreate) -> Team: """ Create new team. - + Args: data: Team creation data - + Returns: Created team - + Examples: team = client.teams.create(TeamCreate(name="Data Ops")) """ return super().create(data) - + def update(self, team_id: int, data: TeamUpdate) -> Team: """ Update team. - + Args: team_id: Team ID data: Updated team data - + Returns: Updated team """ return super().update(team_id, data) - + def delete(self, team_id: int) -> Dict[str, Any]: """ Delete team. - + Args: team_id: Team ID - + Returns: Response with status """ @@ -89,61 +90,63 @@ def delete(self, team_id: int) -> Dict[str, Any]: def get_members(self, team_id: int) -> List[TeamMember]: """ Get team members. - + Args: team_id: Team ID - + Returns: List of team members """ path = f"{self._path}/{team_id}/members" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return [TeamMember(**member) for member in response] - + def add_members(self, team_id: int, members: TeamMemberList) -> List[TeamMember]: """ Add members to team. - + Args: team_id: Team ID members: Members to add - + Returns: Updated member list """ path = f"{self._path}/{team_id}/members" - response = self._make_request('PUT', path, json=members.to_dict()) + response = self._make_request("PUT", path, json=members.to_dict()) return [TeamMember(**member) for member in response] - - def replace_members(self, team_id: int, members: TeamMemberList) -> List[TeamMember]: + + def replace_members( + self, team_id: int, members: TeamMemberList + ) -> List[TeamMember]: """ Replace all team members. - + Args: team_id: Team ID members: New member list - + Returns: New member list """ path = f"{self._path}/{team_id}/members" - response = self._make_request('POST', path, json=members.to_dict()) + response = self._make_request("POST", path, json=members.to_dict()) return [TeamMember(**member) for member in response] - - def remove_members(self, - team_id: int, - members: Optional[TeamMemberList] = None) -> List[TeamMember]: + + def remove_members( + self, team_id: int, members: Optional[TeamMemberList] = None + ) -> List[TeamMember]: """ Remove members from team. - + Args: team_id: Team ID members: Members to remove (None = remove all) - + Returns: Remaining members """ path = f"{self._path}/{team_id}/members" data = members.to_dict() if members else None - response = self._make_request('DELETE', path, json=data) + response = self._make_request("DELETE", path, json=data) return [TeamMember(**member) for member in response] diff --git a/nexla_sdk/resources/transforms.py b/nexla_sdk/resources/transforms.py index 06a282e..c9b086b 100644 --- a/nexla_sdk/resources/transforms.py +++ b/nexla_sdk/resources/transforms.py @@ -1,7 +1,8 @@ -from typing import List, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.transforms.responses import Transform +from typing import Any, Dict, List + from nexla_sdk.models.transforms.requests import TransformCreate, TransformUpdate +from nexla_sdk.models.transforms.responses import Transform +from nexla_sdk.resources.base_resource import BaseResource class TransformsResource(BaseResource): @@ -53,5 +54,5 @@ def copy(self, transform_id: int) -> Transform: def list_public(self) -> List[Transform]: """List publicly shared transforms.""" path = f"{self._path}/public" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return self._parse_response(response) diff --git a/nexla_sdk/resources/users.py b/nexla_sdk/resources/users.py index 44bc45e..1bff4d8 100644 --- a/nexla_sdk/resources/users.py +++ b/nexla_sdk/resources/users.py @@ -1,99 +1,102 @@ -from typing import List, Optional, Dict, Any -from nexla_sdk.resources.base_resource import BaseResource -from nexla_sdk.models.users.responses import User, UserExpanded, UserSettings -from nexla_sdk.models.users.requests import UserCreate, UserUpdate +from typing import Any, Dict, List, Optional + from nexla_sdk.models.metrics.enums import UserMetricResourceType +from nexla_sdk.models.users.requests import UserCreate, UserUpdate +from nexla_sdk.models.users.responses import User, UserExpanded, UserSettings +from nexla_sdk.resources.base_resource import BaseResource class UsersResource(BaseResource): """Resource for managing users.""" - + def __init__(self, client): super().__init__(client) self._path = "/users" self._model_class = User - + def list(self, expand: bool = False, **kwargs) -> List[User]: """ List users with optional filters. - + Args: expand: Include expanded information page: Page number (via kwargs) per_page: Items per page (via kwargs) access_role: Filter by access role (via kwargs) **kwargs: Additional query parameters - + Returns: List of users - + Examples: client.users.list(page=1, per_page=50) client.users.list(expand=True) """ if expand: - response = self._make_request('GET', f"{self._path}?expand=1", params=kwargs) + response = self._make_request( + "GET", f"{self._path}?expand=1", params=kwargs + ) return [UserExpanded(**item) for item in response] - + return super().list(**kwargs) - + def get(self, user_id: int, expand: bool = False) -> User: """ Get user by ID. - + Args: user_id: User ID expand: Include expanded information - + Returns: User object - + Examples: client.users.get(42) client.users.get(42, expand=True) """ if expand: path = f"{self._path}/{user_id}?expand=1" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return UserExpanded(**response) - + return super().get(user_id, expand=False) - + def create(self, data: UserCreate) -> User: """ Create new user. - + Args: data: User creation data - + Returns: Created user - + Examples: client.users.create(UserCreate(email="user@example.com", name="Jane")) """ return super().create(data) - + def update(self, user_id: int, data: UserUpdate) -> User: """ Update user. - + Args: user_id: User ID data: Updated user data - + Returns: Updated user """ return super().update(user_id, data) - + def delete(self, user_id: int) -> Dict[str, Any]: """ Delete user. - + Args: user_id: User ID - + Returns: Response with status """ @@ -102,199 +105,194 @@ def delete(self, user_id: int) -> Dict[str, Any]: def get_settings(self) -> List[UserSettings]: """ Get current user's settings. - + Returns: List of user settings """ path = "/user_settings" - response = self._make_request('GET', path) + response = self._make_request("GET", path) return [UserSettings(**item) for item in response] def get_current(self) -> Dict[str, Any]: """Get info on current user (includes org memberships and current org info).""" path = "/users/current" - return self._make_request('GET', path) - + return self._make_request("GET", path) + def get_quarantine_settings(self, user_id: int) -> Dict[str, Any]: """ Get quarantine data export settings for user. - + Args: user_id: User ID - + Returns: Quarantine settings """ path = f"{self._path}/{user_id}/quarantine_settings" - return self._make_request('GET', path) - - def create_quarantine_settings(self, - user_id: int, - data_credentials_id: int, - config: Dict[str, Any]) -> Dict[str, Any]: + return self._make_request("GET", path) + + def create_quarantine_settings( + self, user_id: int, data_credentials_id: int, config: Dict[str, Any] + ) -> Dict[str, Any]: """ Create quarantine data export settings. - + Args: user_id: User ID data_credentials_id: Credential ID for export location config: Configuration including cron schedule and path - + Returns: Created settings """ path = f"{self._path}/{user_id}/quarantine_settings" - data = { - 'data_credentials_id': data_credentials_id, - 'config': config - } - return self._make_request('POST', path, json=data) - - def update_quarantine_settings(self, - user_id: int, - data: Dict[str, Any]) -> Dict[str, Any]: + data = {"data_credentials_id": data_credentials_id, "config": config} + return self._make_request("POST", path, json=data) + + def update_quarantine_settings( + self, user_id: int, data: Dict[str, Any] + ) -> Dict[str, Any]: """ Update quarantine data export settings. - + Args: user_id: User ID data: Updated settings - + Returns: Updated settings """ path = f"{self._path}/{user_id}/quarantine_settings" - return self._make_request('PUT', path, json=data) - + return self._make_request("PUT", path, json=data) + def delete_quarantine_settings(self, user_id: int) -> Dict[str, Any]: """ Delete quarantine data export settings. - + Args: user_id: User ID - + Returns: Response status """ path = f"{self._path}/{user_id}/quarantine_settings" - return self._make_request('DELETE', path) + return self._make_request("DELETE", path) def get_audit_log(self, user_id: int, **params) -> List[Dict[str, Any]]: """Get audit log for a user.""" path = f"{self._path}/{user_id}/audit_log" - response = self._make_request('GET', path, params=params) + response = self._make_request("GET", path, params=params) if isinstance(response, list): return response return [] - + def get_transferable_resources(self, user_id: int, org_id: int) -> Dict[str, Any]: """ Get a list of resources owned by a user that can be transferred. - + Args: user_id: The ID of the user whose resources are being checked org_id: The ID of the organization context - + Returns: A dictionary of transferable resources by type """ path = f"{self._path}/{user_id}/transferable" - params = {'org_id': org_id} - return self._make_request('GET', path, params=params) - - def transfer_resources(self, user_id: int, org_id: int, delegate_owner_id: int) -> Dict[str, Any]: + params = {"org_id": org_id} + return self._make_request("GET", path, params=params) + + def transfer_resources( + self, user_id: int, org_id: int, delegate_owner_id: int + ) -> Dict[str, Any]: """ Transfer a user's resources to another user within an organization. - + Args: user_id: The ID of the user whose resources are being transferred org_id: The ID of the organization context delegate_owner_id: The ID of the user to whom resources will be transferred - + Returns: A dictionary confirming the transfer details """ path = f"{self._path}/{user_id}/transfer" - data = { - 'org_id': org_id, - 'delegate_owner_id': delegate_owner_id - } - return self._make_request('PUT', path, json=data) - - def get_account_metrics(self, - user_id: int, - from_date: str, - to_date: Optional[str] = None, - org_id: Optional[int] = None) -> Dict[str, Any]: + data = {"org_id": org_id, "delegate_owner_id": delegate_owner_id} + return self._make_request("PUT", path, json=data) + + def get_account_metrics( + self, + user_id: int, + from_date: str, + to_date: Optional[str] = None, + org_id: Optional[int] = None, + ) -> Dict[str, Any]: """ Get total account metrics for user. - + Args: user_id: User ID from_date: Start date (YYYY-MM-DD) to_date: End date (optional) org_id: Organization ID (for users in multiple orgs) - + Returns: Account metrics """ path = f"{self._path}/{user_id}/flows/account_metrics" - params = {'from': from_date} + params = {"from": from_date} if to_date: - params['to'] = to_date + params["to"] = to_date if org_id: - params['org_id'] = org_id - - return self._make_request('GET', path, params=params) - - def get_dashboard_metrics(self, - user_id: int, - access_role: Optional[str] = None) -> Dict[str, Any]: + params["org_id"] = org_id + + return self._make_request("GET", path, params=params) + + def get_dashboard_metrics( + self, user_id: int, access_role: Optional[str] = None + ) -> Dict[str, Any]: """ Get 24 hour flow stats for user. - + Args: user_id: User ID access_role: Filter by access role - + Returns: Dashboard metrics """ path = f"{self._path}/{user_id}/flows/dashboard" params = {} if access_role: - params['access_role'] = access_role - - return self._make_request('GET', path, params=params) - - def get_daily_metrics(self, - user_id: int, - resource_type: UserMetricResourceType, - from_date: str, - to_date: Optional[str] = None, - org_id: Optional[int] = None) -> Dict[str, Any]: + params["access_role"] = access_role + + return self._make_request("GET", path, params=params) + + def get_daily_metrics( + self, + user_id: int, + resource_type: UserMetricResourceType, + from_date: str, + to_date: Optional[str] = None, + org_id: Optional[int] = None, + ) -> Dict[str, Any]: """ Get daily data processing metrics for a user. - + Args: user_id: User ID resource_type: Type of resource (SOURCE, SINK) from_date: Start date (YYYY-MM-DD) to_date: End date (optional) org_id: Organization ID (optional) - + Returns: Daily metrics data """ path = f"{self._path}/{user_id}/metrics" - params = { - 'resource_type': resource_type, - 'from': from_date, - 'aggregate': 1 - } + params = {"resource_type": resource_type, "from": from_date, "aggregate": 1} if to_date: - params['to'] = to_date + params["to"] = to_date if org_id: - params['org_id'] = org_id - - return self._make_request('GET', path, params=params) + params["org_id"] = org_id + + return self._make_request("GET", path, params=params) diff --git a/nexla_sdk/resources/webhooks.py b/nexla_sdk/resources/webhooks.py index 8aeaaff..bc1be25 100644 --- a/nexla_sdk/resources/webhooks.py +++ b/nexla_sdk/resources/webhooks.py @@ -1,9 +1,11 @@ """Resource for sending data to Nexla webhooks.""" -from typing import Dict, Any, List, Optional + import base64 +from typing import Any, Dict, List, Optional + +from nexla_sdk.exceptions import NexlaError from nexla_sdk.models.webhooks.requests import WebhookSendOptions from nexla_sdk.models.webhooks.responses import WebhookResponse -from nexla_sdk.exceptions import NexlaError class WebhooksResource: @@ -53,6 +55,7 @@ def _get_http_client(self): return self._http_client # Import here to avoid circular imports from nexla_sdk.http_client import RequestsHttpClient + self._http_client = RequestsHttpClient() return self._http_client @@ -62,7 +65,7 @@ def _make_request( url: str, json: Any = None, options: Optional[WebhookSendOptions] = None, - auth_method: str = "query" + auth_method: str = "query", ) -> Dict[str, Any]: """Make authenticated request to webhook. @@ -79,9 +82,7 @@ def _make_request( Raises: NexlaError: If request fails """ - headers = { - "Content-Type": "application/json" - } + headers = {"Content-Type": "application/json"} params = {} @@ -111,7 +112,7 @@ def _make_request( url=url, headers=headers, params=params if params else None, - json=json + json=json, ) return response except Exception as e: @@ -119,7 +120,7 @@ def _make_request( message=f"Webhook request failed: {e}", operation="webhook_send", context={"url": url, "method": method}, - original_error=e + original_error=e, ) from e def send_one_record( @@ -127,7 +128,7 @@ def send_one_record( webhook_url: str, record: Dict[str, Any], options: Optional[WebhookSendOptions] = None, - auth_method: str = "query" + auth_method: str = "query", ) -> WebhookResponse: """Send a single record to a webhook. @@ -165,7 +166,7 @@ def send_one_record( url=webhook_url, json=record, options=options, - auth_method=auth_method + auth_method=auth_method, ) return WebhookResponse.model_validate(response) @@ -174,7 +175,7 @@ def send_many_records( webhook_url: str, records: List[Dict[str, Any]], options: Optional[WebhookSendOptions] = None, - auth_method: str = "query" + auth_method: str = "query", ) -> WebhookResponse: """Send multiple records to a webhook. @@ -211,6 +212,6 @@ def send_many_records( url=webhook_url, json=records, options=options, - auth_method=auth_method + auth_method=auth_method, ) return WebhookResponse.model_validate(response) diff --git a/nexla_sdk/telemetry.py b/nexla_sdk/telemetry.py index 1efd0ad..87e3805 100644 --- a/nexla_sdk/telemetry.py +++ b/nexla_sdk/telemetry.py @@ -5,13 +5,15 @@ without any OpenTelemetry packages installed. If tracing is disabled or OpenTelemetry isn't available, a no-op tracer is provided. """ -from typing import Optional, Any + import os import threading +from typing import Any, Optional # Guard against missing OpenTelemetry installation try: # pragma: no cover - optional dependency from opentelemetry import trace # type: ignore + _opentelemetry_available = True except Exception: # pragma: no cover trace = None # type: ignore @@ -39,7 +41,9 @@ def is_recording(self) -> bool: class _NoOpTracer: - def start_as_current_span(self, *args: Any, **kwargs: Any) -> _NoOpSpan: # noqa: D401 + def start_as_current_span( + self, *args: Any, **kwargs: Any + ) -> _NoOpSpan: # noqa: D401 return _NoOpSpan() def start_span(self, *args: Any, **kwargs: Any) -> _NoOpSpan: # noqa: D401 @@ -67,6 +71,7 @@ def get_tracer(trace_enabled: bool): # Using a stable instrumentation name for the SDK tracer try: from importlib.metadata import version # Python 3.8+ + pkg_version = version("nexla-sdk") except Exception: # pragma: no cover pkg_version = "unknown" @@ -90,7 +95,8 @@ def is_tracing_configured() -> bool: provider = trace.get_tracer_provider() # type: ignore[union-attr] # If provider is not the default NoOpTracerProvider, assume configured if getattr(trace, "NoOpTracerProvider", None) and not isinstance( - provider, trace.NoOpTracerProvider # type: ignore[attr-defined] + provider, + trace.NoOpTracerProvider, # type: ignore[attr-defined] ): return True except Exception: # pragma: no cover diff --git a/nexla_sdk/utils/pagination.py b/nexla_sdk/utils/pagination.py index cac9961..4c0b128 100644 --- a/nexla_sdk/utils/pagination.py +++ b/nexla_sdk/utils/pagination.py @@ -1,23 +1,25 @@ -from typing import TypeVar, Generic, List, Optional, Dict, Any, Iterator +from typing import Any, Dict, Generic, Iterator, List, Optional, TypeVar + from nexla_sdk.models.base import BaseModel -T = TypeVar('T') +T = TypeVar("T") class PageInfo(BaseModel): """Information about the current page of results.""" + current_page: int total_pages: Optional[int] = None total_count: Optional[int] = None page_size: int = 20 - + @property def has_next(self) -> bool: """Check if there's a next page.""" if self.total_pages is not None: return self.current_page < self.total_pages return True # Assume there might be more if we don't know total - + @property def has_previous(self) -> bool: """Check if there's a previous page.""" @@ -26,35 +28,34 @@ def has_previous(self) -> bool: class Page(Generic[T]): """A single page of results.""" - - def __init__(self, - items: List[T], - page_info: PageInfo, - raw_response: Optional[Dict[str, Any]] = None): + + def __init__( + self, + items: List[T], + page_info: PageInfo, + raw_response: Optional[Dict[str, Any]] = None, + ): self.items = items self.page_info = page_info self.raw_response = raw_response - + def __iter__(self) -> Iterator[T]: return iter(self.items) - + def __len__(self) -> int: return len(self.items) - + def __getitem__(self, index: int) -> T: return self.items[index] class Paginator(Generic[T]): """Paginator for iterating through pages of results.""" - - def __init__(self, - fetch_func, - page_size: int = 20, - **kwargs): + + def __init__(self, fetch_func, page_size: int = 20, **kwargs): """ Initialize paginator. - + Args: fetch_func: Function to fetch a page of results page_size: Number of items per page @@ -64,33 +65,30 @@ def __init__(self, self.page_size = page_size self.kwargs = kwargs self.current_page = 1 - + def get_page(self, page_number: int) -> Page[T]: """Get a specific page of results.""" response = self.fetch_func( - page=page_number, - per_page=self.page_size, - **self.kwargs + page=page_number, per_page=self.page_size, **self.kwargs ) # Extract page info from response if available - page_info = PageInfo( - current_page=page_number, - page_size=self.page_size - ) + page_info = PageInfo(current_page=page_number, page_size=self.page_size) # Try to extract total pages/count from response metadata items: List[T] if isinstance(response, dict): - if 'meta' in response: - meta = response['meta'] or {} + if "meta" in response: + meta = response["meta"] or {} # Support both snake_case and camelCase keys - page_info.total_pages = meta.get('pageCount') or meta.get('total_pages') - page_info.total_count = meta.get('totalCount') or meta.get('total_count') - current = meta.get('currentPage') or meta.get('current_page') + page_info.total_pages = meta.get("pageCount") or meta.get("total_pages") + page_info.total_count = meta.get("totalCount") or meta.get( + "total_count" + ) + current = meta.get("currentPage") or meta.get("current_page") if isinstance(current, int): page_info.current_page = current - items = response.get('data', []) + items = response.get("data", []) else: # Response is not paginated; assume it's a list-like payload items = response # type: ignore[assignment] @@ -98,7 +96,7 @@ def get_page(self, page_number: int) -> Page[T]: items = response # type: ignore[assignment] return Page(items=items, page_info=page_info, raw_response=response) - + def __iter__(self) -> Iterator[T]: """Iterate through all items across all pages.""" self.current_page = 1 @@ -114,7 +112,7 @@ def __iter__(self) -> Iterator[T]: break self.current_page += 1 - + def iter_pages(self) -> Iterator[Page[T]]: """Iterate through pages instead of individual items.""" page_num = 1 diff --git a/skills/nexla/scripts/batch_operations.py b/skills/nexla/scripts/batch_operations.py index 98f1b7d..8bb856d 100644 --- a/skills/nexla/scripts/batch_operations.py +++ b/skills/nexla/scripts/batch_operations.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """Batch operations for Nexla resources.""" -import sys -import json import argparse -from typing import Dict, List, Any +import json +import sys +from typing import Any, Dict, List try: from nexla_sdk import NexlaClient diff --git a/skills/nexla/scripts/circuit_breaker.py b/skills/nexla/scripts/circuit_breaker.py index 2962a25..fe6467f 100644 --- a/skills/nexla/scripts/circuit_breaker.py +++ b/skills/nexla/scripts/circuit_breaker.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """Circuit breaker pattern implementation for Nexla operations.""" -import time import functools +import time from enum import Enum -from typing import Callable, TypeVar, Optional +from typing import Callable, Optional, TypeVar T = TypeVar("T") diff --git a/skills/nexla/scripts/deploy_flow.py b/skills/nexla/scripts/deploy_flow.py index 6db3270..6ef0dd1 100644 --- a/skills/nexla/scripts/deploy_flow.py +++ b/skills/nexla/scripts/deploy_flow.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 """Deploy Nexla flow with validation and rollback.""" -import sys +import argparse import json +import sys import time -import argparse -from typing import Dict, Any, List, Tuple +from typing import Any, Dict, List, Tuple try: - from nexla_sdk import NexlaClient, CredentialError, FlowError + from nexla_sdk import CredentialError, FlowError, NexlaClient except ImportError: print("Error: nexla_sdk not installed. Run: pip install nexla-sdk", file=sys.stderr) sys.exit(1) diff --git a/skills/nexla/scripts/get_resource_logs.py b/skills/nexla/scripts/get_resource_logs.py index 3a90eca..f485c74 100644 --- a/skills/nexla/scripts/get_resource_logs.py +++ b/skills/nexla/scripts/get_resource_logs.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """Fetch flow logs for a Nexla resource run.""" -import sys -import json import argparse +import json +import sys from typing import Any, Dict, List, Optional try: diff --git a/skills/nexla/scripts/health_check.py b/skills/nexla/scripts/health_check.py index 228dcf0..80ede2e 100644 --- a/skills/nexla/scripts/health_check.py +++ b/skills/nexla/scripts/health_check.py @@ -1,11 +1,11 @@ #!/usr/bin/env python3 """Health check script for Nexla flows with alerting.""" -import sys -import json import argparse +import json +import sys from datetime import datetime -from typing import Dict, List, Any +from typing import Any, Dict, List try: from nexla_sdk import NexlaClient @@ -15,7 +15,10 @@ def check_flow_health( - client: NexlaClient, resource_type: str, resource_id: int, error_threshold: float = 0.2 + client: NexlaClient, + resource_type: str, + resource_id: int, + error_threshold: float = 0.2, ) -> Dict[str, Any]: """ Comprehensive health check for a flow. @@ -206,7 +209,9 @@ def main(): resource_type = resource["type"] resource_id = resource["id"] - health = check_flow_health(client, resource_type, resource_id, args.threshold) + health = check_flow_health( + client, resource_type, resource_id, args.threshold + ) results.append(health) if health["issues"]: diff --git a/skills/nexla/scripts/list_resources.py b/skills/nexla/scripts/list_resources.py index a2bf5fc..f717217 100644 --- a/skills/nexla/scripts/list_resources.py +++ b/skills/nexla/scripts/list_resources.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """List and filter Nexla resources.""" -import sys -import json import argparse -from typing import Dict, List, Any, Optional +import json +import sys +from typing import Any, Dict, List, Optional try: from nexla_sdk import NexlaClient @@ -144,7 +144,10 @@ def main() -> None: "--name", help="Filter by name (substring match, case-insensitive)" ) parser.add_argument( - "--limit", type=int, default=10, help="Maximum number of results (default: 10, max: 500)" + "--limit", + type=int, + default=10, + help="Maximum number of results (default: 10, max: 500)", ) parser.add_argument( "--full", diff --git a/skills/nexla/scripts/manage_access.py b/skills/nexla/scripts/manage_access.py index 7704a55..edef116 100644 --- a/skills/nexla/scripts/manage_access.py +++ b/skills/nexla/scripts/manage_access.py @@ -22,10 +22,10 @@ NEXLA_API_URL can override the default API endpoint. """ -import sys -import json import argparse -from typing import List, Dict, Any +import json +import sys +from typing import Any, Dict, List try: from nexla_sdk import NexlaClient @@ -41,18 +41,21 @@ def list_accessors(client, resource_type: str, resource_id: int) -> List[Dict]: return [ { - "type": acc.type.value if hasattr(acc.type, 'value') else acc.type, - "id": getattr(acc, 'id', None), - "email": getattr(acc, 'email', None), - "name": getattr(acc, 'name', None), - "access_roles": [r.value if hasattr(r, 'value') else r for r in acc.access_roles] + "type": acc.type.value if hasattr(acc.type, "value") else acc.type, + "id": getattr(acc, "id", None), + "email": getattr(acc, "email", None), + "name": getattr(acc, "name", None), + "access_roles": [ + r.value if hasattr(r, "value") else r for r in acc.access_roles + ], } for acc in accessors ] -def grant_access(client, resource_type: str, resource_ids: List[int], - accessor: Dict) -> Dict[str, Any]: +def grant_access( + client, resource_type: str, resource_ids: List[int], accessor: Dict +) -> Dict[str, Any]: """Grant access to multiple resources.""" resource_api = getattr(client, resource_type) results = {"success": [], "failed": []} @@ -69,8 +72,9 @@ def grant_access(client, resource_type: str, resource_ids: List[int], return results -def revoke_access(client, resource_type: str, resource_ids: List[int], - accessor: Dict) -> Dict[str, Any]: +def revoke_access( + client, resource_type: str, resource_ids: List[int], accessor: Dict +) -> Dict[str, Any]: """Revoke access from multiple resources.""" resource_api = getattr(client, resource_type) results = {"success": [], "failed": []} @@ -91,7 +95,7 @@ def build_accessor(args) -> Dict[str, Any]: """Build accessor dict from CLI arguments.""" accessor = { "type": args.accessor_type, - "access_roles": [args.role] if args.role else ["collaborator"] + "access_roles": [args.role] if args.role else ["collaborator"], } if args.accessor_id: @@ -106,53 +110,38 @@ def main(): parser = argparse.ArgumentParser( description="Manage Nexla resource access control", formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=__doc__ + epilog=__doc__, ) parser.add_argument( - "--operation", "-o", + "--operation", + "-o", choices=["list", "grant", "revoke"], required=True, - help="Operation to perform" + help="Operation to perform", ) parser.add_argument( - "--resource-type", "-t", + "--resource-type", + "-t", required=True, - help="Resource type: sources, nexsets, destinations, flows, credentials, etc." - ) - parser.add_argument( - "--resource-id", "-r", - type=int, - help="Single resource ID" + help="Resource type: sources, nexsets, destinations, flows, credentials, etc.", ) + parser.add_argument("--resource-id", "-r", type=int, help="Single resource ID") parser.add_argument( - "--resource-ids", - help="Comma-separated resource IDs for batch operations" + "--resource-ids", help="Comma-separated resource IDs for batch operations" ) parser.add_argument( - "--accessor-type", - choices=["USER", "TEAM", "ORG"], - help="Type of accessor" - ) - parser.add_argument( - "--accessor-id", - type=int, - help="Accessor ID (for TEAM or ORG)" - ) - parser.add_argument( - "--email", - help="Email address (for USER accessor)" + "--accessor-type", choices=["USER", "TEAM", "ORG"], help="Type of accessor" ) + parser.add_argument("--accessor-id", type=int, help="Accessor ID (for TEAM or ORG)") + parser.add_argument("--email", help="Email address (for USER accessor)") parser.add_argument( "--role", choices=["owner", "admin", "operator", "collaborator"], default="collaborator", - help="Access role (default: collaborator)" - ) - parser.add_argument( - "--output", "-O", - help="Output file for results (JSON)" + help="Access role (default: collaborator)", ) + parser.add_argument("--output", "-O", help="Output file for results (JSON)") args = parser.parse_args() @@ -193,15 +182,21 @@ def main(): accessor = build_accessor(args) if args.operation == "grant": - result = grant_access(client, args.resource_type, resource_ids, accessor) + result = grant_access( + client, args.resource_type, resource_ids, accessor + ) else: - result = revoke_access(client, args.resource_type, resource_ids, accessor) + result = revoke_access( + client, args.resource_type, resource_ids, accessor + ) # Summary - print(f"\nSummary: {len(result['success'])} succeeded, {len(result['failed'])} failed") + print( + f"\nSummary: {len(result['success'])} succeeded, {len(result['failed'])} failed" + ) if args.output: - with open(args.output, 'w') as f: + with open(args.output, "w") as f: json.dump(result, f, indent=2) print(f"Results saved to {args.output}") diff --git a/skills/nexla/scripts/retry_helpers.py b/skills/nexla/scripts/retry_helpers.py index 3b486a6..bfc1a13 100644 --- a/skills/nexla/scripts/retry_helpers.py +++ b/skills/nexla/scripts/retry_helpers.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 """Retry and backoff utilities for Nexla operations.""" -import time -import random import functools -from typing import Callable, TypeVar, Type, Tuple +import random +import time +from typing import Callable, Tuple, Type, TypeVar # Import Nexla SDK exceptions try: - from nexla_sdk import RateLimitError, ServerError, NexlaError + from nexla_sdk import NexlaError, RateLimitError, ServerError except ImportError: # Fallback for when SDK is not installed class NexlaError(Exception): diff --git a/tests/conftest.py b/tests/conftest.py index db5ce45..dd88ba9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,12 +2,13 @@ import logging import os + import pytest from dotenv import load_dotenv from nexla_sdk import NexlaClient from nexla_sdk.exceptions import AuthenticationError -from tests.utils import MockHTTPClient, MockResponseBuilder, MockDataFactory +from tests.utils import MockDataFactory, MockHTTPClient, MockResponseBuilder from tests.utils.assertions import NexlaAssertions # Load environment variables from .env file in the tests directory @@ -75,22 +76,25 @@ def mock_http_client(): def mock_client(mock_http_client): """Create a Nexla client with mock HTTP client for unit tests.""" # First, add the authentication token response for the initial token request - mock_http_client.add_response("/token", { - "access_token": "mock-token-12345", - "expires_in": 86400, - "token_type": "Bearer" - }) - + mock_http_client.add_response( + "/token", + { + "access_token": "mock-token-12345", + "expires_in": 86400, + "token_type": "Bearer", + }, + ) + # Create client with service key authentication client = NexlaClient( service_key="test-service-key", base_url="https://api.test.nexla.io/nexla-api", - http_client=mock_http_client + http_client=mock_http_client, ) - + # Clear any previous requests from initialization mock_http_client.clear_requests() - + return client @@ -119,40 +123,42 @@ def integration_client(api_url: str, api_version: str) -> NexlaClient: Provides a NexlaClient instance configured for integration tests. Tries to make a simple call to verify authentication. """ - logger.info(f"Initializing Nexla client with URL: {api_url}, API version: {api_version}") - + logger.info( + f"Initializing Nexla client with URL: {api_url}, API version: {api_version}" + ) + # Try service key first, then access token if NEXLA_TEST_SERVICE_KEY: client = NexlaClient( - service_key=NEXLA_TEST_SERVICE_KEY, - base_url=api_url, - api_version=api_version + service_key=NEXLA_TEST_SERVICE_KEY, + base_url=api_url, + api_version=api_version, ) elif NEXLA_TEST_ACCESS_TOKEN: client = NexlaClient( access_token=NEXLA_TEST_ACCESS_TOKEN, base_url=api_url, - api_version=api_version + api_version=api_version, ) else: pytest.skip("No authentication credentials available for integration tests") - + # Perform a lightweight check to ensure the client is functional try: logger.info("Testing client authentication") # Try to get credentials list as a lightweight auth check credentials = client.credentials.list() logger.info(f"Authentication successful, found {len(credentials)} credentials") - + except AuthenticationError as e: logger.error(f"Authentication failed for integration tests: {e}") pytest.skip(f"Authentication failed for integration tests: {e}") - + except Exception as e: # Catch other potential issues like network errors during setup logger.error(f"Could not connect to Nexla API or other setup error: {e}") pytest.skip(f"Could not connect to Nexla API or other setup error: {e}") - + return client @@ -166,8 +172,8 @@ def sample_credential_data(): "properties": { "access_key_id": "test-access-key", "secret_access_key": "test-secret-key", - "region": "us-east-1" - } + "region": "us-east-1", + }, } @@ -181,34 +187,30 @@ def sample_credential_response(): def sample_credentials_list(): """Sample list of credentials for testing.""" from tests.utils.mock_builders import credential_list + return credential_list(count=3) @pytest.fixture def sample_probe_tree_request(): """Sample probe tree request for testing.""" - return { - "depth": 3, - "path": "/" - } + return {"depth": 3, "path": "/"} @pytest.fixture def sample_probe_sample_request(): """Sample probe sample request for testing.""" - return { - "connection_type": "s3", - "path": "/data/sample.csv", - "max_rows": 100 - } + return {"connection_type": "s3", "path": "/data/sample.csv", "max_rows": 100} # Auto-use fixtures for marking tests @pytest.fixture(autouse=True) def mark_unit_tests_by_default(request): """Automatically mark tests as unit tests if not otherwise marked.""" - if not any(mark.name in ['integration', 'performance', 'contract'] - for mark in request.node.iter_markers()): + if not any( + mark.name in ["integration", "performance", "contract"] + for mark in request.node.iter_markers() + ): request.node.add_marker(pytest.mark.unit) @@ -217,7 +219,7 @@ def mark_unit_tests_by_default(request): def temp_env_vars(): """Temporarily set environment variables for testing.""" original_env = {} - + def set_env(**kwargs): for key, value in kwargs.items(): original_env[key] = os.environ.get(key) @@ -225,9 +227,9 @@ def set_env(**kwargs): os.environ.pop(key, None) else: os.environ[key] = str(value) - + yield set_env - + # Restore original environment for key, value in original_env.items(): if value is None: @@ -241,13 +243,13 @@ def set_env(**kwargs): def cleanup_credentials(): """Track created credentials for cleanup in integration tests.""" created_credentials = [] - + def track_credential(credential): created_credentials.append(credential) return credential - + yield track_credential - + # Cleanup (this will run after the test) # Note: This would need access to the client, so in practice # you'd pass the client to this fixture or use a different approach diff --git a/tests/integration/test_credentials.py b/tests/integration/test_credentials.py index 3949042..e170e76 100644 --- a/tests/integration/test_credentials.py +++ b/tests/integration/test_credentials.py @@ -1,24 +1,28 @@ """Integration tests for credentials resource with real API calls.""" -import pytest import time +import pytest + from nexla_sdk.exceptions import AuthenticationError, NotFoundError -from nexla_sdk.models.credentials.responses import Credential from nexla_sdk.models.credentials.requests import ( - CredentialCreate, CredentialUpdate, ProbeTreeRequest, ProbeSampleRequest + CredentialCreate, + CredentialUpdate, + ProbeSampleRequest, + ProbeTreeRequest, ) +from nexla_sdk.models.credentials.responses import Credential @pytest.mark.integration class TestCredentialsIntegration: """Integration tests for credentials using real API.""" - + def test_list_credentials(self, integration_client): """Test listing credentials with real API.""" # Act credentials = integration_client.credentials.list() - + # Assert assert isinstance(credentials, list) # Each credential should be a Credential model @@ -27,79 +31,79 @@ def test_list_credentials(self, integration_client): assert credential.id is not None assert credential.name is not None assert credential.credentials_type is not None - + def test_list_credentials_with_filters(self, integration_client): """Test listing credentials with type filter.""" # Act all_credentials = integration_client.credentials.list() - + if all_credentials: # Get the first credential type to filter by credential_type = all_credentials[0].credentials_type filtered_credentials = integration_client.credentials.list( credentials_type=credential_type ) - + # Assert assert isinstance(filtered_credentials, list) for credential in filtered_credentials: assert credential.credentials_type == credential_type - + def test_get_credential(self, integration_client): """Test getting a single credential.""" # Arrange - get first available credential credentials = integration_client.credentials.list() if not credentials: pytest.skip("No credentials available for testing") - + credential_id = credentials[0].id - + # Act credential = integration_client.credentials.get(credential_id) - + # Assert assert isinstance(credential, Credential) assert credential.id == credential_id assert credential.name is not None assert credential.credentials_type is not None - + def test_get_credential_with_expand(self, integration_client): """Test getting a credential with expand option.""" # Arrange credentials = integration_client.credentials.list() if not credentials: pytest.skip("No credentials available for testing") - + credential_id = credentials[0].id - + # Act credential = integration_client.credentials.get(credential_id, expand=True) - + # Assert assert isinstance(credential, Credential) assert credential.id == credential_id - + def test_get_nonexistent_credential(self, integration_client): """Test getting a credential that doesn't exist.""" # Arrange - use a very high ID that's unlikely to exist nonexistent_id = 999999999 - + # Act & Assert with pytest.raises(NotFoundError): integration_client.credentials.get(nonexistent_id) - + def test_probe_credential(self, integration_client): """Test probing a credential.""" # Arrange credentials = integration_client.credentials.list() if not credentials: pytest.skip("No credentials available for testing") - + credential_id = credentials[0].id - + # Act result = integration_client.credentials.probe(credential_id) - + # Assert assert isinstance(result, dict) assert "status" in result @@ -107,10 +111,10 @@ def test_probe_credential(self, integration_client): assert result["status"] in ["ok", "success"] or "message" in result -@pytest.mark.integration +@pytest.mark.integration class TestCredentialsLifecycle: """Test full credential lifecycle with cleanup.""" - + @pytest.fixture def test_credential_data(self): """Create test credential data for a mock/test connector.""" @@ -122,21 +126,21 @@ def test_credential_data(self): "credentials": { "api_key": "test-key-12345", "endpoint": "https://httpbin.org/get", # Safe test endpoint - } + }, } - + @pytest.fixture def cleanup_credential(self, integration_client): """Fixture to cleanup created credentials after test.""" created_credentials = [] - + def track_credential(credential): """Track a credential for cleanup.""" created_credentials.append(credential) return credential - + yield track_credential - + # Cleanup for credential in created_credentials: try: @@ -144,42 +148,43 @@ def track_credential(credential): print(f"Cleaned up test credential: {credential.id}") except Exception as e: print(f"Failed to cleanup credential {credential.id}: {e}") - - def test_credential_create_update_delete(self, integration_client, test_credential_data, cleanup_credential): + + def test_credential_create_update_delete( + self, integration_client, test_credential_data, cleanup_credential + ): """Test full credential lifecycle: create, read, update, delete.""" # Create create_request = CredentialCreate(**test_credential_data) credential = integration_client.credentials.create(create_request) cleanup_credential(credential) # Track for cleanup - + assert isinstance(credential, Credential) assert credential.id is not None assert credential.name == test_credential_data["name"] assert credential.credentials_type == test_credential_data["credentials_type"] - + # Read fetched = integration_client.credentials.get(credential.id) assert fetched.id == credential.id assert fetched.name == credential.name - + # Update update_data = CredentialUpdate( - name=f"Updated {credential.name}", - description="Updated description" + name=f"Updated {credential.name}", description="Updated description" ) updated = integration_client.credentials.update(credential.id, update_data) assert updated.name == update_data.name assert updated.description == update_data.description - + # Verify in list all_credentials = integration_client.credentials.list() assert any(c.id == credential.id for c in all_credentials) - + # Delete result = integration_client.credentials.delete(credential.id) assert isinstance(result, dict) # Should indicate success or similar - + # Verify deletion with pytest.raises(NotFoundError): integration_client.credentials.get(credential.id) @@ -189,79 +194,75 @@ def test_credential_create_update_delete(self, integration_client, test_credenti @pytest.mark.slow class TestCredentialsProbing: """Test credential probing operations (may be slow).""" - + def test_probe_tree_with_real_credential(self, integration_client): """Test probing tree structure with a real credential.""" # Arrange credentials = integration_client.credentials.list() if not credentials: pytest.skip("No credentials available for testing") - + # Find a credential that supports tree probing (usually file-based) suitable_credential = None for credential in credentials: if credential.credentials_type in ["s3", "gcs", "azure_blb", "ftp"]: suitable_credential = credential break - + if not suitable_credential: pytest.skip("No file-based credentials available for tree probing") - + # Act probe_request = ProbeTreeRequest( depth=2, - path="/" # Root path + path="/", # Root path ) - + try: result = integration_client.credentials.probe_tree( - suitable_credential.id, - probe_request + suitable_credential.id, probe_request ) - + # Assert assert result.status in ["ok", "success"] assert result.connection_type is not None - assert hasattr(result, 'object') # Should have object/output field - + assert hasattr(result, "object") # Should have object/output field + except Exception as e: # Tree probing might fail for various reasons (permissions, empty bucket, etc.) # This is acceptable for integration tests pytest.skip(f"Tree probing failed (expected for some credentials): {e}") - + def test_probe_sample_with_real_credential(self, integration_client): """Test probing sample data with a real credential.""" # Arrange credentials = integration_client.credentials.list() if not credentials: pytest.skip("No credentials available for testing") - + # Find a suitable credential suitable_credential = None for credential in credentials: if credential.credentials_type in ["s3", "gcs", "azure_blb"]: suitable_credential = credential break - + if not suitable_credential: pytest.skip("No suitable credentials available for sample probing") - + # Act - probe_request = ProbeSampleRequest( - path="/test/" # Generic test path - ) - + probe_request = ProbeSampleRequest(path="/test/") # Generic test path + try: result = integration_client.credentials.probe_sample( - suitable_credential.id, - probe_request + suitable_credential.id, probe_request ) - + # Assert assert result.status in ["ok", "success"] assert result.connection_type is not None - assert hasattr(result, 'output') - + assert hasattr(result, "output") + except Exception as e: # Sample probing might fail if no data exists at the path pytest.skip(f"Sample probing failed (expected for some credentials): {e}") @@ -270,31 +271,29 @@ def test_probe_sample_with_real_credential(self, integration_client): @pytest.mark.integration class TestCredentialsErrorHandling: """Test error handling with real API.""" - + def test_authentication_error_simulation(self, api_url, api_version): """Test authentication error with invalid credentials.""" from nexla_sdk import NexlaClient - + # Create client with invalid service key invalid_client = NexlaClient( - service_key="invalid-key-12345", - base_url=api_url, - api_version=api_version + service_key="invalid-key-12345", base_url=api_url, api_version=api_version ) - + # Act & Assert with pytest.raises(AuthenticationError): invalid_client.credentials.list() - + def test_create_credential_with_invalid_type(self, integration_client): """Test creating credential with invalid type.""" # Arrange invalid_data = { "name": "Invalid Credential", "credentials_type": "invalid_type_12345", - "credentials": {"test": "data"} + "credentials": {"test": "data"}, } - + # Act & Assert # This might raise ValidationError or APIError depending on validation with pytest.raises(Exception): # Broad exception for now @@ -305,36 +304,36 @@ def test_create_credential_with_invalid_type(self, integration_client): @pytest.mark.performance class TestCredentialsPerformance: """Test performance characteristics of credentials API.""" - + def test_list_credentials_performance(self, integration_client): """Test that listing credentials completes in reasonable time.""" # Act start_time = time.time() credentials = integration_client.credentials.list() end_time = time.time() - + # Assert duration = end_time - start_time assert duration < 10.0, f"Listing credentials took too long: {duration:.2f}s" - + # Also verify we got results assert isinstance(credentials, list) - + def test_get_credential_performance(self, integration_client): """Test that getting a credential completes quickly.""" # Arrange credentials = integration_client.credentials.list() if not credentials: pytest.skip("No credentials available for testing") - + credential_id = credentials[0].id - + # Act start_time = time.time() credential = integration_client.credentials.get(credential_id) end_time = time.time() - + # Assert duration = end_time - start_time assert duration < 5.0, f"Getting credential took too long: {duration:.2f}s" - assert isinstance(credential, Credential) \ No newline at end of file + assert isinstance(credential, Credential) diff --git a/tests/integration/test_destinations.py b/tests/integration/test_destinations.py index 5a4a60f..af354e9 100644 --- a/tests/integration/test_destinations.py +++ b/tests/integration/test_destinations.py @@ -1,9 +1,16 @@ """Integration tests for destinations resource.""" -import pytest + import os + +import pytest + from nexla_sdk import NexlaClient, NexlaError from nexla_sdk.exceptions import NotFoundError -from nexla_sdk.models.destinations import DestinationCreate, DestinationUpdate, DestinationCopyOptions +from nexla_sdk.models.destinations import ( + DestinationCopyOptions, + DestinationCreate, + DestinationUpdate, +) from tests.utils.assertions import NexlaAssertions @@ -15,11 +22,11 @@ class TestDestinationsIntegration: def client(self): """Create authenticated client for integration tests.""" service_key = os.getenv("NEXLA_SERVICE_KEY") - access_token = os.getenv("NEXLA_ACCESS_TOKEN") - + access_token = os.getenv("NEXLA_ACCESS_TOKEN") + if not service_key and not access_token: pytest.skip("No authentication credentials provided") - + if service_key: return NexlaClient(service_key=service_key) else: @@ -33,11 +40,11 @@ def assertions(self): def test_destination_crud_operations(self, client, assertions): """Test complete CRUD lifecycle for destinations.""" created_destination = None - + try: # Step 1: Get initial count (not storing for performance) # initial_destinations = client.destinations.list() - + # Step 2: Create new destination (requires existing credential and dataset) # Note: This will fail without real credentials and datasets # Using mock data for demonstration @@ -46,40 +53,46 @@ def test_destination_crud_operations(self, client, assertions): sink_type="s3", data_credentials_id=1, # Replace with real credential ID data_set_id=1, # Replace with real dataset ID - description="Created by integration test" + description="Created by integration test", ) - + # This will likely fail due to missing real IDs, but shows the pattern try: created_destination = client.destinations.create(create_data) - + # Verify creation assertions.assert_destination_response(created_destination) assert created_destination.name == "Test Integration Destination" assert created_destination.sink_type == "s3" - + # Step 3: Update the destination update_data = DestinationUpdate( name="Updated Integration Destination", - description="Updated by integration test" + description="Updated by integration test", + ) + + updated_destination = client.destinations.update( + created_destination.id, update_data ) - - updated_destination = client.destinations.update(created_destination.id, update_data) assertions.assert_destination_response(updated_destination) assert updated_destination.name == "Updated Integration Destination" - + # Step 4: Get the destination retrieved_destination = client.destinations.get(created_destination.id) assertions.assert_destination_response(retrieved_destination) assert retrieved_destination.id == created_destination.id - + # Step 5: Get with expand - expanded_destination = client.destinations.get(created_destination.id, expand=True) + expanded_destination = client.destinations.get( + created_destination.id, expand=True + ) assertions.assert_destination_response(expanded_destination) - + except Exception as e: - pytest.skip(f"Destination CRUD test requires valid data credentials and dataset IDs: {e}") - + pytest.skip( + f"Destination CRUD test requires valid data credentials and dataset IDs: {e}" + ) + finally: # Cleanup: Delete created destination if created_destination: @@ -92,7 +105,7 @@ def test_destination_list_with_pagination(self, client, assertions): """Test listing destinations with pagination.""" # Get first page destinations_page1 = client.destinations.list(page=1, per_page=10) - + # Verify structure assert isinstance(destinations_page1, list) for destination in destinations_page1: @@ -101,51 +114,56 @@ def test_destination_list_with_pagination(self, client, assertions): def test_destination_activate_pause_operations(self, client): """Test destination activation and pause operations.""" destinations = client.destinations.list() - + if not destinations: pytest.skip("No destinations available for activate/pause testing") - + destination = destinations[0] - + try: # Test activation activated = client.destinations.activate(destination.id) - assert hasattr(activated, 'id') + assert hasattr(activated, "id") assert activated.id == destination.id - + # Test pause paused = client.destinations.pause(destination.id) - assert hasattr(paused, 'id') + assert hasattr(paused, "id") assert paused.id == destination.id - + except Exception as e: - pytest.skip(f"Activate/pause operations failed (may require specific permissions): {e}") + pytest.skip( + f"Activate/pause operations failed (may require specific permissions): {e}" + ) def test_destination_copy_operation(self, client, assertions): """Test destination copying.""" destinations = client.destinations.list() - + if not destinations: pytest.skip("No destinations available for copy testing") - + source_destination = destinations[0] copied_destination = None - + try: copy_options = DestinationCopyOptions( - reuse_data_credentials=True, - copy_access_controls=False + reuse_data_credentials=True, copy_access_controls=False + ) + + copied_destination = client.destinations.copy( + source_destination.id, copy_options ) - - copied_destination = client.destinations.copy(source_destination.id, copy_options) - + # Verify copy assertions.assert_destination_response(copied_destination) assert copied_destination.id != source_destination.id assert copied_destination.sink_type == source_destination.sink_type - + except Exception as e: - pytest.skip(f"Copy operation failed (may require specific permissions): {e}") + pytest.skip( + f"Copy operation failed (may require specific permissions): {e}" + ) finally: # Cleanup copied destination if copied_destination: @@ -157,12 +175,14 @@ def test_destination_copy_operation(self, client, assertions): def test_destination_not_found_error(self, client): """Test handling of destination not found errors.""" non_existent_id = 999999999 - + with pytest.raises((NotFoundError, NexlaError)) as exc_info: client.destinations.get(non_existent_id) - + # The specific error type may vary based on API implementation - assert "not found" in str(exc_info.value).lower() or "404" in str(exc_info.value) + assert "not found" in str(exc_info.value).lower() or "404" in str( + exc_info.value + ) def test_destination_validation_errors(self, client): """Test handling of destination validation errors.""" @@ -172,58 +192,58 @@ def test_destination_validation_errors(self, client): name="", # Empty name should fail validation sink_type="invalid_type", data_credentials_id=-1, # Invalid ID - data_set_id=-1 # Invalid ID + data_set_id=-1, # Invalid ID ) - + with pytest.raises(Exception) as exc_info: client.destinations.create(invalid_data) - + # Should get some kind of validation or API error assert "error" in str(exc_info.value).lower() - + except Exception as e: pytest.skip(f"Validation error test failed: {e}") def test_destination_access_control_operations(self, client): """Test destination access control operations.""" destinations = client.destinations.list() - + if not destinations: pytest.skip("No destinations available for access control testing") - + destination = destinations[0] - + try: # Test getting accessors accessors = client.destinations.get_accessors(destination.id) assert isinstance(accessors, list) - + except Exception as e: pytest.skip(f"Access control operations may not be available: {e}") def test_destination_audit_log(self, client): """Test getting destination audit log.""" destinations = client.destinations.list() - + if not destinations: pytest.skip("No destinations available for audit log testing") - + destination = destinations[0] - + try: # Test getting audit log audit_log = client.destinations.get_audit_log(destination.id) assert isinstance(audit_log, list) - + except Exception as e: pytest.skip(f"Audit log operations may not be available: {e}") def test_destination_list_with_access_role_filter(self, client, assertions): """Test listing destinations with access role filter.""" destinations = client.destinations.list(access_role="owner") - + assert isinstance(destinations, list) for destination in destinations: assertions.assert_destination_response(destination) - if hasattr(destination, 'access_roles'): - assert "owner" in destination.access_roles \ No newline at end of file + if hasattr(destination, "access_roles"): + assert "owner" in destination.access_roles diff --git a/tests/integration/test_flows.py b/tests/integration/test_flows.py index 49530e8..32465df 100644 --- a/tests/integration/test_flows.py +++ b/tests/integration/test_flows.py @@ -1,30 +1,31 @@ """Integration tests for flows resource.""" + import os -import pytest from typing import Optional +import pytest + from nexla_sdk import NexlaClient -from nexla_sdk.models.flows.responses import FlowResponse, FlowMetrics -from nexla_sdk.models.flows.requests import FlowCopyOptions -from nexla_sdk.models.common import FlowNode from nexla_sdk.exceptions import ServerError - +from nexla_sdk.models.common import FlowNode +from nexla_sdk.models.flows.requests import FlowCopyOptions +from nexla_sdk.models.flows.responses import FlowMetrics, FlowResponse from tests.utils.fixtures import get_test_credentials @pytest.mark.integration class TestFlowsIntegration: """Integration tests for flows resource.""" - + @pytest.fixture(scope="class") def client(self) -> Optional[NexlaClient]: """Create a real Nexla client for integration tests.""" creds = get_test_credentials() if not creds: pytest.skip("No test credentials available") - + return NexlaClient(**creds) - + @pytest.fixture(scope="class") def test_flow_id(self) -> Optional[int]: """Get test flow ID from environment.""" @@ -32,7 +33,7 @@ def test_flow_id(self) -> Optional[int]: if flow_id: return int(flow_id) return None - + @pytest.fixture(scope="class") def test_source_id(self) -> Optional[int]: """Get test source ID from environment.""" @@ -40,7 +41,7 @@ def test_source_id(self) -> Optional[int]: if source_id: return int(source_id) return None - + @pytest.fixture(scope="class") def test_dataset_id(self) -> Optional[int]: """Get test dataset ID from environment.""" @@ -48,73 +49,73 @@ def test_dataset_id(self) -> Optional[int]: if dataset_id: return int(dataset_id) return None - + def test_list_flows(self, client): """Test listing all flows.""" # Act flows = client.flows.list() - + # Assert assert isinstance(flows, list) assert len(flows) >= 0 - + if flows: flow = flows[0] assert isinstance(flow, FlowResponse) assert isinstance(flow.flows, list) - + # Check flow structure if flow.flows: node = flow.flows[0] - assert hasattr(node, 'id') - assert hasattr(node, 'parent_node_id') - + assert hasattr(node, "id") + assert hasattr(node, "parent_node_id") + def test_list_flows_with_elements(self, client): """Test listing flows with expanded elements.""" # Act flows = client.flows.list(flows_only=False) - + # Assert assert isinstance(flows, list) - + if flows and flows[0].flows: flow = flows[0] # Check for expanded elements if flow.data_sources: - assert all(hasattr(src, 'id') for src in flow.data_sources) + assert all(hasattr(src, "id") for src in flow.data_sources) if flow.data_sets: - assert all(hasattr(ds, 'id') for ds in flow.data_sets) + assert all(hasattr(ds, "id") for ds in flow.data_sets) if flow.data_sinks: - assert all(hasattr(sink, 'id') for sink in flow.data_sinks) - + assert all(hasattr(sink, "id") for sink in flow.data_sinks) + def test_list_flows_only(self, client): """Test listing flows without expanded elements.""" # Act flows = client.flows.list(flows_only=True) - + # Assert assert isinstance(flows, list) - + if flows: flow = flows[0] # Expanded elements should be None when flows_only=True assert flow.data_sources is None or len(flow.data_sources) == 0 assert flow.data_sets is None or len(flow.data_sets) == 0 assert flow.data_sinks is None or len(flow.data_sinks) == 0 - + def test_get_flow_by_id(self, client, test_flow_id): """Test getting a specific flow by ID.""" if not test_flow_id: pytest.skip("No test flow ID provided") - + # Act flow = client.flows.get(test_flow_id) - + # Assert assert isinstance(flow, FlowResponse) assert isinstance(flow.flows, list) assert len(flow.flows) > 0 - + # Check that we got the right flow found = False for node in flow.flows: @@ -125,121 +126,120 @@ def test_get_flow_by_id(self, client, test_flow_id): if self._find_node_in_children(node, test_flow_id): found = True break - + assert found, f"Flow ID {test_flow_id} not found in response" - + def test_get_flow_by_source(self, client, test_source_id): """Test getting flow by data source.""" if not test_source_id: pytest.skip("No test source ID provided") - + # Act flow = client.flows.get_by_resource("data_sources", test_source_id) - + # Assert assert isinstance(flow, FlowResponse) assert isinstance(flow.flows, list) - + # Verify the flow contains the source if flow.flows: # Root nodes should have data_source_id matching source_found = any( - node.data_source_id == test_source_id - for node in flow.flows + node.data_source_id == test_source_id for node in flow.flows ) assert source_found, f"Source ID {test_source_id} not found in flow" - + def test_get_flow_by_dataset(self, client, test_dataset_id): """Test getting flow by dataset.""" if not test_dataset_id: pytest.skip("No test dataset ID provided") - + # Act flow = client.flows.get_by_resource("data_sets", test_dataset_id) - + # Assert assert isinstance(flow, FlowResponse) assert isinstance(flow.flows, list) - + # Verify the flow contains the dataset if flow.flows: # Nodes should have data_set_id matching dataset_found = any( - getattr(node, 'data_set_id', None) == test_dataset_id + getattr(node, "data_set_id", None) == test_dataset_id for node in flow.flows ) assert dataset_found, f"Dataset ID {test_dataset_id} not found in flow" - + def test_flow_activation_pause_cycle(self, client, test_flow_id): """Test activating and pausing a flow.""" if not test_flow_id: pytest.skip("No test flow ID provided") - + # Get initial state # initial_flow = client.flows.get(test_flow_id) # Not used, saving API call - + try: # Pause the flow first to ensure we can activate it paused_flow = client.flows.pause(test_flow_id) assert isinstance(paused_flow, FlowResponse) - + # Activate the flow activated_flow = client.flows.activate(test_flow_id) assert isinstance(activated_flow, FlowResponse) - + # Pause it again final_flow = client.flows.pause(test_flow_id) assert isinstance(final_flow, FlowResponse) - + except ServerError as e: # Some flows may not support activation/pause if e.status_code in (400, 403, 405): pytest.skip(f"Flow does not support activation/pause: {e}") raise - + def test_flow_metrics(self, client): """Test getting flows with metrics.""" # Act flows = client.flows.list(include_run_metrics=True) - + # Assert assert isinstance(flows, list) - + if flows and flows[0].metrics: metrics = flows[0].metrics assert isinstance(metrics, list) - + for metric in metrics: assert isinstance(metric, FlowMetrics) - assert hasattr(metric, 'origin_node_id') - assert hasattr(metric, 'records') - assert hasattr(metric, 'size') - assert hasattr(metric, 'errors') - assert hasattr(metric, 'run_id') - + assert hasattr(metric, "origin_node_id") + assert hasattr(metric, "records") + assert hasattr(metric, "size") + assert hasattr(metric, "errors") + assert hasattr(metric, "run_id") + def test_flow_copy(self, client, test_flow_id): """Test copying a flow.""" if not test_flow_id: pytest.skip("No test flow ID provided") - + # Arrange copy_options = FlowCopyOptions( reuse_data_credentials=True, copy_access_controls=False, - copy_dependent_data_flows=False + copy_dependent_data_flows=False, ) - + try: # Act copied_flow = client.flows.copy(test_flow_id, copy_options) - + # Assert assert isinstance(copied_flow, FlowResponse) assert isinstance(copied_flow.flows, list) - + # The copied flow should have new IDs assert all(node.id != test_flow_id for node in copied_flow.flows) - + # Clean up - delete the copied flow if copied_flow.flows: for node in copied_flow.flows: @@ -249,21 +249,21 @@ def test_flow_copy(self, client, test_flow_id): client.flows.delete(node.id) except ServerError: pass # Best effort cleanup - + except ServerError as e: if e.status_code in (403, 405): pytest.skip(f"Flow copy not supported: {e}") raise - + def test_delete_flow_validation(self, client): """Test that deleting active flow fails with proper error.""" # We don't actually want to delete real flows in integration tests # Just verify the error handling works - + # Find an active flow flows = client.flows.list() active_flow_id = None - + for flow_resp in flows: for node in flow_resp.flows: # Assuming we can check status somehow @@ -272,59 +272,61 @@ def test_delete_flow_validation(self, client): break if active_flow_id: break - + if not active_flow_id: pytest.skip("No active flow found for testing") - + # Try to delete active flow - should fail with pytest.raises(ServerError) as exc_info: client.flows.delete(active_flow_id) - + # Verify error is about active resources assert exc_info.value.status_code in (400, 405) - + def test_flow_structure_validation(self, client): """Test that flow structures are properly formed.""" # Act flows = client.flows.list() - + # Assert for flow_resp in flows: for node in flow_resp.flows: self._validate_flow_node(node) - + # Helper methods def _find_node_in_children(self, node: FlowNode, target_id: int) -> bool: """Find a node with target_id in the children of the given node.""" - if hasattr(node, 'children') and node.children: + if hasattr(node, "children") and node.children: for child in node.children: if child.id == target_id: return True if self._find_node_in_children(child, target_id): return True return False - + def _validate_flow_node(self, node: FlowNode) -> None: """Validate flow node structure.""" - assert hasattr(node, 'id') + assert hasattr(node, "id") assert isinstance(node.id, int) - + # Root nodes should have no parent but should have data_source if node.parent_node_id is None: - assert node.data_source_id is not None or (hasattr(node, 'data_source') and node.data_source is not None) - + assert node.data_source_id is not None or ( + hasattr(node, "data_source") and node.data_source is not None + ) + # Recursively validate children if node.children: assert isinstance(node.children, list) for child in node.children: assert child.parent_node_id == node.id - self._validate_flow_node(child) + self._validate_flow_node(child) if node.parent_node_id is None: - assert node.data_source_id is not None or hasattr(node, 'data_source') - + assert node.data_source_id is not None or hasattr(node, "data_source") + # Recursively validate children if node.children: assert isinstance(node.children, list) for child in node.children: assert child.parent_node_id == node.id - self._validate_flow_node(child) \ No newline at end of file + self._validate_flow_node(child) diff --git a/tests/integration/test_lookups.py b/tests/integration/test_lookups.py index 6e46a7f..f3aa341 100644 --- a/tests/integration/test_lookups.py +++ b/tests/integration/test_lookups.py @@ -1,28 +1,29 @@ """Integration tests for lookups resource.""" -import pytest + from typing import Optional +import pytest + from nexla_sdk import NexlaClient -from nexla_sdk.models.lookups.responses import Lookup -from nexla_sdk.models.lookups.requests import LookupCreate, LookupUpdate from nexla_sdk.exceptions import ServerError - +from nexla_sdk.models.lookups.requests import LookupCreate, LookupUpdate +from nexla_sdk.models.lookups.responses import Lookup from tests.utils.fixtures import get_test_credentials @pytest.mark.integration class TestLookupsIntegration: """Integration tests for lookups resource.""" - + @pytest.fixture(scope="class") def client(self) -> Optional[NexlaClient]: """Create a real Nexla client for integration tests.""" creds = get_test_credentials() if not creds: pytest.skip("No test credentials available") - + return NexlaClient(**creds) - + @pytest.fixture def test_lookup_data(self) -> LookupCreate: """Create test lookup data.""" @@ -33,14 +34,14 @@ def test_lookup_data(self) -> LookupCreate: description="Test lookup created by SDK integration tests", data_defaults={"eventId": "Unknown", "description": "Unknown Event"}, emit_data_default=True, - tags=["test", "sdk", "integration"] + tags=["test", "sdk", "integration"], ) - + def test_lookup_crud_operations(self, client, test_lookup_data): """Test complete CRUD operations for lookups.""" if not client: pytest.skip("No test client available") - + created_lookup = None try: # Create lookup @@ -51,147 +52,159 @@ def test_lookup_crud_operations(self, client, test_lookup_data): assert created_lookup.map_primary_key == test_lookup_data.map_primary_key assert created_lookup.description == test_lookup_data.description assert "test" in created_lookup.tags - + # Get lookup retrieved_lookup = client.lookups.get(created_lookup.id) assert isinstance(retrieved_lookup, Lookup) assert retrieved_lookup.id == created_lookup.id assert retrieved_lookup.name == created_lookup.name - + # Update lookup update_data = LookupUpdate( name="Updated Test SDK Lookup", description="Updated description for test lookup", - emit_data_default=False + emit_data_default=False, ) updated_lookup = client.lookups.update(created_lookup.id, update_data) assert isinstance(updated_lookup, Lookup) assert updated_lookup.name == "Updated Test SDK Lookup" assert updated_lookup.description == "Updated description for test lookup" assert updated_lookup.emit_data_default is False - + # List lookups (should include our created lookup) lookups = client.lookups.list() assert isinstance(lookups, list) lookup_ids = [lookup.id for lookup in lookups] assert created_lookup.id in lookup_ids - + finally: # Clean up - delete the lookup if created_lookup: try: client.lookups.delete(created_lookup.id) except Exception as e: - print(f"Warning: Failed to clean up test lookup {created_lookup.id}: {e}") - + print( + f"Warning: Failed to clean up test lookup {created_lookup.id}: {e}" + ) + def test_lookup_entry_operations(self, client, test_lookup_data): """Test lookup entry operations.""" if not client: pytest.skip("No test client available") - + created_lookup = None try: # Create lookup first created_lookup = client.lookups.create(test_lookup_data) - + # Test upsert entries entries = [ {"eventId": "001", "description": "Login Event", "category": "Auth"}, {"eventId": "002", "description": "Logout Event", "category": "Auth"}, - {"eventId": "003", "description": "Purchase Event", "category": "Commerce"} + { + "eventId": "003", + "description": "Purchase Event", + "category": "Commerce", + }, ] - + upserted_entries = client.lookups.upsert_entries(created_lookup.id, entries) assert isinstance(upserted_entries, list) assert len(upserted_entries) == 3 - + # Test get single entry single_entry = client.lookups.get_entries(created_lookup.id, "001") assert isinstance(single_entry, list) assert len(single_entry) == 1 assert single_entry[0]["eventId"] == "001" assert single_entry[0]["description"] == "Login Event" - + # Test get multiple entries - multiple_entries = client.lookups.get_entries(created_lookup.id, ["001", "002"]) + multiple_entries = client.lookups.get_entries( + created_lookup.id, ["001", "002"] + ) assert isinstance(multiple_entries, list) assert len(multiple_entries) == 2 entry_ids = [entry["eventId"] for entry in multiple_entries] assert "001" in entry_ids assert "002" in entry_ids - + # Test delete single entry client.lookups.delete_entries(created_lookup.id, "003") - + # Verify entry was deleted (should only have 001 and 002 now) - remaining_entries = client.lookups.get_entries(created_lookup.id, ["001", "002", "003"]) + remaining_entries = client.lookups.get_entries( + created_lookup.id, ["001", "002", "003"] + ) assert len(remaining_entries) == 2 # 003 should be gone - + # Test delete multiple entries client.lookups.delete_entries(created_lookup.id, ["001", "002"]) - + finally: # Clean up if created_lookup: try: client.lookups.delete(created_lookup.id) except Exception as e: - print(f"Warning: Failed to clean up test lookup {created_lookup.id}: {e}") - + print( + f"Warning: Failed to clean up test lookup {created_lookup.id}: {e}" + ) + def test_lookup_with_expand(self, client): """Test getting lookup with expanded details.""" if not client: pytest.skip("No test client available") - + # Get first available lookup lookups = client.lookups.list() if not lookups: pytest.skip("No lookups available for testing expand functionality") - + first_lookup = lookups[0] - + # Get with expand expanded_lookup = client.lookups.get(first_lookup.id, expand=True) assert isinstance(expanded_lookup, Lookup) assert expanded_lookup.id == first_lookup.id # Expanded version may have additional details - + def test_list_with_pagination(self, client): """Test listing lookups with pagination.""" if not client: pytest.skip("No test client available") - + # Test pagination parameters page1 = client.lookups.list(page=1, per_page=5) assert isinstance(page1, list) assert len(page1) <= 5 - + # Test with access role filter filtered_lookups = client.lookups.list(access_role="owner") assert isinstance(filtered_lookups, list) - + def test_lookup_not_found_error(self, client): """Test handling of lookup not found error.""" if not client: pytest.skip("No test client available") - + # Try to get a non-existent lookup with pytest.raises(ServerError) as exc_info: client.lookups.get(999999) # Very unlikely to exist - + assert exc_info.value.status_code == 404 - + def test_lookup_validation_errors(self, client): """Test validation errors during lookup creation.""" if not client: pytest.skip("No test client available") - + # Test with missing required fields invalid_data = LookupCreate( name="", # Empty name should cause validation error data_type="string", - map_primary_key="key" + map_primary_key="key", ) - + with pytest.raises((ServerError, Exception)): - client.lookups.create(invalid_data) \ No newline at end of file + client.lookups.create(invalid_data) diff --git a/tests/integration/test_nexsets.py b/tests/integration/test_nexsets.py index 20825b3..e768f32 100644 --- a/tests/integration/test_nexsets.py +++ b/tests/integration/test_nexsets.py @@ -1,9 +1,12 @@ """Integration tests for nexsets resource.""" -import pytest + import os + +import pytest + from nexla_sdk import NexlaClient, NexlaError from nexla_sdk.exceptions import NotFoundError -from nexla_sdk.models.nexsets import NexsetCreate, NexsetUpdate, NexsetCopyOptions +from nexla_sdk.models.nexsets import NexsetCopyOptions, NexsetCreate, NexsetUpdate from tests.utils.assertions import NexlaAssertions @@ -15,11 +18,11 @@ class TestNexsetsIntegration: def client(self): """Create authenticated client for integration tests.""" service_key = os.getenv("NEXLA_SERVICE_KEY") - access_token = os.getenv("NEXLA_ACCESS_TOKEN") - + access_token = os.getenv("NEXLA_ACCESS_TOKEN") + if not service_key and not access_token: pytest.skip("No authentication credentials provided for integration tests") - + if service_key: return NexlaClient(service_key=service_key) else: @@ -43,42 +46,42 @@ def test_nexset_id(self, client): def test_nexset_crud_operations(self, client, assertions): """Test complete CRUD lifecycle for nexsets.""" created_nexset = None - + try: # Skip creation test if no parent dataset provided parent_id = os.getenv("TEST_PARENT_DATASET_ID") if not parent_id: pytest.skip("No test parent dataset ID provided for CRUD test") - + # Test CREATE create_data = NexsetCreate( name="Integration Test Dataset", - description="Created during integration testing", + description="Created during integration testing", parent_data_set_id=int(parent_id), - has_custom_transform=False + has_custom_transform=False, ) - + created_nexset = client.nexsets.create(create_data) assertions.assert_nexset_response(created_nexset) assert created_nexset.name == "Integration Test Dataset" - + # Test READ fetched_nexset = client.nexsets.get(created_nexset.id) assertions.assert_nexset_response(fetched_nexset) assert fetched_nexset.id == created_nexset.id assert fetched_nexset.name == created_nexset.name - + # Test UPDATE update_data = NexsetUpdate( name="Updated Integration Test Dataset", - description="Updated during integration testing" + description="Updated during integration testing", ) - + updated_nexset = client.nexsets.update(created_nexset.id, update_data) assertions.assert_nexset_response(updated_nexset) assert updated_nexset.name == "Updated Integration Test Dataset" assert updated_nexset.description == "Updated during integration testing" - + finally: # Test DELETE - cleanup if created_nexset: @@ -86,17 +89,19 @@ def test_nexset_crud_operations(self, client, assertions): result = client.nexsets.delete(created_nexset.id) assert "message" in result or "success" in str(result).lower() except Exception as e: - print(f"Warning: Failed to cleanup test nexset {created_nexset.id}: {e}") + print( + f"Warning: Failed to cleanup test nexset {created_nexset.id}: {e}" + ) def test_list_nexsets(self, client, assertions): """Test listing nexsets.""" # Test basic list nexsets = client.nexsets.list() assert isinstance(nexsets, list) - + for nexset in nexsets[:5]: # Check first 5 to avoid long test times assertions.assert_nexset_response(nexset) - + # Test with pagination paginated_nexsets = client.nexsets.list(page=1, per_page=5) assert isinstance(paginated_nexsets, list) @@ -114,19 +119,17 @@ def test_nexset_samples(self, client, test_nexset_id, assertions): # Test basic samples samples = client.nexsets.get_samples(test_nexset_id, count=3) assert isinstance(samples, list) - + # If samples exist, validate them for sample in samples: assertions.assert_nexset_sample(sample) - + # Test with metadata samples_with_metadata = client.nexsets.get_samples( - test_nexset_id, - count=2, - include_metadata=True + test_nexset_id, count=2, include_metadata=True ) assert isinstance(samples_with_metadata, list) - + except NexlaError as e: if "no samples available" in str(e).lower(): pytest.skip("No samples available for test nexset") @@ -139,13 +142,16 @@ def test_nexset_lifecycle_operations(self, client, test_nexset_id, assertions): # Test activate activated_nexset = client.nexsets.activate(test_nexset_id) assertions.assert_nexset_response(activated_nexset) - + # Test pause paused_nexset = client.nexsets.pause(test_nexset_id) assertions.assert_nexset_response(paused_nexset) - + except NexlaError as e: - if "not supported" in str(e).lower() or "cannot be activated" in str(e).lower(): + if ( + "not supported" in str(e).lower() + or "cannot be activated" in str(e).lower() + ): pytest.skip("Activate/pause not supported for this nexset type") else: raise @@ -153,17 +159,15 @@ def test_nexset_lifecycle_operations(self, client, test_nexset_id, assertions): def test_nexset_copy(self, client, test_nexset_id, assertions): """Test copying a nexset.""" copied_nexset = None - + try: - copy_options = NexsetCopyOptions( - copy_access_controls=False - ) - + copy_options = NexsetCopyOptions(copy_access_controls=False) + copied_nexset = client.nexsets.copy(test_nexset_id, copy_options) assertions.assert_nexset_response(copied_nexset) assert copied_nexset.id != test_nexset_id assert copied_nexset.copied_from_id == test_nexset_id - + except NexlaError as e: if "copy not supported" in str(e).lower(): pytest.skip("Copy operation not supported for this nexset") @@ -175,17 +179,19 @@ def test_nexset_copy(self, client, test_nexset_id, assertions): try: client.nexsets.delete(copied_nexset.id) except Exception as e: - print(f"Warning: Failed to cleanup copied nexset {copied_nexset.id}: {e}") + print( + f"Warning: Failed to cleanup copied nexset {copied_nexset.id}: {e}" + ) def test_nexset_not_found_error(self, client): """Test error handling for non-existent nexset.""" non_existent_id = 999999999 - + with pytest.raises((NotFoundError, NexlaError)) as exc_info: client.nexsets.get(non_existent_id) - + # Should be a 404 error - if hasattr(exc_info.value, 'status_code'): + if hasattr(exc_info.value, "status_code"): assert exc_info.value.status_code == 404 def test_nexset_validation_errors(self, client): @@ -195,16 +201,16 @@ def test_nexset_validation_errors(self, client): invalid_create_data = NexsetCreate( name="Invalid Test", parent_data_set_id=-1, # Invalid ID - has_custom_transform=False + has_custom_transform=False, ) - + with pytest.raises(NexlaError) as exc_info: client.nexsets.create(invalid_create_data) - + # Should be a 400 or 422 error - if hasattr(exc_info.value, 'status_code'): + if hasattr(exc_info.value, "status_code"): assert exc_info.value.status_code in [400, 422] - + except Exception as e: # Some validation might be caught at different levels assert "invalid" in str(e).lower() or "error" in str(e).lower() @@ -215,13 +221,15 @@ def test_list_with_pagination(self, client): page1 = client.nexsets.list(page=1, per_page=3) assert isinstance(page1, list) assert len(page1) <= 3 - + # Get second page if there are enough nexsets page2 = client.nexsets.list(page=2, per_page=3) assert isinstance(page2, list) - + # Pages should be different (if there are enough nexsets) if len(page1) == 3 and len(page2) > 0: page1_ids = {nexset.id for nexset in page1} page2_ids = {nexset.id for nexset in page2} - assert page1_ids.isdisjoint(page2_ids), "Pages should contain different nexsets" \ No newline at end of file + assert page1_ids.isdisjoint( + page2_ids + ), "Pages should contain different nexsets" diff --git a/tests/integration/test_projects.py b/tests/integration/test_projects.py index 71675ca..1ef46ac 100644 --- a/tests/integration/test_projects.py +++ b/tests/integration/test_projects.py @@ -1,9 +1,11 @@ """Integration tests for projects resource.""" + import pytest -from nexla_sdk.models.projects.responses import Project -from nexla_sdk.models.projects.requests import ProjectCreate, ProjectUpdate -from nexla_sdk.models.flows.responses import FlowResponse + from nexla_sdk.exceptions import NotFoundError, ValidationError +from nexla_sdk.models.flows.responses import FlowResponse +from nexla_sdk.models.projects.requests import ProjectCreate, ProjectUpdate +from nexla_sdk.models.projects.responses import Project @pytest.mark.integration @@ -13,68 +15,74 @@ class TestProjectsIntegration: def test_project_crud_operations(self, integration_client): """Test complete CRUD lifecycle for projects.""" created_project = None - + try: # Test create project_data = ProjectCreate( name=f"Integration Test Project {pytest.current_timestamp}", - description="Test project for integration testing" + description="Test project for integration testing", ) - + created_project = integration_client.projects.create(project_data) assert isinstance(created_project, Project) assert created_project.name == project_data.name assert created_project.description == project_data.description assert created_project.id is not None - + project_id = created_project.id - + # Test get retrieved_project = integration_client.projects.get(project_id) assert isinstance(retrieved_project, Project) assert retrieved_project.id == project_id assert retrieved_project.name == project_data.name - + # Test update update_data = ProjectUpdate( name=f"Updated Test Project {pytest.current_timestamp}", - description="Updated description" + description="Updated description", + ) + + updated_project = integration_client.projects.update( + project_id, update_data ) - - updated_project = integration_client.projects.update(project_id, update_data) assert isinstance(updated_project, Project) assert updated_project.id == project_id assert updated_project.name == update_data.name assert updated_project.description == update_data.description - + # Test list (verify project appears in list) projects = integration_client.projects.list() assert isinstance(projects, list) project_ids = [p.id for p in projects] assert project_id in project_ids - + finally: # Cleanup: delete project if it was created if created_project: try: integration_client.projects.delete(created_project.id) except Exception as e: - pytest.fail(f"Cleanup failed for project {created_project.id}: {e}", pytrace=False) + pytest.fail( + f"Cleanup failed for project {created_project.id}: {e}", + pytrace=False, + ) + def test_list_projects_with_expand(self, integration_client): """Test listing projects with expand parameter.""" # Test without expand projects = integration_client.projects.list() assert isinstance(projects, list) - + # Test with expand expanded_projects = integration_client.projects.list(expand=True) assert isinstance(expanded_projects, list) - + # If projects exist, verify expanded structure if expanded_projects: project = expanded_projects[0] - assert hasattr(project, 'data_flows') - assert hasattr(project, 'flows') + assert hasattr(project, "data_flows") + assert hasattr(project, "flows") assert isinstance(project.data_flows, list) assert isinstance(project.flows, list) @@ -83,7 +91,7 @@ def test_project_with_pagination(self, integration_client): # Test first page page1_projects = integration_client.projects.list(page=1, per_page=5) assert isinstance(page1_projects, list) - + # Test access role filter owner_projects = integration_client.projects.list(access_role="owner") assert isinstance(owner_projects, list) @@ -92,29 +100,31 @@ def test_get_project_flows(self, integration_client): """Test getting flows for a project.""" # First get a project projects = integration_client.projects.list() - + if not projects: pytest.skip("No projects available for testing flows") - + if projects: project_id = projects[0].id - + # Test get flows flows = integration_client.projects.get_flows(project_id) assert isinstance(flows, FlowResponse) - assert hasattr(flows, 'flows') + assert hasattr(flows, "flows") # Test search flows (if project has flows) - if hasattr(flows, 'flows') and flows.flows: + if hasattr(flows, "flows") and flows.flows: search_filters = [ {"field": "name", "operator": "contains", "value": "test"} ] - search_result = integration_client.projects.search_flows(project_id, search_filters) + search_result = integration_client.projects.search_flows( + project_id, search_filters + ) assert isinstance(search_result, FlowResponse) def test_project_not_found_error(self, integration_client): """Test error handling for non-existent project.""" non_existent_id = 999999 - + with pytest.raises(NotFoundError): integration_client.projects.get(non_existent_id) @@ -123,11 +133,13 @@ def test_project_not_found_error(self, integration_client): invalid_data = ProjectCreate(name="", description="Test") integration_client.projects.create(invalid_data) invalid_data = ProjectCreate(name="", description="Test") - + @pytest.fixture(scope="class") def timestamp(self): """Provide timestamp for unique naming.""" import time + return int(time.time()) import time - pytest.current_timestamp = int(time.time()) \ No newline at end of file + + pytest.current_timestamp = int(time.time()) diff --git a/tests/integration/test_sources.py b/tests/integration/test_sources.py index 2dda914..3012686 100644 --- a/tests/integration/test_sources.py +++ b/tests/integration/test_sources.py @@ -1,22 +1,23 @@ """Integration tests for sources resource with real API calls.""" -import pytest import time +import pytest + from nexla_sdk.exceptions import AuthenticationError, NotFoundError -from nexla_sdk.models.sources.responses import Source from nexla_sdk.models.sources.requests import SourceCreate, SourceUpdate +from nexla_sdk.models.sources.responses import Source @pytest.mark.integration class TestSourcesIntegration: """Integration tests for sources using real API.""" - + def test_list_sources(self, integration_client): """Test listing sources with real API.""" # Act sources = integration_client.sources.list() - + # Assert assert isinstance(sources, list) for source in sources: @@ -24,63 +25,63 @@ def test_list_sources(self, integration_client): assert source.id is not None assert source.name is not None assert source.source_type is not None - + def test_list_sources_with_pagination(self, integration_client): """Test listing sources with pagination.""" # Act page1 = integration_client.sources.list(page=1, per_page=5) page2 = integration_client.sources.list(page=2, per_page=5) - + # Assert assert isinstance(page1, list) assert isinstance(page2, list) assert len(page1) <= 5 assert len(page2) <= 5 - + # Ensure no overlap (if we have enough sources) if len(page1) == 5 and len(page2) > 0: page1_ids = {s.id for s in page1} page2_ids = {s.id for s in page2} assert page1_ids.isdisjoint(page2_ids) - + def test_list_sources_with_access_role_filter(self, integration_client): """Test listing sources filtered by access role.""" # Act owner_sources = integration_client.sources.list(access_role="owner") - + # Assert assert isinstance(owner_sources, list) for source in owner_sources: assert "owner" in source.access_roles - + def test_get_source_details(self, integration_client): """Test getting detailed source information.""" # Arrange - Get a source ID from the list sources = integration_client.sources.list(per_page=1) if not sources: pytest.skip("No sources available for testing") - + source_id = sources[0].id - + # Act source = integration_client.sources.get(source_id) detailed_source = integration_client.sources.get(source_id, expand=True) - + # Assert assert isinstance(source, Source) assert isinstance(detailed_source, Source) assert source.id == source_id assert detailed_source.id == source_id - + # Expanded version might have more information # (depends on actual API response structure) - + def test_get_nonexistent_source(self, integration_client): """Test getting a source that doesn't exist.""" # Act & Assert with pytest.raises(NotFoundError): integration_client.sources.get(999999999) # Very unlikely to exist - + @pytest.mark.skip(reason="Requires specific test credentials and cleanup") def test_create_update_delete_source_lifecycle(self, integration_client): """Test complete source lifecycle: create, update, delete.""" @@ -88,77 +89,81 @@ def test_create_update_delete_source_lifecycle(self, integration_client): # 1. Valid test credentials for a source type # 2. Proper cleanup to avoid leaving test resources # 3. Specific test environment setup - + # Create source create_data = SourceCreate( name=f"Integration Test Source {int(time.time())}", source_type="api_push", # Use a safe source type - description="Integration test source - safe to delete" + description="Integration test source - safe to delete", ) - + # Act - Create created_source = integration_client.sources.create(create_data) - + try: # Assert creation assert isinstance(created_source, Source) assert created_source.name == create_data.name assert created_source.source_type == create_data.source_type - + # Act - Update - update_data = SourceUpdate( - description="Updated integration test source" + update_data = SourceUpdate(description="Updated integration test source") + updated_source = integration_client.sources.update( + created_source.id, update_data ) - updated_source = integration_client.sources.update(created_source.id, update_data) - + # Assert update assert updated_source.description == update_data.description - + # Act - Activate/Pause (if supported) if created_source.status in ["INIT", "PAUSED"]: - activated_source = integration_client.sources.activate(created_source.id) + activated_source = integration_client.sources.activate( + created_source.id + ) assert activated_source.status == "ACTIVE" - + paused_source = integration_client.sources.pause(created_source.id) assert paused_source.status == "PAUSED" - + finally: # Cleanup - Delete the test source try: integration_client.sources.delete(created_source.id) except Exception as e: # Log but don't fail the test on cleanup issues - print(f"Warning: Failed to clean up test source {created_source.id}: {e}") - + print( + f"Warning: Failed to clean up test source {created_source.id}: {e}" + ) + def test_source_access_control(self, integration_client): """Test source access control operations.""" # Arrange - Get a source the user owns owner_sources = integration_client.sources.list(access_role="owner", per_page=1) if not owner_sources: pytest.skip("No owned sources available for access control testing") - + source_id = owner_sources[0].id - + # Act - Get current accessors accessors = integration_client.sources.get_accessors(source_id) - + # Assert assert isinstance(accessors, list) # Should at least have the owner's access assert len(accessors) >= 1 - + def test_source_audit_log(self, integration_client): """Test getting source audit log.""" # Arrange - Get a source ID sources = integration_client.sources.list(per_page=1) if not sources: pytest.skip("No sources available for audit log testing") - + source_id = sources[0].id - + # Act audit_log = integration_client.sources.get_audit_log(source_id) - + # Assert assert isinstance(audit_log, list) # Audit log might be empty for new sources, so just check structure @@ -166,18 +171,18 @@ def test_source_audit_log(self, integration_client): assert "id" in entry assert "event" in entry assert "created_at" in entry - + def test_source_pagination_consistency(self, integration_client): """Test that pagination returns consistent results.""" # Act - Get first page twice page1_first = integration_client.sources.list(page=1, per_page=3) page1_second = integration_client.sources.list(page=1, per_page=3) - + # Assert - Should be identical (assuming no concurrent modifications) assert len(page1_first) == len(page1_second) for i in range(len(page1_first)): assert page1_first[i].id == page1_second[i].id - + @pytest.mark.performance def test_list_sources_performance(self, integration_client): """Test that listing sources completes within reasonable time.""" @@ -186,21 +191,21 @@ def test_list_sources_performance(self, integration_client): start_time = time.time() sources = integration_client.sources.list(per_page=50) end_time = time.time() - + # Assert - Should complete within 5 seconds elapsed_time = end_time - start_time assert elapsed_time < 5.0, f"List sources took {elapsed_time:.2f} seconds" - + # Should return some sources (or at least not fail) assert isinstance(sources, list) - + def test_source_data_structure_consistency(self, integration_client): """Test that source data structure is consistent across API calls.""" # Arrange sources = integration_client.sources.list(per_page=5) if not sources: pytest.skip("No sources available for consistency testing") - + # Act & Assert - Check each source has consistent structure for source in sources: # Required fields should always be present @@ -209,23 +214,23 @@ def test_source_data_structure_consistency(self, integration_client): assert source.status is not None assert source.source_type is not None assert isinstance(source.access_roles, list) - + # Optional fields should be properly typed when present if source.description is not None: assert isinstance(source.description, str) - + if source.data_sets is not None: assert isinstance(source.data_sets, list) - + if source.tags is not None: assert isinstance(source.tags, list) - + def test_error_handling_with_invalid_requests(self, integration_client): """Test error handling with various invalid requests.""" # Test invalid source ID with pytest.raises(NotFoundError): integration_client.sources.get(-1) - + # Test invalid pagination parameters try: # Very large page number should either return empty list or error gracefully @@ -234,7 +239,7 @@ def test_error_handling_with_invalid_requests(self, integration_client): except Exception as e: # If it raises an exception, it should be a reasonable one assert not isinstance(e, AuthenticationError) # Should not be auth error - + @pytest.mark.slow def test_comprehensive_source_fields(self, integration_client): """Test that sources have all expected fields from the API documentation.""" @@ -242,25 +247,32 @@ def test_comprehensive_source_fields(self, integration_client): sources = integration_client.sources.list(per_page=10) if not sources: pytest.skip("No sources available for field testing") - + # Get detailed view of first source source = integration_client.sources.get(sources[0].id, expand=True) - + # Assert - Check for expected fields based on API documentation expected_fields = [ - 'id', 'name', 'status', 'source_type', 'access_roles', - 'owner', 'org', 'created_at', 'updated_at' + "id", + "name", + "status", + "source_type", + "access_roles", + "owner", + "org", + "created_at", + "updated_at", ] - + for field in expected_fields: assert hasattr(source, field), f"Source missing expected field: {field}" - + # Check owner structure if source.owner: - assert hasattr(source.owner, 'id') - assert hasattr(source.owner, 'full_name') - - # Check org structure + assert hasattr(source.owner, "id") + assert hasattr(source.owner, "full_name") + + # Check org structure if source.org: - assert hasattr(source.org, 'id') - assert hasattr(source.org, 'name') \ No newline at end of file + assert hasattr(source.org, "id") + assert hasattr(source.org, "name") diff --git a/tests/integration/test_teams.py b/tests/integration/test_teams.py index 3447913..fc9379f 100644 --- a/tests/integration/test_teams.py +++ b/tests/integration/test_teams.py @@ -1,7 +1,9 @@ """Integration tests for TeamsResource.""" -import pytest import os + +import pytest + from nexla_sdk import NexlaClient from nexla_sdk.exceptions import NotFoundError, ServerError from nexla_sdk.models.teams.requests import TeamCreate, TeamUpdate @@ -22,29 +24,29 @@ def client(self): def test_list_teams_integration(self, client): """Test listing teams against real API.""" teams = client.teams.list() - + # Should return a list (may be empty for new accounts) assert isinstance(teams, list) - + # If there are teams, verify structure for team in teams: - assert hasattr(team, 'id') - assert hasattr(team, 'name') - assert hasattr(team, 'description') - assert hasattr(team, 'owner') - assert hasattr(team, 'org') - assert hasattr(team, 'members') - assert hasattr(team, 'access_roles') + assert hasattr(team, "id") + assert hasattr(team, "name") + assert hasattr(team, "description") + assert hasattr(team, "owner") + assert hasattr(team, "org") + assert hasattr(team, "members") + assert hasattr(team, "access_roles") def test_list_teams_with_access_role_member(self, client): """Test listing teams with access_role=member parameter.""" member_teams = client.teams.list(access_role="member") - + assert isinstance(member_teams, list) - + # All returned teams should have member=True for team in member_teams: - assert hasattr(team, 'member') + assert hasattr(team, "member") assert team.member def test_get_nonexistent_team(self, client): @@ -57,7 +59,7 @@ def test_pagination_functionality(self, client): # Test first page page1 = client.teams.list(page=1, per_page=10) assert isinstance(page1, list) - + # Test second page (might be empty) page2 = client.teams.list(page=2, per_page=10) assert isinstance(page2, list) @@ -67,46 +69,45 @@ def test_team_lifecycle_integration(self, client): """Test complete team lifecycle: create, update, manage members, delete.""" # Note: This test requires the ability to create/delete teams # Use @pytest.mark.create_team to run only when explicitly requested - + try: # Create a test team create_request = TeamCreate( name="Test Integration Team", - description="A team created by integration tests" + description="A team created by integration tests", ) - + created_team = client.teams.create(create_request) team_id = created_team.id - + assert created_team.name == "Test Integration Team" assert created_team.description == "A team created by integration tests" - + # Get the created team retrieved_team = client.teams.get(team_id) assert retrieved_team.id == team_id assert retrieved_team.name == "Test Integration Team" - + # Update the team update_request = TeamUpdate( - name="Updated Integration Team", - description="Updated description" + name="Updated Integration Team", description="Updated description" ) - + updated_team = client.teams.update(team_id, update_request) assert updated_team.name == "Updated Integration Team" assert updated_team.description == "Updated description" - + # Get team members (should be empty initially) members = client.teams.get_members(team_id) assert isinstance(members, list) - + # Clean up - delete the team client.teams.delete(team_id) - + # Verify deletion with pytest.raises(NotFoundError): client.teams.get(team_id) - + except ServerError as e: if e.status_code == 403: pytest.skip("User does not have permission to create teams") @@ -116,25 +117,25 @@ def test_team_lifecycle_integration(self, client): def test_get_team_members_real_team(self, client): """Test getting members of a real team if any exist.""" teams = client.teams.list() - + if teams: # Test getting members of the first team team_id = teams[0].id members = client.teams.get_members(team_id) - + assert isinstance(members, list) - + # If there are members, verify their structure for member in members: - assert hasattr(member, 'id') - assert hasattr(member, 'email') - assert hasattr(member, 'admin') + assert hasattr(member, "id") + assert hasattr(member, "email") + assert hasattr(member, "admin") assert isinstance(member.admin, bool) def test_error_handling_real_api(self, client): """Test error handling with real API responses.""" # Test various error scenarios - + # Invalid team ID format (if the API validates this) try: client.teams.get(-1) @@ -145,49 +146,49 @@ def test_error_handling_real_api(self, client): def test_team_validation_with_real_api(self, client): """Test that team responses match expected model structure.""" teams = client.teams.list() - + for team in teams: # Verify all required fields are present assert team.id is not None assert team.name is not None - assert hasattr(team, 'description') + assert hasattr(team, "description") assert team.owner is not None assert team.org is not None - assert hasattr(team, 'member') - assert hasattr(team, 'access_roles') + assert hasattr(team, "member") + assert hasattr(team, "access_roles") assert isinstance(team.access_roles, list) - + # Verify owner structure - assert hasattr(team.owner, 'id') - assert hasattr(team.owner, 'full_name') - assert hasattr(team.owner, 'email') - + assert hasattr(team.owner, "id") + assert hasattr(team.owner, "full_name") + assert hasattr(team.owner, "email") + # Verify org structure - assert hasattr(team.org, 'id') - assert hasattr(team.org, 'name') - + assert hasattr(team.org, "id") + assert hasattr(team.org, "name") + # Verify members structure - assert hasattr(team, 'members') + assert hasattr(team, "members") assert isinstance(team.members, list) - + # Verify each member structure for member in team.members: - assert hasattr(member, 'id') - assert hasattr(member, 'email') - assert hasattr(member, 'admin') + assert hasattr(member, "id") + assert hasattr(member, "email") + assert hasattr(member, "admin") assert isinstance(member.admin, bool) def test_team_access_roles_validation(self, client): """Test that team access roles are properly validated.""" teams = client.teams.list() - + for team in teams: # Access roles should be a list of strings assert isinstance(team.access_roles, list) - + # Common access roles from the API docs # valid_roles = ["owner", "admin", "collaborator", "operator", "member"] # Not used - + for role in team.access_roles: assert isinstance(role, str) # Note: Not all roles may be in our predefined list @@ -196,16 +197,16 @@ def test_team_access_roles_validation(self, client): def test_expand_parameter_functionality(self, client): """Test expand parameter functionality.""" teams = client.teams.list() - + if teams: team_id = teams[0].id - + # Get team without expand team_normal = client.teams.get(team_id) - + # Get team with expand team_expanded = client.teams.get(team_id, expand=True) - + # Both should have the same basic structure assert team_normal.id == team_expanded.id assert team_normal.name == team_expanded.name @@ -213,17 +214,17 @@ def test_expand_parameter_functionality(self, client): def test_list_vs_get_consistency(self, client): """Test that list and get operations return consistent data.""" teams = client.teams.list() - + if teams: # Get the first team from the list list_team = teams[0] team_id = list_team.id - + # Get the same team individually get_team = client.teams.get(team_id) - + # Verify key fields match assert list_team.id == get_team.id assert list_team.name == get_team.name assert list_team.owner.id == get_team.owner.id - assert list_team.org.id == get_team.org.id \ No newline at end of file + assert list_team.org.id == get_team.org.id diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 24e718c..d754026 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -1,7 +1,9 @@ """Integration tests for UsersResource.""" -import pytest import os + +import pytest + from nexla_sdk import NexlaClient from nexla_sdk.exceptions import NotFoundError, ServerError @@ -21,19 +23,19 @@ def client(self): def test_list_users_integration(self, client): """Test listing users against real API.""" users = client.users.list() - + # Should return at least the current user assert len(users) >= 1 - + # Verify structure user = users[0] - assert hasattr(user, 'id') - assert hasattr(user, 'email') - assert hasattr(user, 'full_name') - assert hasattr(user, 'default_org') - + assert hasattr(user, "id") + assert hasattr(user, "email") + assert hasattr(user, "full_name") + assert hasattr(user, "default_org") + # Verify API key is included - assert hasattr(user, 'api_key') + assert hasattr(user, "api_key") def test_list_users_with_access_role_all(self, client): """Test listing all users with access_role=all parameter.""" @@ -51,23 +53,23 @@ def test_list_users_with_access_role_all(self, client): def test_list_users_with_expand(self, client): """Test listing users with expand parameter.""" users = client.users.list(expand=True) - + assert len(users) >= 1 - + # When expanded, might include account summary user = users[0] - assert hasattr(user, 'id') - assert hasattr(user, 'email') + assert hasattr(user, "id") + assert hasattr(user, "email") def test_get_current_user(self, client): """Test getting current user details.""" # Get the current user from list users = client.users.list() current_user_id = users[0].id - + # Get user details user = client.users.get(current_user_id) - + assert user.id == current_user_id assert user.email is not None assert user.full_name is not None @@ -77,12 +79,12 @@ def test_get_user_with_expand(self, client): """Test getting user with expand parameter.""" users = client.users.list() user_id = users[0].id - + user = client.users.get(user_id, expand=True) - + assert user.id == user_id # Expanded user might have account summary - assert hasattr(user, 'account_summary') + assert hasattr(user, "account_summary") def test_get_nonexistent_user(self, client): """Test getting a user that doesn't exist.""" @@ -92,7 +94,7 @@ def test_get_nonexistent_user(self, client): def test_get_settings(self, client): """Test getting user settings.""" settings = client.users.get_settings() - + # Settings might be empty, but should be a list assert isinstance(settings, list) @@ -100,13 +102,10 @@ def test_get_account_metrics(self, client): """Test getting account metrics.""" users = client.users.list() user_id = users[0].id - + # Get metrics for the last 30 days - metrics = client.users.get_account_metrics( - user_id, - from_date="2023-01-01" - ) - + metrics = client.users.get_account_metrics(user_id, from_date="2023-01-01") + # Should return some metrics structure assert isinstance(metrics, dict) @@ -114,9 +113,9 @@ def test_get_dashboard_metrics(self, client): """Test getting dashboard metrics.""" users = client.users.list() user_id = users[0].id - + metrics = client.users.get_dashboard_metrics(user_id) - + # Should return metrics structure assert isinstance(metrics, dict) @@ -124,13 +123,11 @@ def test_get_daily_metrics(self, client): """Test getting daily metrics.""" users = client.users.list() user_id = users[0].id - + metrics = client.users.get_daily_metrics( - user_id, - resource_type="SOURCE", - from_date="2023-01-01" + user_id, resource_type="SOURCE", from_date="2023-01-01" ) - + # Should return metrics data assert isinstance(metrics, dict) @@ -139,7 +136,7 @@ def test_pagination_functionality(self, client): # Test first page page1 = client.users.list(page=1, per_page=1) assert len(page1) >= 0 # Might be 0 or 1 user - + # If there are users, verify pagination works if len(page1) > 0: # Try getting a second page (might be empty) @@ -149,7 +146,7 @@ def test_pagination_functionality(self, client): def test_error_handling_real_api(self, client): """Test error handling with real API responses.""" # Test various error scenarios that might occur - + # Invalid user ID format (if the API validates this) with pytest.raises((NotFoundError, ServerError)) as exc_info: client.users.get(-1) @@ -159,29 +156,30 @@ def test_error_handling_real_api(self, client): # Optionally verify the error message or status code if isinstance(exc_info.value, ServerError): assert exc_info.value.status_code in [400, 404] + def test_user_validation_with_real_api(self, client): """Test that user responses match expected model structure.""" users = client.users.list() - + if users: user = users[0] - + # Verify all required fields are present assert user.id is not None assert user.email is not None assert user.full_name is not None assert user.default_org is not None - assert hasattr(user, 'status') - assert hasattr(user, 'impersonated') - + assert hasattr(user, "status") + assert hasattr(user, "impersonated") + # Verify org_memberships structure - assert hasattr(user, 'org_memberships') + assert hasattr(user, "org_memberships") assert isinstance(user.org_memberships, list) - + # If there are org memberships, verify their structure for membership in user.org_memberships: - assert hasattr(membership, 'id') - assert hasattr(membership, 'name') - assert hasattr(membership, 'org_membership_status') + assert hasattr(membership, "id") + assert hasattr(membership, "name") + assert hasattr(membership, "org_membership_status") # Should have API key after our model enhancement - assert hasattr(membership, 'api_key') \ No newline at end of file + assert hasattr(membership, "api_key") diff --git a/tests/property/test_credentials.py b/tests/property/test_credentials.py index c6bc5e7..335d307 100644 --- a/tests/property/test_credentials.py +++ b/tests/property/test_credentials.py @@ -1,12 +1,13 @@ """Property-based tests for credentials using hypothesis.""" import pytest -from hypothesis import given, strategies as st, settings, assume +from hypothesis import assume, given, settings +from hypothesis import strategies as st from hypothesis.strategies import composite from pydantic import ValidationError -from nexla_sdk.models.credentials.responses import Credential from nexla_sdk.models.credentials.requests import CredentialCreate, ProbeTreeRequest +from nexla_sdk.models.credentials.responses import Credential # Custom strategies for generating test data @@ -16,44 +17,87 @@ def credential_dict(draw): return { "id": draw(st.integers(min_value=1, max_value=999999)), # Avoid whitespace/control characters to prevent stripping - "name": draw(st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=200)), - "credentials_type": draw(st.sampled_from([ - "s3", "postgres", "mysql", "bigquery", "snowflake", "azure_blb", - "gcs", "ftp", "dropbox", "rest", "kafka" - ])), + "name": draw( + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=200, + ) + ), + "credentials_type": draw( + st.sampled_from( + [ + "s3", + "postgres", + "mysql", + "bigquery", + "snowflake", + "azure_blb", + "gcs", + "ftp", + "dropbox", + "rest", + "kafka", + ] + ) + ), "description": draw(st.one_of(st.none(), st.text(max_size=500))), - "verified_status": draw(st.one_of(st.none(), st.sampled_from(["VERIFIED", "UNVERIFIED", "FAILED"]))), - "credentials_version": draw(st.one_of(st.none(), st.text(min_size=1, max_size=10))), + "verified_status": draw( + st.one_of(st.none(), st.sampled_from(["VERIFIED", "UNVERIFIED", "FAILED"])) + ), + "credentials_version": draw( + st.one_of(st.none(), st.text(min_size=1, max_size=10)) + ), "managed": draw(st.booleans()), "tags": draw( st.lists( - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=50), + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=50, + ), max_size=10, ) ), - "created_at": draw(st.one_of(st.none(), st.datetimes().map(lambda dt: dt.isoformat() + "Z"))), - "updated_at": draw(st.one_of(st.none(), st.datetimes().map(lambda dt: dt.isoformat() + "Z"))), + "created_at": draw( + st.one_of(st.none(), st.datetimes().map(lambda dt: dt.isoformat() + "Z")) + ), + "updated_at": draw( + st.one_of(st.none(), st.datetimes().map(lambda dt: dt.isoformat() + "Z")) + ), } @composite def credential_create_dict(draw): """Generate random credential creation data.""" - credentials_type = draw(st.sampled_from(["s3", "postgres", "mysql", "rest", "bigquery"])) - + credentials_type = draw( + st.sampled_from(["s3", "postgres", "mysql", "rest", "bigquery"]) + ) + base_data = { # Avoid whitespace/control characters to prevent stripping - "name": draw(st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=200)), + "name": draw( + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=200, + ) + ), "credentials_type": credentials_type, "description": draw(st.one_of(st.none(), st.text(max_size=500))), } - + # Add type-specific credentials if credentials_type == "s3": base_data["credentials"] = { "access_key_id": draw(st.text(min_size=10, max_size=50)), "secret_key": draw(st.text(min_size=20, max_size=100)), - "region": draw(st.sampled_from(["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"])), + "region": draw( + st.sampled_from( + ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"] + ) + ), } elif credentials_type in ["postgres", "mysql"]: base_data["credentials"] = { @@ -68,7 +112,7 @@ def credential_create_dict(draw): "api_key": draw(st.text(min_size=10, max_size=100)), "endpoint": draw(st.text(min_size=10, max_size=200)), } - + return base_data @@ -76,10 +120,10 @@ def credential_create_dict(draw): def probe_tree_request_dict(draw): """Generate random probe tree request data.""" depth = draw(st.integers(min_value=1, max_value=10)) - + # Choose between file system or database probing probe_type = draw(st.sampled_from(["filesystem", "database"])) - + if probe_type == "filesystem": return { "depth": depth, @@ -96,7 +140,7 @@ def probe_tree_request_dict(draw): @pytest.mark.unit class TestCredentialModelProperties: """Property-based tests for credential models.""" - + @given(credential_dict()) @settings(max_examples=100, deadline=1000) def test_credential_model_handles_various_inputs(self, credential_data): @@ -104,85 +148,91 @@ def test_credential_model_handles_various_inputs(self, credential_data): # Act & Assert - should either validate successfully or raise ValidationError try: credential = Credential(**credential_data) - + # If validation succeeds, verify basic properties assert credential.id == credential_data["id"] assert credential.name == credential_data["name"] assert credential.credentials_type == credential_data["credentials_type"] - + # Verify serialization works credential_dict = credential.to_dict() assert isinstance(credential_dict, dict) assert credential_dict["id"] == credential_data["id"] - + # Verify JSON serialization works credential_json = credential.to_json() assert isinstance(credential_json, str) assert str(credential_data["id"]) in credential_json - + except ValidationError: # Validation errors are expected for some random inputs pass - + @given(credential_create_dict()) @settings(max_examples=50, deadline=1000) def test_credential_create_model_validation(self, create_data): """Test CredentialCreate model with various inputs.""" try: credential_create = CredentialCreate(**create_data) - + # If validation succeeds, verify required fields assert credential_create.name == create_data["name"] assert credential_create.credentials_type == create_data["credentials_type"] - + # Verify serialization create_dict = credential_create.to_dict() assert isinstance(create_dict, dict) assert create_dict["name"] == create_data["name"] - + except ValidationError: # Some random inputs may not be valid pass - + @given(probe_tree_request_dict()) @settings(max_examples=50, deadline=1000) def test_probe_tree_request_validation(self, request_data): """Test ProbeTreeRequest with various inputs.""" try: probe_request = ProbeTreeRequest(**request_data) - + # If validation succeeds, verify depth is preserved assert probe_request.depth == request_data["depth"] assert probe_request.depth > 0 - + except ValidationError: # Some combinations may not be valid pass - - @given(st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=500)) + + @given( + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=500, + ) + ) def test_credential_name_property(self, name): """Test that credential names are handled correctly.""" - minimal_data = { - "id": 1, - "name": name, - "credentials_type": "s3" - } - + minimal_data = {"id": 1, "name": name, "credentials_type": "s3"} + try: credential = Credential(**minimal_data) assert credential.name == name - + # Test string representation includes name str_repr = str(credential) assert name in str_repr - + except ValidationError: # Some names might be invalid (e.g., very long strings) pass - + @given( st.lists( - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=50), + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=50, + ), min_size=0, max_size=20, ) @@ -193,12 +243,12 @@ def test_credential_tags_property(self, tags): "id": 1, "name": "Test Credential", "credentials_type": "s3", - "tags": tags + "tags": tags, } - + credential = Credential(**credential_data) assert credential.tags == tags - + # Test serialization includes tags credential_dict = credential.to_dict() assert credential_dict["tags"] == tags @@ -207,58 +257,58 @@ def test_credential_tags_property(self, tags): @pytest.mark.unit class TestCredentialInvariants: """Test invariants that should hold for all credential operations.""" - + @given(credential_dict()) @settings(max_examples=50) def test_serialization_round_trip(self, credential_data): """Test that serialization and deserialization preserve data.""" assume(credential_data.get("name")) # Assume name is not empty - + try: # Create credential from dict credential = Credential(**credential_data) - + # Serialize to dict serialized = credential.to_dict() - + # Create new credential from serialized data credential2 = Credential(**serialized) - + # Verify key fields are preserved assert credential2.id == credential.id assert credential2.name == credential.name assert credential2.credentials_type == credential.credentials_type - + except ValidationError: # Skip invalid inputs pass - + @given(credential_create_dict()) @settings(max_examples=30) def test_create_request_always_has_required_fields(self, create_data): """Test that valid create requests always have required fields.""" try: credential_create = CredentialCreate(**create_data) - + # Required fields should always be present assert credential_create.name is not None assert credential_create.name != "" assert credential_create.credentials_type is not None assert credential_create.credentials_type != "" - + except ValidationError: # Invalid inputs are acceptable pass - + @given(st.integers(min_value=1, max_value=10)) def test_probe_tree_depth_bounds(self, depth): """Test that probe tree requests handle depth correctly.""" probe_request = ProbeTreeRequest(depth=depth) - + # Depth should be preserved and positive assert probe_request.depth == depth assert probe_request.depth > 0 - + # Serialization should preserve depth serialized = probe_request.to_dict() assert serialized["depth"] == depth @@ -267,23 +317,19 @@ def test_probe_tree_depth_bounds(self, depth): @pytest.mark.unit class TestCredentialEdgeCases: """Test edge cases and boundary conditions.""" - + def test_credential_with_empty_optional_fields(self): """Test credential with all optional fields empty.""" - minimal_data = { - "id": 1, - "name": "Minimal Credential", - "credentials_type": "s3" - } - + minimal_data = {"id": 1, "name": "Minimal Credential", "credentials_type": "s3"} + credential = Credential(**minimal_data) - + # Optional fields should have sensible defaults assert credential.description is None assert credential.tags == [] assert credential.managed is False assert credential.access_roles is None - + @given( st.text( alphabet=st.characters(min_codepoint=33, max_codepoint=126), @@ -297,35 +343,37 @@ def test_credential_with_long_strings(self, long_text): "id": 1, "name": long_text[:200], # Limit name to reasonable size "credentials_type": "s3", - "description": long_text + "description": long_text, } - + try: credential = Credential(**credential_data) assert len(credential.name) <= 200 assert credential.description == long_text - + except ValidationError: # Very long strings might be rejected pass - + @given(st.lists(st.integers(), min_size=0, max_size=100)) def test_credential_with_various_list_sizes(self, int_list): """Test credential with various list sizes for access_roles.""" # Convert integers to valid role strings valid_roles = ["owner", "admin", "collaborator", "operator"] - access_roles = [valid_roles[i % len(valid_roles)] for i in int_list[:10]] # Limit size - + access_roles = [ + valid_roles[i % len(valid_roles)] for i in int_list[:10] + ] # Limit size + credential_data = { "id": 1, "name": "Test Credential", "credentials_type": "s3", - "access_roles": access_roles + "access_roles": access_roles, } - + credential = Credential(**credential_data) assert credential.access_roles == access_roles - + def test_credential_with_null_values(self): """Test credential handling of explicit None values.""" credential_data = { @@ -337,11 +385,11 @@ def test_credential_with_null_values(self): "verified_at": None, "tags": None, # Should be converted to empty list } - + credential = Credential(**credential_data) - + # None values should be handled gracefully assert credential.description is None assert credential.verified_status is None assert credential.verified_at is None - assert credential.tags == [] # Should be converted to empty list + assert credential.tags == [] # Should be converted to empty list diff --git a/tests/property/test_destinations.py b/tests/property/test_destinations.py index 33f253f..795c97d 100644 --- a/tests/property/test_destinations.py +++ b/tests/property/test_destinations.py @@ -1,20 +1,22 @@ """Property-based tests for destinations resource.""" + import os -import pytest -from hypothesis import given, strategies as st, settings, HealthCheck from unittest.mock import MagicMock +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + from nexla_sdk.models.destinations import DestinationCreate, DestinationUpdate +from tests.utils.assertions import NexlaAssertions from tests.utils.fixtures import create_test_client from tests.utils.mock_builders import MockResponseBuilder -from tests.utils.assertions import NexlaAssertions - # Suppress function-scoped fixture warnings for CI SETTINGS = settings( suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=3 if os.getenv("CI") else 10, - deadline=None + deadline=None, ) @@ -34,10 +36,23 @@ def assertions(self): @given( destination_name=st.text(min_size=1, max_size=100).filter(lambda x: x.strip()), destination_description=st.one_of(st.none(), st.text(max_size=500)), - sink_type=st.sampled_from(["s3", "gcs", "mysql", "postgres", "snowflake", "bigquery", "kafka", "dropbox"]) + sink_type=st.sampled_from( + [ + "s3", + "gcs", + "mysql", + "postgres", + "snowflake", + "bigquery", + "kafka", + "dropbox", + ] + ), ) @SETTINGS - def test_create_destination_serialization(self, client, destination_name, destination_description, sink_type): + def test_create_destination_serialization( + self, client, destination_name, destination_description, sink_type + ): """Test destination creation with various input combinations.""" # Arrange create_data = DestinationCreate( @@ -45,19 +60,21 @@ def test_create_destination_serialization(self, client, destination_name, destin sink_type=sink_type, data_credentials_id=1, data_set_id=1, - description=destination_description + description=destination_description, + ) + + mock_response = MockResponseBuilder.destination( + { + "name": destination_name.strip(), + "sink_type": sink_type, + "description": destination_description, + } ) - - mock_response = MockResponseBuilder.destination({ - "name": destination_name.strip(), - "sink_type": sink_type, - "description": destination_description - }) client.http_client.request = MagicMock(return_value=mock_response) - + # Act destination = client.destinations.create(create_data) - + # Assert assert destination.name == destination_name.strip() assert destination.sink_type == sink_type @@ -65,12 +82,16 @@ def test_create_destination_serialization(self, client, destination_name, destin assert destination.description == destination_description @given( - response_data=st.fixed_dictionaries({ - "id": st.integers(min_value=1, max_value=999999), - "name": st.text(min_size=1, max_size=200), - "sink_type": st.sampled_from(["s3", "gcs", "mysql", "postgres", "snowflake", "bigquery", "kafka"]), - "status": st.sampled_from(["ACTIVE", "PAUSED", "DRAFT", "ERROR"]) - }) + response_data=st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=999999), + "name": st.text(min_size=1, max_size=200), + "sink_type": st.sampled_from( + ["s3", "gcs", "mysql", "postgres", "snowflake", "bigquery", "kafka"] + ), + "status": st.sampled_from(["ACTIVE", "PAUSED", "DRAFT", "ERROR"]), + } + ) ) @SETTINGS def test_destination_response_parsing(self, client, assertions, response_data): @@ -78,10 +99,10 @@ def test_destination_response_parsing(self, client, assertions, response_data): # Arrange mock_response = MockResponseBuilder.destination(response_data) client.http_client.request = MagicMock(return_value=mock_response) - + # Act destination = client.destinations.get(response_data["id"]) - + # Assert assertions.assert_destination_response(destination) assert destination.id == response_data["id"] @@ -90,25 +111,31 @@ def test_destination_response_parsing(self, client, assertions, response_data): @given( destinations_data=st.lists( - st.fixed_dictionaries({ - "id": st.integers(min_value=1, max_value=999999), - "name": st.text(min_size=1, max_size=100), - "sink_type": st.sampled_from(["s3", "mysql", "bigquery"]) - }), + st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=999999), + "name": st.text(min_size=1, max_size=100), + "sink_type": st.sampled_from(["s3", "mysql", "bigquery"]), + } + ), min_size=0, - max_size=5 + max_size=5, ) ) @SETTINGS - def test_list_destinations_response_parsing(self, client, assertions, destinations_data): + def test_list_destinations_response_parsing( + self, client, assertions, destinations_data + ): """Test parsing list destinations responses with various data combinations.""" # Arrange - mock_destinations = [MockResponseBuilder.destination(data) for data in destinations_data] + mock_destinations = [ + MockResponseBuilder.destination(data) for data in destinations_data + ] client.http_client.request = MagicMock(return_value=mock_destinations) - + # Act destinations = client.destinations.list() - + # Assert assert len(destinations) == len(destinations_data) for i, destination in enumerate(destinations): @@ -117,13 +144,19 @@ def test_list_destinations_response_parsing(self, client, assertions, destinatio assert destination.sink_type == destinations_data[i]["sink_type"] @given( - name=st.one_of(st.none(), st.text(min_size=1, max_size=200).filter(lambda x: x.strip())), + name=st.one_of( + st.none(), st.text(min_size=1, max_size=200).filter(lambda x: x.strip()) + ), description=st.one_of(st.none(), st.text(max_size=500)), - data_credentials_id=st.one_of(st.none(), st.integers(min_value=1, max_value=999999)), - data_set_id=st.one_of(st.none(), st.integers(min_value=1, max_value=999999)) + data_credentials_id=st.one_of( + st.none(), st.integers(min_value=1, max_value=999999) + ), + data_set_id=st.one_of(st.none(), st.integers(min_value=1, max_value=999999)), ) @SETTINGS - def test_update_destination_with_various_data(self, client, assertions, name, description, data_credentials_id, data_set_id): + def test_update_destination_with_various_data( + self, client, assertions, name, description, data_credentials_id, data_set_id + ): """Test destination updates with various data combinations.""" # Arrange destination_id = 12345 @@ -131,9 +164,9 @@ def test_update_destination_with_various_data(self, client, assertions, name, de name=name, description=description, data_credentials_id=data_credentials_id, - data_set_id=data_set_id + data_set_id=data_set_id, ) - + # Build expected response expected_response = {"id": destination_id} if name and name.strip(): @@ -144,13 +177,13 @@ def test_update_destination_with_various_data(self, client, assertions, name, de expected_response["data_credentials_id"] = data_credentials_id if data_set_id: expected_response["data_set_id"] = data_set_id - + mock_response = MockResponseBuilder.destination(expected_response) client.http_client.request = MagicMock(return_value=mock_response) - + # Act destination = client.destinations.update(destination_id, update_data) - + # Assert assertions.assert_destination_response(destination) assert destination.id == destination_id @@ -158,138 +191,162 @@ def test_update_destination_with_various_data(self, client, assertions, name, de assert destination.name == name.strip() @given( - sink_config_data=st.fixed_dictionaries({ - "data_format": st.sampled_from(["json", "csv", "parquet", "avro"]), - "path": st.text(min_size=1, max_size=200), - "mapping": st.fixed_dictionaries({ - "mode": st.sampled_from(["auto", "manual"]), - "tracker_mode": st.just("NONE") - }) - }) + sink_config_data=st.fixed_dictionaries( + { + "data_format": st.sampled_from(["json", "csv", "parquet", "avro"]), + "path": st.text(min_size=1, max_size=200), + "mapping": st.fixed_dictionaries( + { + "mode": st.sampled_from(["auto", "manual"]), + "tracker_mode": st.just("NONE"), + } + ), + } + ) ) - @SETTINGS - def test_destination_with_various_sink_configs(self, client, assertions, sink_config_data): + @SETTINGS + def test_destination_with_various_sink_configs( + self, client, assertions, sink_config_data + ): """Test destinations with various sink configuration combinations.""" # Arrange - mock_response = MockResponseBuilder.destination({ - "id": 12345, - "sink_config": sink_config_data - }) + mock_response = MockResponseBuilder.destination( + {"id": 12345, "sink_config": sink_config_data} + ) client.http_client.request = MagicMock(return_value=mock_response) - + # Act destination = client.destinations.get(12345) - + # Assert assertions.assert_destination_response(destination) - if hasattr(destination, 'sink_config') and destination.sink_config: - assert destination.sink_config["data_format"] == sink_config_data["data_format"] - assert destination.sink_config["mapping"]["mode"] == sink_config_data["mapping"]["mode"] + if hasattr(destination, "sink_config") and destination.sink_config: + assert ( + destination.sink_config["data_format"] + == sink_config_data["data_format"] + ) + assert ( + destination.sink_config["mapping"]["mode"] + == sink_config_data["mapping"]["mode"] + ) @given( destination_name=st.text( - min_size=1, + min_size=1, max_size=255, - alphabet=st.characters(min_codepoint=32, max_codepoint=126) + alphabet=st.characters(min_codepoint=32, max_codepoint=126), ).filter(lambda x: x.strip()), - sink_type=st.sampled_from(["s3", "mysql", "postgres", "snowflake"]) + sink_type=st.sampled_from(["s3", "mysql", "postgres", "snowflake"]), ) @SETTINGS - def test_destination_name_edge_cases(self, client, assertions, destination_name, sink_type): + def test_destination_name_edge_cases( + self, client, assertions, destination_name, sink_type + ): """Test destination creation with various name edge cases.""" # Arrange create_data = DestinationCreate( name=destination_name.strip(), sink_type=sink_type, data_credentials_id=1, - data_set_id=1 + data_set_id=1, + ) + + mock_response = MockResponseBuilder.destination( + {"name": destination_name.strip(), "sink_type": sink_type} ) - - mock_response = MockResponseBuilder.destination({ - "name": destination_name.strip(), - "sink_type": sink_type - }) client.http_client.request = MagicMock(return_value=mock_response) - + # Act destination = client.destinations.create(create_data) - + # Assert assertions.assert_destination_response(destination) assert destination.name == destination_name.strip() assert destination.sink_type == sink_type @given( - data_set_info=st.fixed_dictionaries({ - "id": st.integers(min_value=1, max_value=999999), - "name": st.text(min_size=1, max_size=100), - "status": st.sampled_from(["ACTIVE", "PAUSED", "DRAFT"]) - }), - data_map_info=st.fixed_dictionaries({ - "id": st.integers(min_value=1, max_value=999999), - "owner_id": st.integers(min_value=1, max_value=1000), - "org_id": st.integers(min_value=1, max_value=100), - "name": st.text(min_size=1, max_size=100), - "description": st.text(min_size=1, max_size=200), - "public": st.booleans(), - "created_at": st.just("2023-01-01T12:00:00.000Z"), - "updated_at": st.just("2023-01-01T12:00:00.000Z") - }) + data_set_info=st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=999999), + "name": st.text(min_size=1, max_size=100), + "status": st.sampled_from(["ACTIVE", "PAUSED", "DRAFT"]), + } + ), + data_map_info=st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=999999), + "owner_id": st.integers(min_value=1, max_value=1000), + "org_id": st.integers(min_value=1, max_value=100), + "name": st.text(min_size=1, max_size=100), + "description": st.text(min_size=1, max_size=200), + "public": st.booleans(), + "created_at": st.just("2023-01-01T12:00:00.000Z"), + "updated_at": st.just("2023-01-01T12:00:00.000Z"), + } + ), ) @SETTINGS - def test_destination_with_nested_objects(self, client, assertions, data_set_info, data_map_info): + def test_destination_with_nested_objects( + self, client, assertions, data_set_info, data_map_info + ): """Test destinations with various nested object combinations.""" # Arrange - mock_response = MockResponseBuilder.destination({ - "id": 12345, - "data_set": data_set_info, - "data_map": data_map_info - }) + mock_response = MockResponseBuilder.destination( + {"id": 12345, "data_set": data_set_info, "data_map": data_map_info} + ) client.http_client.request = MagicMock(return_value=mock_response) - + # Act destination = client.destinations.get(12345) - + # Assert assertions.assert_destination_response(destination) - if hasattr(destination, 'data_set') and destination.data_set: + if hasattr(destination, "data_set") and destination.data_set: assert destination.data_set.id == data_set_info["id"] assert destination.data_set.status == data_set_info["status"] - if hasattr(destination, 'data_map') and destination.data_map: + if hasattr(destination, "data_map") and destination.data_map: assert destination.data_map.id == data_map_info["id"] assert destination.data_map.public == data_map_info["public"] @given( - vendor_data=st.fixed_dictionaries({ - "id": st.integers(min_value=1, max_value=1000), - "name": st.text(min_size=1, max_size=100), - "type": st.text(min_size=1, max_size=50) - }), - vendor_endpoint=st.fixed_dictionaries({ - "id": st.integers(min_value=1, max_value=1000), - "name": st.text(min_size=1, max_size=100), - "url": st.text(min_size=10, max_size=200) - }) + vendor_data=st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=1000), + "name": st.text(min_size=1, max_size=100), + "type": st.text(min_size=1, max_size=50), + } + ), + vendor_endpoint=st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=1000), + "name": st.text(min_size=1, max_size=100), + "url": st.text(min_size=10, max_size=200), + } + ), ) @SETTINGS - def test_destination_vendor_configuration(self, client, assertions, vendor_data, vendor_endpoint): + def test_destination_vendor_configuration( + self, client, assertions, vendor_data, vendor_endpoint + ): """Test destinations with various vendor configurations.""" # Arrange - mock_response = MockResponseBuilder.destination({ - "id": 12345, - "vendor": vendor_data, - "vendor_endpoint": vendor_endpoint, - "has_template": True - }) + mock_response = MockResponseBuilder.destination( + { + "id": 12345, + "vendor": vendor_data, + "vendor_endpoint": vendor_endpoint, + "has_template": True, + } + ) client.http_client.request = MagicMock(return_value=mock_response) - + # Act destination = client.destinations.get(12345) - + # Assert assertions.assert_destination_response(destination) - if hasattr(destination, 'vendor') and destination.vendor: + if hasattr(destination, "vendor") and destination.vendor: assert destination.vendor["id"] == vendor_data["id"] assert destination.vendor["name"] == vendor_data["name"] - if hasattr(destination, 'vendor_endpoint') and destination.vendor_endpoint: - assert destination.vendor_endpoint["id"] == vendor_endpoint["id"] \ No newline at end of file + if hasattr(destination, "vendor_endpoint") and destination.vendor_endpoint: + assert destination.vendor_endpoint["id"] == vendor_endpoint["id"] diff --git a/tests/property/test_flows.py b/tests/property/test_flows.py index e1c5356..c7168c2 100644 --- a/tests/property/test_flows.py +++ b/tests/property/test_flows.py @@ -1,16 +1,17 @@ """Property-based tests for flows resource.""" -from hypothesis import given, strategies as st, settings -from hypothesis.stateful import RuleBasedStateMachine, rule, invariant, Bundle + +from typing import Any, Dict, Optional from unittest.mock import MagicMock -from typing import Dict, Any, Optional -from nexla_sdk.models.flows.responses import FlowResponse -from nexla_sdk.models.flows.requests import FlowCopyOptions +from hypothesis import given, settings +from hypothesis import strategies as st +from hypothesis.stateful import Bundle, RuleBasedStateMachine, invariant, rule +from nexla_sdk.models.flows.requests import FlowCopyOptions +from nexla_sdk.models.flows.responses import FlowResponse from tests.utils.fixtures import create_test_client from tests.utils.mock_builders import MockDataFactory - # Strategies for flow-specific types flow_status_strategy = st.sampled_from(["ACTIVE", "PAUSED", "DRAFT", "ERROR", "INIT"]) resource_type_strategy = st.sampled_from(["data_sources", "data_sets", "data_sinks"]) @@ -18,69 +19,80 @@ class TestFlowsProperty: """Property-based tests for flows resource.""" - + @given( depth=st.integers(min_value=1, max_value=5), - children_per_node=st.integers(min_value=0, max_value=3) + children_per_node=st.integers(min_value=0, max_value=3), ) @settings(max_examples=50) def test_flow_node_structure_invariants(self, depth, children_per_node): """Test that flow node structures maintain invariants.""" # Create a mock flow structure factory = MockDataFactory() - - def create_node_with_children(parent_id: Optional[int], current_depth: int) -> Dict[str, Any]: + + def create_node_with_children( + parent_id: Optional[int], current_depth: int + ) -> Dict[str, Any]: node_id = factory.fake.random_int(1, 10000) node = { "id": node_id, "parent_data_set_id": parent_id, - "data_source": {"id": factory.fake.random_int(1, 10000)} if parent_id is None else None, + "data_source": ( + {"id": factory.fake.random_int(1, 10000)} + if parent_id is None + else None + ), "data_sinks": [], "sharers": {"sharers": [], "external_sharers": []}, - "children": [] + "children": [], } - + if current_depth < depth: for _ in range(children_per_node): child = create_node_with_children(node_id, current_depth + 1) node["children"].append(child) else: # Leaf nodes might have sinks - node["data_sinks"] = [factory.fake.random_int(1, 10000) for _ in range(factory.fake.random_int(0, 2))] - + node["data_sinks"] = [ + factory.fake.random_int(1, 10000) + for _ in range(factory.fake.random_int(0, 2)) + ] + return node - + # Create root node root = create_node_with_children(None, 1) - + # Validate invariants self._validate_node_invariants(root) - - def _validate_node_invariants(self, node: Dict[str, Any], parent_id: Optional[int] = None) -> None: + + def _validate_node_invariants( + self, node: Dict[str, Any], parent_id: Optional[int] = None + ) -> None: """Validate flow node invariants.""" # Every node must have an ID assert "id" in node assert isinstance(node["id"], int) assert node["id"] > 0 - + # Parent relationship must be consistent assert node.get("parent_data_set_id") == parent_id - + # Root nodes (no parent) should have data source if parent_id is None: assert node.get("data_source") is not None - + # Children must be a list assert isinstance(node.get("children", []), list) - + # Recursively validate children for child in node.get("children", []): self._validate_node_invariants(child, node["id"]) - + @given( include_elements=st.booleans(), num_flows=st.integers(min_value=0, max_value=5), - include_metrics=st.booleans() + include_metrics=st.booleans(), ) @settings(max_examples=50) def test_flow_response_parsing(self, include_elements, num_flows, include_metrics): @@ -88,52 +100,60 @@ def test_flow_response_parsing(self, include_elements, num_flows, include_metric # Arrange client = create_test_client() factory = MockDataFactory() - + # Create mock response mock_response = { "flows": [factory.create_mock_flow_node() for _ in range(num_flows)] } - + if include_elements: - mock_response.update({ - "data_sources": [factory.create_mock_source() for _ in range(2)], - "data_sets": [factory.create_mock_nexset() for _ in range(3)], - "data_sinks": [factory.create_mock_destination() for _ in range(2)], - "data_credentials": [factory.create_mock_credential() for _ in range(1)] - }) - + mock_response.update( + { + "data_sources": [factory.create_mock_source() for _ in range(2)], + "data_sets": [factory.create_mock_nexset() for _ in range(3)], + "data_sinks": [factory.create_mock_destination() for _ in range(2)], + "data_credentials": [ + factory.create_mock_credential() for _ in range(1) + ], + } + ) + if include_metrics: - mock_response["metrics"] = [factory.create_mock_flow_metrics() for _ in range(3)] - + mock_response["metrics"] = [ + factory.create_mock_flow_metrics() for _ in range(3) + ] + client.http_client.request = MagicMock(return_value=mock_response) - + # Act flows = client.flows.list() - + # Assert assert len(flows) == 1 flow = flows[0] assert isinstance(flow, FlowResponse) assert len(flow.flows) == num_flows - + if include_elements: assert flow.data_sources is not None assert flow.data_sets is not None assert flow.data_sinks is not None assert flow.data_credentials is not None - + if include_metrics: assert flow.metrics is not None assert len(flow.metrics) == 3 - + @given( reuse_credentials=st.booleans(), copy_access=st.booleans(), copy_dependent=st.booleans(), owner_id=st.one_of(st.none(), st.integers(min_value=1, max_value=10000)), - org_id=st.one_of(st.none(), st.integers(min_value=1, max_value=10000)) + org_id=st.one_of(st.none(), st.integers(min_value=1, max_value=10000)), ) - def test_flow_copy_options_validation(self, reuse_credentials, copy_access, copy_dependent, owner_id, org_id): + def test_flow_copy_options_validation( + self, reuse_credentials, copy_access, copy_dependent, owner_id, org_id + ): """Test that flow copy options are properly validated.""" # Act & Assert - should not raise options = FlowCopyOptions( @@ -141,16 +161,16 @@ def test_flow_copy_options_validation(self, reuse_credentials, copy_access, copy copy_access_controls=copy_access, copy_dependent_data_flows=copy_dependent, owner_id=owner_id, - org_id=org_id + org_id=org_id, ) - + # Verify all fields are set correctly assert options.reuse_data_credentials == reuse_credentials assert options.copy_access_controls == copy_access assert options.copy_dependent_data_flows == copy_dependent assert options.owner_id == owner_id assert options.org_id == org_id - + @given(st.data()) @settings(max_examples=20) def test_flow_api_parameter_combinations(self, data): @@ -158,20 +178,24 @@ def test_flow_api_parameter_combinations(self, data): # Arrange client = create_test_client() factory = MockDataFactory() - + # Generate random parameters flows_only = data.draw(st.booleans()) include_metrics = data.draw(st.booleans()) page = data.draw(st.one_of(st.none(), st.integers(min_value=1, max_value=100))) - per_page = data.draw(st.one_of(st.none(), st.integers(min_value=1, max_value=100))) - + per_page = data.draw( + st.one_of(st.none(), st.integers(min_value=1, max_value=100)) + ) + # Create appropriate mock response - mock_response = factory.create_mock_flow_response(include_elements=not flows_only) + mock_response = factory.create_mock_flow_response( + include_elements=not flows_only + ) if include_metrics: mock_response["metrics"] = [factory.create_mock_flow_metrics()] - + client.http_client.request = MagicMock(return_value=mock_response) - + # Build kwargs kwargs = {} if flows_only: @@ -182,18 +206,18 @@ def test_flow_api_parameter_combinations(self, data): kwargs["page"] = page if per_page: kwargs["per_page"] = per_page - + # Act flows = client.flows.list(**kwargs) - + # Assert assert len(flows) == 1 assert isinstance(flows[0], FlowResponse) - + # Verify parameters were passed correctly _, _, call_kwargs = client.http_client.request.mock_calls[0] params = call_kwargs["params"] - + if flows_only: assert params.get("flows_only") == 1 if include_metrics: @@ -206,19 +230,19 @@ def test_flow_api_parameter_combinations(self, data): class FlowStateMachine(RuleBasedStateMachine): """Stateful testing for flow operations.""" - + def __init__(self): super().__init__() self.client = create_test_client() self.factory = MockDataFactory() self.flows = {} # Track flows by ID self.flow_states = {} # Track flow states - + # Setup default mock self.client.http_client.request = MagicMock() - - flows_bundle = Bundle('flows') - + + flows_bundle = Bundle("flows") + @rule(target=flows_bundle) def create_flow(self): """Create a new flow (via list operation).""" @@ -226,56 +250,56 @@ def create_flow(self): flow_id = self.factory.fake.random_int(1000, 9999) mock_response = self.factory.create_mock_flow_response() mock_response["flows"][0]["id"] = flow_id - + self.client.http_client.request.return_value = mock_response - + # List flows (simulating flow creation) flows = self.client.flows.list() - + # Track the flow self.flows[flow_id] = flows[0] self.flow_states[flow_id] = "ACTIVE" - + return flow_id - + @rule(flow_id=flows_bundle) def activate_flow(self, flow_id): """Activate a flow.""" mock_response = self.factory.create_mock_flow_response() mock_response["flows"][0]["id"] = flow_id - + self.client.http_client.request.return_value = mock_response - + # Activate self.client.flows.activate(flow_id) self.flow_states[flow_id] = "ACTIVE" - + @rule(flow_id=flows_bundle) def pause_flow(self, flow_id): """Pause a flow.""" mock_response = self.factory.create_mock_flow_response() mock_response["flows"][0]["id"] = flow_id - + self.client.http_client.request.return_value = mock_response - + # Pause self.client.flows.pause(flow_id) self.flow_states[flow_id] = "PAUSED" - + @rule( flow_id=flows_bundle, resource_type=resource_type_strategy, - resource_id=st.integers(min_value=1, max_value=10000) + resource_id=st.integers(min_value=1, max_value=10000), ) def get_flow_by_resource(self, flow_id, resource_type, resource_id): """Get flow by resource.""" mock_response = self.factory.create_mock_flow_response() self.client.http_client.request.return_value = mock_response - + # Get by resource flow = self.client.flows.get_by_resource(resource_type, resource_id) assert isinstance(flow, FlowResponse) - + @invariant() def flow_states_consistent(self): """Check that flow states remain consistent.""" @@ -286,4 +310,4 @@ def flow_states_consistent(self): # Run the state machine tests -TestFlowStateMachine = FlowStateMachine.TestCase \ No newline at end of file +TestFlowStateMachine = FlowStateMachine.TestCase diff --git a/tests/property/test_lookups.py b/tests/property/test_lookups.py index 4771ca3..6ba9c3a 100644 --- a/tests/property/test_lookups.py +++ b/tests/property/test_lookups.py @@ -1,28 +1,39 @@ """Property-based tests for lookups resource.""" -from hypothesis import given, strategies as st, settings, HealthCheck + from unittest.mock import MagicMock -from nexla_sdk.models.lookups.responses import Lookup -from nexla_sdk.models.lookups.requests import LookupCreate, LookupUpdate +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from nexla_sdk.models.lookups.requests import LookupCreate, LookupUpdate +from nexla_sdk.models.lookups.responses import Lookup from tests.utils.fixtures import create_test_client from tests.utils.mock_builders import MockDataFactory - # Strategies for lookup-specific types data_type_strategy = st.sampled_from(["string", "integer", "number", "boolean"]) primary_key_strategy = st.text( - alphabet=st.characters(whitelist_categories=('Ll', 'Lu', 'Lt', 'Lm', 'Lo', 'Nd'), - min_codepoint=ord('a'), max_codepoint=ord('z')), - min_size=1, - max_size=20 + alphabet=st.characters( + whitelist_categories=("Ll", "Lu", "Lt", "Lm", "Lo", "Nd"), + min_codepoint=ord("a"), + max_codepoint=ord("z"), + ), + min_size=1, + max_size=20, ).filter(lambda x: x.isidentifier()) -lookup_name_strategy = st.text( - alphabet=st.characters(whitelist_categories=('Ll', 'Lu', 'Lt', 'Lm', 'Lo', 'Nd', 'Pc', 'Pd'), - min_codepoint=32, max_codepoint=126), - min_size=1, - max_size=50 -).map(lambda x: x.strip()).filter(lambda x: len(x) > 0) +lookup_name_strategy = ( + st.text( + alphabet=st.characters( + whitelist_categories=("Ll", "Lu", "Lt", "Lm", "Lo", "Nd", "Pc", "Pd"), + min_codepoint=32, + max_codepoint=126, + ), + min_size=1, + max_size=50, + ) + .map(lambda x: x.strip()) + .filter(lambda x: len(x) > 0) +) @st.composite @@ -30,21 +41,25 @@ def lookup_data_defaults_strategy(draw): """Generate valid data defaults dictionary.""" primary_key = draw(primary_key_strategy) data_type = draw(data_type_strategy) - + defaults = {primary_key: "default_key"} - + # Add some additional default fields for i in range(draw(st.integers(0, 3))): - field_name = draw(st.text(min_size=1, max_size=20).filter(lambda x: x.isidentifier())) + field_name = draw( + st.text(min_size=1, max_size=20).filter(lambda x: x.isidentifier()) + ) if data_type == "string": defaults[field_name] = draw(st.text(max_size=50)) elif data_type == "integer": defaults[field_name] = draw(st.integers(-1000, 1000)) elif data_type == "number": - defaults[field_name] = draw(st.floats(-1000.0, 1000.0, allow_nan=False, allow_infinity=False)) + defaults[field_name] = draw( + st.floats(-1000.0, 1000.0, allow_nan=False, allow_infinity=False) + ) else: # boolean defaults[field_name] = draw(st.booleans()) - + return defaults @@ -53,7 +68,7 @@ def lookup_create_strategy(draw): """Generate valid LookupCreate instances.""" primary_key = draw(primary_key_strategy) data_type = draw(data_type_strategy) - + return LookupCreate( name=draw(lookup_name_strategy), data_type=data_type, @@ -61,7 +76,7 @@ def lookup_create_strategy(draw): description=draw(st.one_of(st.none(), st.text(max_size=200))), data_defaults=draw(lookup_data_defaults_strategy()), emit_data_default=draw(st.booleans()), - tags=draw(st.lists(st.text(min_size=1, max_size=20), max_size=5)) + tags=draw(st.lists(st.text(min_size=1, max_size=20), max_size=5)), ) @@ -71,7 +86,7 @@ def lookup_response_strategy(draw): factory = MockDataFactory() primary_key = draw(primary_key_strategy) data_type = draw(data_type_strategy) - + return factory.create_mock_lookup( id=draw(st.integers(1, 10000)), name=draw(lookup_name_strategy), @@ -80,7 +95,7 @@ def lookup_response_strategy(draw): public=draw(st.booleans()), managed=draw(st.booleans()), emit_data_default=draw(st.booleans()), - use_versioning=draw(st.booleans()) + use_versioning=draw(st.booleans()), ) @@ -89,23 +104,28 @@ def lookup_entry_strategy(draw): """Generate valid lookup entries.""" key = draw(st.text(min_size=1, max_size=50)) value = draw(st.text(max_size=100)) - + entry = {"key": key, "value": value} - + # Add optional fields if draw(st.booleans()): entry["description"] = draw(st.text(max_size=100)) if draw(st.booleans()): entry["category"] = draw(st.text(min_size=1, max_size=30)) - + return entry class TestLookupsProperty: """Property-based tests for lookups resource.""" - + @given(lookup_create_strategy()) - @settings(suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.filter_too_much]) + @settings( + suppress_health_check=[ + HealthCheck.function_scoped_fixture, + HealthCheck.filter_too_much, + ] + ) def test_create_lookup_serialization(self, lookup_data): """Test that LookupCreate serializes correctly.""" # Arrange @@ -113,138 +133,161 @@ def test_create_lookup_serialization(self, lookup_data): mock_response = MockDataFactory().create_mock_lookup( name=lookup_data.name, data_type=lookup_data.data_type, - map_primary_key=lookup_data.map_primary_key + map_primary_key=lookup_data.map_primary_key, ) mock_client.http_client.request = MagicMock(return_value=mock_response) - + # Act result = mock_client.lookups.create(lookup_data) - + # Assert assert isinstance(result, Lookup) assert result.name == lookup_data.name assert result.data_type == lookup_data.data_type assert result.map_primary_key == lookup_data.map_primary_key - + # Verify serialization call_args = mock_client.http_client.request.call_args - json_data = call_args[1]['json'] - assert json_data['name'] == lookup_data.name - assert json_data['data_type'] == lookup_data.data_type - assert json_data['map_primary_key'] == lookup_data.map_primary_key - + json_data = call_args[1]["json"] + assert json_data["name"] == lookup_data.name + assert json_data["data_type"] == lookup_data.data_type + assert json_data["map_primary_key"] == lookup_data.map_primary_key + @given(st.integers(1, 10000), lookup_response_strategy()) - @settings(suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.filter_too_much]) + @settings( + suppress_health_check=[ + HealthCheck.function_scoped_fixture, + HealthCheck.filter_too_much, + ] + ) def test_lookup_response_parsing(self, lookup_id, lookup_data): """Test that lookup responses are parsed correctly.""" # Arrange mock_client = create_test_client() - lookup_data['id'] = lookup_id + lookup_data["id"] = lookup_id mock_client.http_client.request = MagicMock(return_value=lookup_data) - + # Act result = mock_client.lookups.get(lookup_id) - + # Assert assert isinstance(result, Lookup) assert result.id == lookup_id # Name might be stripped, so compare the stripped version - assert result.name == lookup_data['name'].strip() - assert result.data_type == lookup_data['data_type'] - assert result.map_primary_key == lookup_data['map_primary_key'] - assert result.public == lookup_data['public'] - assert result.managed == lookup_data['managed'] - + assert result.name == lookup_data["name"].strip() + assert result.data_type == lookup_data["data_type"] + assert result.map_primary_key == lookup_data["map_primary_key"] + assert result.public == lookup_data["public"] + assert result.managed == lookup_data["managed"] + @given(st.lists(lookup_response_strategy(), min_size=0, max_size=10)) - @settings(suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.filter_too_much]) + @settings( + suppress_health_check=[ + HealthCheck.function_scoped_fixture, + HealthCheck.filter_too_much, + ] + ) def test_list_lookups_response_parsing(self, lookups_data): """Test that list responses are parsed correctly.""" # Arrange mock_client = create_test_client() for i, lookup_data in enumerate(lookups_data): - lookup_data['id'] = i + 1 + lookup_data["id"] = i + 1 mock_client.http_client.request = MagicMock(return_value=lookups_data) - + # Act result = mock_client.lookups.list() - + # Assert assert isinstance(result, list) assert len(result) == len(lookups_data) for lookup, expected in zip(result, lookups_data): assert isinstance(lookup, Lookup) - assert lookup.id == expected['id'] + assert lookup.id == expected["id"] # Name might be stripped, so compare the stripped version - assert lookup.name == expected['name'].strip() - + assert lookup.name == expected["name"].strip() + @given( st.integers(1, 10000), - st.lists(lookup_entry_strategy(), min_size=1, max_size=10) + st.lists(lookup_entry_strategy(), min_size=1, max_size=10), + ) + @settings( + suppress_health_check=[ + HealthCheck.function_scoped_fixture, + HealthCheck.filter_too_much, + ] ) - @settings(suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.filter_too_much]) def test_upsert_entries_with_various_data(self, lookup_id, entries): """Test upserting entries with various data types.""" # Arrange mock_client = create_test_client() mock_client.http_client.request = MagicMock(return_value=entries) - + # Act result = mock_client.lookups.upsert_entries(lookup_id, entries) - + # Assert assert result == entries - + # Verify request format call_args = mock_client.http_client.request.call_args - json_data = call_args[1]['json'] - assert json_data['entries'] == entries - + json_data = call_args[1]["json"] + assert json_data["entries"] == entries + @given( st.integers(1, 10000), st.one_of( st.text(min_size=1, max_size=50), - st.lists(st.text(min_size=1, max_size=50), min_size=1, max_size=5) - ) + st.lists(st.text(min_size=1, max_size=50), min_size=1, max_size=5), + ), + ) + @settings( + suppress_health_check=[ + HealthCheck.function_scoped_fixture, + HealthCheck.filter_too_much, + ] ) - @settings(suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.filter_too_much]) def test_get_entries_with_various_keys(self, lookup_id, entry_keys): """Test getting entries with various key formats.""" # Arrange mock_client = create_test_client() - mock_entries = [{"key": str(key), "value": f"value_{key}"} for key in - (entry_keys if isinstance(entry_keys, list) else [entry_keys])] + mock_entries = [ + {"key": str(key), "value": f"value_{key}"} + for key in (entry_keys if isinstance(entry_keys, list) else [entry_keys]) + ] mock_client.http_client.request = MagicMock(return_value=mock_entries) - + # Act result = mock_client.lookups.get_entries(lookup_id, entry_keys) - + # Assert assert isinstance(result, list) assert len(result) == len(mock_entries) - + # Verify URL format call_args = mock_client.http_client.request.call_args url = call_args[0][1] # Second positional argument is the URL if isinstance(entry_keys, list): - expected_keys = ','.join(str(key) for key in entry_keys) + expected_keys = ",".join(str(key) for key in entry_keys) else: expected_keys = str(entry_keys) assert expected_keys in url - + @given(st.text(min_size=1, max_size=100)) - @settings(suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.filter_too_much]) + @settings( + suppress_health_check=[ + HealthCheck.function_scoped_fixture, + HealthCheck.filter_too_much, + ] + ) def test_lookup_name_edge_cases(self, name): """Test lookup creation with various name edge cases.""" # Arrange mock_client = create_test_client() - lookup_data = LookupCreate( - name=name, - data_type="string", - map_primary_key="key" - ) + lookup_data = LookupCreate(name=name, data_type="string", map_primary_key="key") mock_response = MockDataFactory().create_mock_lookup(name=name.strip()) mock_client.http_client.request = MagicMock(return_value=mock_response) - + # Act & Assert try: result = mock_client.lookups.create(lookup_data) @@ -253,38 +296,42 @@ def test_lookup_name_edge_cases(self, name): except Exception: # Some names might be invalid, which is acceptable pass - + @given( st.integers(1, 10000), st.dictionaries( st.text(min_size=1, max_size=20).filter(lambda x: x.isidentifier()), st.one_of(st.text(max_size=50), st.integers(-100, 100), st.booleans()), min_size=1, - max_size=5 - ) + max_size=5, + ), + ) + @settings( + suppress_health_check=[ + HealthCheck.function_scoped_fixture, + HealthCheck.filter_too_much, + ] ) - @settings(suppress_health_check=[HealthCheck.function_scoped_fixture, HealthCheck.filter_too_much]) def test_data_defaults_serialization(self, lookup_id, data_defaults): """Test that data_defaults are serialized correctly.""" # Arrange mock_client = create_test_client() update_data = LookupUpdate(data_defaults=data_defaults) mock_response = MockDataFactory().create_mock_lookup( - id=lookup_id, - data_defaults=data_defaults + id=lookup_id, data_defaults=data_defaults ) mock_client.http_client.request = MagicMock(return_value=mock_response) - + # Act result = mock_client.lookups.update(lookup_id, update_data) - + # Assert assert isinstance(result, Lookup) - + # Verify serialization call_args = mock_client.http_client.request.call_args - json_data = call_args[1]['json'] - assert json_data['data_defaults'] == data_defaults + json_data = call_args[1]["json"] + assert json_data["data_defaults"] == data_defaults # Remove the state machine test for now as it's too complex @@ -296,4 +343,4 @@ def test_data_defaults_serialization(self, lookup_id, data_defaults): # Reduce the number of examples for faster testing in CI settings.register_profile("ci", max_examples=3, deadline=3000) settings.register_profile("dev", max_examples=10, deadline=5000) -settings.load_profile("ci") \ No newline at end of file +settings.load_profile("ci") diff --git a/tests/property/test_nexsets.py b/tests/property/test_nexsets.py index 4842f9c..378bc8e 100644 --- a/tests/property/test_nexsets.py +++ b/tests/property/test_nexsets.py @@ -1,20 +1,22 @@ """Property-based tests for nexsets resource.""" + import os -import pytest -from hypothesis import given, strategies as st, settings, HealthCheck from unittest.mock import MagicMock +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + from nexla_sdk.models.nexsets import NexsetCreate, NexsetUpdate +from tests.utils.assertions import NexlaAssertions from tests.utils.fixtures import create_test_client from tests.utils.mock_builders import MockResponseBuilder -from tests.utils.assertions import NexlaAssertions - # Suppress function-scoped fixture warnings for CI SETTINGS = settings( suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=3 if os.getenv("CI") else 10, - deadline=None + deadline=None, ) @@ -34,45 +36,53 @@ def assertions(self): @given( nexset_name=st.text(min_size=1, max_size=100).filter(lambda x: x.strip()), nexset_description=st.one_of(st.none(), st.text(max_size=500)), - parent_id=st.integers(min_value=1, max_value=99999) + parent_id=st.integers(min_value=1, max_value=99999), ) @SETTINGS - def test_create_nexset_serialization(self, client, nexset_name, nexset_description, parent_id): + def test_create_nexset_serialization( + self, client, nexset_name, nexset_description, parent_id + ): """Test nexset creation with various input combinations.""" # Arrange create_data = NexsetCreate( name=nexset_name.strip(), description=nexset_description, parent_data_set_id=parent_id, - has_custom_transform=True + has_custom_transform=True, + ) + + mock_response = MockResponseBuilder.nexset( + { + "name": nexset_name.strip(), + "description": nexset_description, + "parent_data_sets": [{"id": parent_id, "owner_id": 1, "org_id": 1}], + } ) - - mock_response = MockResponseBuilder.nexset({ - "name": nexset_name.strip(), - "description": nexset_description, - "parent_data_sets": [{"id": parent_id, "owner_id": 1, "org_id": 1}] - }) - + client.http_client.request = MagicMock(return_value=mock_response) - + # Act nexset = client.nexsets.create(create_data) - + # Assert assert nexset.name == nexset_name.strip() assert nexset.description == nexset_description - + # Verify serialization serialized = create_data.to_dict() assert serialized["name"] == nexset_name.strip() assert serialized["parent_data_set_id"] == parent_id @given( - response_data=st.fixed_dictionaries({ - "id": st.integers(min_value=1, max_value=999999), - "name": st.one_of(st.none(), st.text(min_size=1, max_size=200)), - "status": st.sampled_from(["ACTIVE", "PAUSED", "DRAFT", "PROCESSING", "ERROR"]) - }) + response_data=st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=999999), + "name": st.one_of(st.none(), st.text(min_size=1, max_size=200)), + "status": st.sampled_from( + ["ACTIVE", "PAUSED", "DRAFT", "PROCESSING", "ERROR"] + ), + } + ) ) @SETTINGS def test_nexset_response_parsing(self, client, assertions, response_data): @@ -80,10 +90,10 @@ def test_nexset_response_parsing(self, client, assertions, response_data): # Arrange mock_response = MockResponseBuilder.nexset(response_data) client.http_client.request = MagicMock(return_value=mock_response) - + # Act nexset = client.nexsets.get(response_data["id"]) - + # Assert assertions.assert_nexset_response(nexset) assert nexset.id == response_data["id"] @@ -93,13 +103,15 @@ def test_nexset_response_parsing(self, client, assertions, response_data): @given( nexsets_data=st.lists( - st.fixed_dictionaries({ - "id": st.integers(min_value=1, max_value=999999), - "name": st.one_of(st.none(), st.text(min_size=1, max_size=100)), - "status": st.sampled_from(["ACTIVE", "PAUSED", "DRAFT"]) - }), + st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=999999), + "name": st.one_of(st.none(), st.text(min_size=1, max_size=100)), + "status": st.sampled_from(["ACTIVE", "PAUSED", "DRAFT"]), + } + ), min_size=0, - max_size=10 + max_size=10, ) ) @SETTINGS @@ -108,10 +120,10 @@ def test_list_nexsets_response_parsing(self, client, assertions, nexsets_data): # Arrange mock_response = [MockResponseBuilder.nexset(data) for data in nexsets_data] client.http_client.request = MagicMock(return_value=mock_response) - + # Act nexsets = client.nexsets.list() - + # Assert assert len(nexsets) == len(nexsets_data) for i, nexset in enumerate(nexsets): @@ -119,44 +131,50 @@ def test_list_nexsets_response_parsing(self, client, assertions, nexsets_data): assert nexset.id == nexsets_data[i]["id"] @given( - update_name=st.one_of(st.none(), st.text(min_size=1, max_size=100).filter(lambda x: x.strip())), + update_name=st.one_of( + st.none(), st.text(min_size=1, max_size=100).filter(lambda x: x.strip()) + ), update_description=st.one_of(st.none(), st.text(max_size=500)), tags=st.one_of( st.none(), st.lists( st.text( - min_size=1, - max_size=30, - alphabet=st.characters(min_codepoint=32, max_codepoint=126) - ).filter(lambda x: x.strip()), - min_size=0, - max_size=5 - ) - ) + min_size=1, + max_size=30, + alphabet=st.characters(min_codepoint=32, max_codepoint=126), + ).filter(lambda x: x.strip()), + min_size=0, + max_size=5, + ), + ), ) @SETTINGS - def test_update_nexset_with_various_data(self, client, update_name, update_description, tags): + def test_update_nexset_with_various_data( + self, client, update_name, update_description, tags + ): """Test updating nexsets with various field combinations.""" # Arrange nexset_id = 1001 update_data = NexsetUpdate( name=update_name.strip() if update_name else None, description=update_description, - tags=tags + tags=tags, + ) + + mock_response = MockResponseBuilder.nexset( + { + "id": nexset_id, + "name": update_name.strip() if update_name else "Existing Name", + "description": update_description, + "tags": tags or [], + } ) - - mock_response = MockResponseBuilder.nexset({ - "id": nexset_id, - "name": update_name.strip() if update_name else "Existing Name", - "description": update_description, - "tags": tags or [] - }) - + client.http_client.request = MagicMock(return_value=mock_response) - + # Act nexset = client.nexsets.update(nexset_id, update_data) - + # Assert assert nexset.id == nexset_id if update_name: @@ -166,26 +184,28 @@ def test_update_nexset_with_various_data(self, client, update_name, update_descr assert nexset.tags == (tags or []) @given( - sample_data=st.fixed_dictionaries({ - "field1": st.text(min_size=1, max_size=50), - "field2": st.integers(), - "field3": st.one_of(st.none(), st.floats(allow_nan=False, allow_infinity=False)) - }) + sample_data=st.fixed_dictionaries( + { + "field1": st.text(min_size=1, max_size=50), + "field2": st.integers(), + "field3": st.one_of( + st.none(), st.floats(allow_nan=False, allow_infinity=False) + ), + } + ) ) @SETTINGS def test_nexset_samples_with_various_data(self, client, assertions, sample_data): """Test nexset samples with different data structures.""" # Arrange nexset_id = 1001 - mock_sample = MockResponseBuilder.nexset_sample({ - "raw_message": sample_data - }) + mock_sample = MockResponseBuilder.nexset_sample({"raw_message": sample_data}) mock_response = [mock_sample] client.http_client.request = MagicMock(return_value=mock_response) - + # Act samples = client.nexsets.get_samples(nexset_id, count=1) - + # Assert assert len(samples) == 1 sample = samples[0] @@ -196,56 +216,62 @@ def test_nexset_samples_with_various_data(self, client, assertions, sample_data) @given( count=st.integers(min_value=1, max_value=100), include_metadata=st.booleans(), - live=st.booleans() + live=st.booleans(), ) @SETTINGS def test_get_samples_parameters(self, client, count, include_metadata, live): """Test get_samples with various parameter combinations.""" # Arrange nexset_id = 1001 - mock_response = [MockResponseBuilder.nexset_sample() for _ in range(min(count, 5))] + mock_response = [ + MockResponseBuilder.nexset_sample() for _ in range(min(count, 5)) + ] client.http_client.request = MagicMock(return_value=mock_response) - + # Act - client.nexsets.get_samples(nexset_id, count=count, include_metadata=include_metadata, live=live) - + client.nexsets.get_samples( + nexset_id, count=count, include_metadata=include_metadata, live=live + ) + # Assert expected_params = { - 'count': count, - 'include_metadata': include_metadata, - 'live': live + "count": count, + "include_metadata": include_metadata, + "live": live, } client.http_client.request.assert_called_once_with( - 'GET', - f'{client.api_url}/data_sets/{nexset_id}/samples', + "GET", + f"{client.api_url}/data_sets/{nexset_id}/samples", headers={ "Accept": "application/vnd.nexla.api.v1+json", "Content-Type": "application/json", - "Authorization": "Bearer test-token" + "Authorization": "Bearer test-token", }, - params=expected_params + params=expected_params, ) @given( nexset_name=st.text(min_size=1, max_size=200), - flow_type=st.one_of(st.none(), st.sampled_from(["batch", "streaming", "real_time"])), - status=st.sampled_from(["ACTIVE", "PAUSED", "DRAFT", "PROCESSING"]) + flow_type=st.one_of( + st.none(), st.sampled_from(["batch", "streaming", "real_time"]) + ), + status=st.sampled_from(["ACTIVE", "PAUSED", "DRAFT", "PROCESSING"]), ) @SETTINGS - def test_nexset_name_and_type_combinations(self, client, assertions, nexset_name, flow_type, status): + def test_nexset_name_and_type_combinations( + self, client, assertions, nexset_name, flow_type, status + ): """Test nexsets with various name and type combinations.""" # Arrange - mock_response = MockResponseBuilder.nexset({ - "name": nexset_name.strip(), - "flow_type": flow_type, - "status": status - }) - + mock_response = MockResponseBuilder.nexset( + {"name": nexset_name.strip(), "flow_type": flow_type, "status": status} + ) + client.http_client.request = MagicMock(return_value=mock_response) - + # Act nexset = client.nexsets.get(1001) - + # Assert assertions.assert_nexset_response(nexset) assert nexset.name == nexset_name.strip() @@ -256,44 +282,43 @@ def test_nexset_name_and_type_combinations(self, client, assertions, nexset_name @given( copy_access_controls=st.booleans(), owner_id=st.one_of(st.none(), st.integers(min_value=1, max_value=9999)), - org_id=st.one_of(st.none(), st.integers(min_value=1, max_value=999)) + org_id=st.one_of(st.none(), st.integers(min_value=1, max_value=999)), ) @SETTINGS - def test_copy_options_serialization(self, client, copy_access_controls, owner_id, org_id): + def test_copy_options_serialization( + self, client, copy_access_controls, owner_id, org_id + ): """Test copy options with various parameter combinations.""" # Arrange nexset_id = 1001 from nexla_sdk.models.nexsets import NexsetCopyOptions - + copy_options = NexsetCopyOptions( - copy_access_controls=copy_access_controls, - owner_id=owner_id, - org_id=org_id + copy_access_controls=copy_access_controls, owner_id=owner_id, org_id=org_id + ) + + mock_response = MockResponseBuilder.nexset( + {"id": 1002, "copied_from_id": nexset_id} ) - - mock_response = MockResponseBuilder.nexset({ - "id": 1002, - "copied_from_id": nexset_id - }) - + client.http_client.request = MagicMock(return_value=mock_response) - + # Act client.nexsets.copy(nexset_id, copy_options) - + # Assert serialized = copy_options.to_dict() assert serialized["copy_access_controls"] == copy_access_controls assert serialized.get("owner_id") == owner_id assert serialized.get("org_id") == org_id - + client.http_client.request.assert_called_once_with( - 'POST', - f'{client.api_url}/data_sets/{nexset_id}/copy', + "POST", + f"{client.api_url}/data_sets/{nexset_id}/copy", headers={ "Accept": "application/vnd.nexla.api.v1+json", "Content-Type": "application/json", - "Authorization": "Bearer test-token" + "Authorization": "Bearer test-token", }, - json=serialized - ) \ No newline at end of file + json=serialized, + ) diff --git a/tests/property/test_projects.py b/tests/property/test_projects.py index c242c6f..522dfc3 100644 --- a/tests/property/test_projects.py +++ b/tests/property/test_projects.py @@ -1,19 +1,27 @@ """Property-based tests for projects resource.""" + import os -import pytest from unittest.mock import MagicMock -from hypothesis import given, strategies as st, settings, HealthCheck + +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from nexla_sdk.models.projects.requests import ( + ProjectCreate, + ProjectFlowIdentifier, + ProjectFlowList, + ProjectUpdate, +) from nexla_sdk.models.projects.responses import Project, ProjectDataFlow -from nexla_sdk.models.projects.requests import ProjectCreate, ProjectUpdate, ProjectFlowList, ProjectFlowIdentifier from tests.utils.fixtures import create_test_client from tests.utils.mock_builders import MockDataFactory - # Suppress function-scoped fixture warnings for CI SETTINGS = settings( suppress_health_check=[HealthCheck.function_scoped_fixture], max_examples=3 if os.getenv("CI") else 10, - deadline=None + deadline=None, ) @@ -26,30 +34,42 @@ def mock_client(self): return create_test_client() @given( - name=st.text(min_size=1, max_size=100, alphabet=st.characters(min_codepoint=33, max_codepoint=126)), - description=st.text(min_size=0, max_size=500, alphabet=st.characters(min_codepoint=33, max_codepoint=126)), - flows_count=st.integers(min_value=0, max_value=50) + name=st.text( + min_size=1, + max_size=100, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + description=st.text( + min_size=0, + max_size=500, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + flows_count=st.integers(min_value=0, max_value=50), ) @SETTINGS - def test_create_project_serialization(self, mock_client, name, description, flows_count): + def test_create_project_serialization( + self, mock_client, name, description, flows_count + ): """Test project creation with various input combinations.""" # Arrange factory = MockDataFactory() - mock_project = factory.create_mock_project(name=name, description=description, flows_count=flows_count) + mock_project = factory.create_mock_project( + name=name, description=description, flows_count=flows_count + ) mock_client.http_client.request = MagicMock(return_value=mock_project) - + project_data = ProjectCreate( name=name, description=description, data_flows=[ - ProjectFlowIdentifier(data_source_id=i) + ProjectFlowIdentifier(data_source_id=i) for i in range(min(flows_count, 5)) # Limit for test performance - ] + ], ) - + # Act project = mock_client.projects.create(project_data) - + # Assert assert isinstance(project, Project) assert project.name == name @@ -58,14 +78,38 @@ def test_create_project_serialization(self, mock_client, name, description, flow assert project.flows_count == flows_count @given( - project_data=st.fixed_dictionaries({ - 'id': st.integers(min_value=1, max_value=10000), - 'name': st.text(min_size=1, max_size=100, alphabet=st.characters(min_codepoint=33, max_codepoint=126)), - 'description': st.text(min_size=0, max_size=500, alphabet=st.characters(min_codepoint=33, max_codepoint=126)), - 'flows_count': st.integers(min_value=0, max_value=100), - 'client_identifier': st.one_of(st.none(), st.text(min_size=0, max_size=50, alphabet=st.characters(min_codepoint=33, max_codepoint=126))), - 'client_url': st.one_of(st.none(), st.text(min_size=0, max_size=200, alphabet=st.characters(min_codepoint=33, max_codepoint=126))) - }) + project_data=st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=10000), + "name": st.text( + min_size=1, + max_size=100, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + "description": st.text( + min_size=0, + max_size=500, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + "flows_count": st.integers(min_value=0, max_value=100), + "client_identifier": st.one_of( + st.none(), + st.text( + min_size=0, + max_size=50, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + ), + "client_url": st.one_of( + st.none(), + st.text( + min_size=0, + max_size=200, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + ), + } + ) ) @SETTINGS def test_project_response_parsing(self, mock_client, project_data): @@ -74,32 +118,42 @@ def test_project_response_parsing(self, mock_client, project_data): factory = MockDataFactory() mock_project = factory.create_mock_project(**project_data) mock_client.http_client.request = MagicMock(return_value=mock_project) - + # Act - project = mock_client.projects.get(project_data['id']) - + project = mock_client.projects.get(project_data["id"]) + # Assert assert isinstance(project, Project) - assert project.id == project_data['id'] - assert project.name == project_data['name'] - assert project.description == project_data['description'] - if project_data['flows_count'] is not None: - assert project.flows_count == project_data['flows_count'] - if project_data['client_identifier'] is not None: - assert project.client_identifier == project_data['client_identifier'] - if project_data['client_url'] is not None: - assert project.client_url == project_data['client_url'] + assert project.id == project_data["id"] + assert project.name == project_data["name"] + assert project.description == project_data["description"] + if project_data["flows_count"] is not None: + assert project.flows_count == project_data["flows_count"] + if project_data["client_identifier"] is not None: + assert project.client_identifier == project_data["client_identifier"] + if project_data["client_url"] is not None: + assert project.client_url == project_data["client_url"] @given( projects=st.lists( - st.fixed_dictionaries({ - 'id': st.integers(min_value=1, max_value=10000), - 'name': st.text(min_size=1, max_size=100, alphabet=st.characters(min_codepoint=33, max_codepoint=126)), - 'description': st.text(min_size=0, max_size=500, alphabet=st.characters(min_codepoint=33, max_codepoint=126)), - 'flows_count': st.integers(min_value=0, max_value=50) - }), + st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=10000), + "name": st.text( + min_size=1, + max_size=100, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + "description": st.text( + min_size=0, + max_size=500, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + "flows_count": st.integers(min_value=0, max_value=50), + } + ), min_size=0, - max_size=5 + max_size=5, ) ) @SETTINGS @@ -109,56 +163,72 @@ def test_list_projects_response_parsing(self, mock_client, projects): factory = MockDataFactory() mock_projects = [factory.create_mock_project(**proj) for proj in projects] mock_client.http_client.request = MagicMock(return_value=mock_projects) - + # Act result = mock_client.projects.list() - + # Assert assert isinstance(result, list) assert len(result) == len(projects) - + for i, project in enumerate(result): assert isinstance(project, Project) - assert project.id == mock_projects[i]['id'] - assert project.name == mock_projects[i]['name'] - assert project.description == mock_projects[i]['description'] + assert project.id == mock_projects[i]["id"] + assert project.name == mock_projects[i]["name"] + assert project.description == mock_projects[i]["description"] @given( data_flows=st.lists( - st.fixed_dictionaries({ - 'data_source_id': st.one_of(st.none(), st.integers(min_value=1, max_value=10000)), - 'data_set_id': st.one_of(st.none(), st.integers(min_value=1, max_value=10000)), - 'data_sink_id': st.one_of(st.none(), st.integers(min_value=1, max_value=10000)) - }), + st.fixed_dictionaries( + { + "data_source_id": st.one_of( + st.none(), st.integers(min_value=1, max_value=10000) + ), + "data_set_id": st.one_of( + st.none(), st.integers(min_value=1, max_value=10000) + ), + "data_sink_id": st.one_of( + st.none(), st.integers(min_value=1, max_value=10000) + ), + } + ), min_size=1, - max_size=10 + max_size=10, ) ) @SETTINGS - def test_project_data_flows_with_various_configurations(self, mock_client, data_flows): + def test_project_data_flows_with_various_configurations( + self, mock_client, data_flows + ): """Test project data flow operations with various configurations.""" # Arrange project_id = 123 factory = MockDataFactory() - mock_flows = [factory.create_mock_project_data_flow(**flow) for flow in data_flows] + mock_flows = [ + factory.create_mock_project_data_flow(**flow) for flow in data_flows + ] mock_client.http_client.request = MagicMock(return_value=mock_flows) - + # Create flow identifiers - ensure at least one ID is provided per flow flow_identifiers = [] for flow in data_flows: - if flow['data_source_id']: - flow_identifiers.append(ProjectFlowIdentifier(data_source_id=flow['data_source_id'])) - elif flow['data_set_id']: - flow_identifiers.append(ProjectFlowIdentifier(data_set_id=flow['data_set_id'])) + if flow["data_source_id"]: + flow_identifiers.append( + ProjectFlowIdentifier(data_source_id=flow["data_source_id"]) + ) + elif flow["data_set_id"]: + flow_identifiers.append( + ProjectFlowIdentifier(data_set_id=flow["data_set_id"]) + ) else: # Fallback to source if no valid ID flow_identifiers.append(ProjectFlowIdentifier(data_source_id=1)) - + flows = ProjectFlowList(data_flows=flow_identifiers) - + # Act result = mock_client.projects.add_data_flows(project_id, flows) - + # Assert assert isinstance(result, list) assert len(result) == len(data_flows) @@ -166,8 +236,22 @@ def test_project_data_flows_with_various_configurations(self, mock_client, data_ assert isinstance(flow, ProjectDataFlow) @given( - name=st.one_of(st.none(), st.text(min_size=1, max_size=100, alphabet=st.characters(min_codepoint=33, max_codepoint=126))), - description=st.one_of(st.none(), st.text(min_size=0, max_size=500, alphabet=st.characters(min_codepoint=33, max_codepoint=126))) + name=st.one_of( + st.none(), + st.text( + min_size=1, + max_size=100, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + ), + description=st.one_of( + st.none(), + st.text( + min_size=0, + max_size=500, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + ), ) @SETTINGS def test_project_update_serialization(self, mock_client, name, description): @@ -175,22 +259,22 @@ def test_project_update_serialization(self, mock_client, name, description): # Arrange project_id = 123 factory = MockDataFactory() - + # Create update data with only provided fields update_data = {} if name is not None: - update_data['name'] = name + update_data["name"] = name if description is not None: - update_data['description'] = description - + update_data["description"] = description + mock_project = factory.create_mock_project(**update_data) mock_client.http_client.request = MagicMock(return_value=mock_project) - + update_request = ProjectUpdate(**update_data) - + # Act project = mock_client.projects.update(project_id, update_request) - + # Assert assert isinstance(project, Project) if name is not None: @@ -200,16 +284,20 @@ def test_project_update_serialization(self, mock_client, name, description): @given( tags=st.lists( - st.text(min_size=1, max_size=50, alphabet=st.characters(min_codepoint=33, max_codepoint=126)), + st.text( + min_size=1, + max_size=50, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), min_size=0, - max_size=10 + max_size=10, ), access_roles=st.lists( - st.sampled_from(['owner', 'admin', 'collaborator', 'operator']), + st.sampled_from(["owner", "admin", "collaborator", "operator"]), min_size=1, max_size=4, - unique=True - ) + unique=True, + ), ) @SETTINGS def test_project_metadata_parsing(self, mock_client, tags, access_roles): @@ -218,24 +306,30 @@ def test_project_metadata_parsing(self, mock_client, tags, access_roles): factory = MockDataFactory() mock_project = factory.create_mock_project(tags=tags, access_roles=access_roles) mock_client.http_client.request = MagicMock(return_value=mock_project) - + # Act project = mock_client.projects.get(123) - + # Assert assert isinstance(project, Project) - assert project.tags == mock_project['tags'] - assert project.access_roles == mock_project['access_roles'] + assert project.tags == mock_project["tags"] + assert project.access_roles == mock_project["access_roles"] @given( search_filters=st.lists( - st.fixed_dictionaries({ - 'field': st.sampled_from(['name', 'description', 'status']), - 'operator': st.sampled_from(['contains', 'equals', 'starts_with']), - 'value': st.text(min_size=1, max_size=50, alphabet=st.characters(min_codepoint=33, max_codepoint=126)) - }), + st.fixed_dictionaries( + { + "field": st.sampled_from(["name", "description", "status"]), + "operator": st.sampled_from(["contains", "equals", "starts_with"]), + "value": st.text( + min_size=1, + max_size=50, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ), + } + ), min_size=1, - max_size=5 + max_size=5, ) ) @SETTINGS @@ -244,41 +338,47 @@ def test_flows_search_with_various_filters(self, mock_client, search_filters): # Arrange project_id = 123 mock_response = { - "flows": [{ - "id": 1, - "origin_node_id": 1, - "parent_node_id": None, - "data_source_id": None, - "data_set_id": None, - "data_sink_id": None, - "status": None, - "project_id": None, - "flow_type": None, - "ingestion_mode": None, - "name": "test flow", - "description": None, - "children": None - }], + "flows": [ + { + "id": 1, + "origin_node_id": 1, + "parent_node_id": None, + "data_source_id": None, + "data_set_id": None, + "data_sink_id": None, + "status": None, + "project_id": None, + "flow_type": None, + "ingestion_mode": None, + "name": "test flow", + "description": None, + "children": None, + } + ], "data_sources": [], "data_sets": [], - "data_sinks": [] + "data_sinks": [], } mock_client.http_client.request = MagicMock(return_value=mock_response) - + # Act result = mock_client.projects.search_flows(project_id, search_filters) - + # Assert assert result is not None - + # Verify the call was made mock_client.http_client.request.assert_called_once() call_args = mock_client.http_client.request.call_args - assert call_args[0][0] == 'POST' - assert f'/projects/{project_id}/flows/search' in call_args[0][1] + assert call_args[0][0] == "POST" + assert f"/projects/{project_id}/flows/search" in call_args[0][1] @given( - project_name=st.text(min_size=1, max_size=100, alphabet=st.characters(min_codepoint=33, max_codepoint=126)) + project_name=st.text( + min_size=1, + max_size=100, + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + ) ) @SETTINGS def test_project_name_edge_cases(self, mock_client, project_name): @@ -287,15 +387,12 @@ def test_project_name_edge_cases(self, mock_client, project_name): factory = MockDataFactory() mock_project = factory.create_mock_project(name=project_name) mock_client.http_client.request = MagicMock(return_value=mock_project) - - project_data = ProjectCreate( - name=project_name, - description="Test description" - ) - + + project_data = ProjectCreate(name=project_name, description="Test description") + # Act project = mock_client.projects.create(project_data) - + # Assert assert isinstance(project, Project) - assert project.name == project_name \ No newline at end of file + assert project.name == project_name diff --git a/tests/property/test_sources.py b/tests/property/test_sources.py index 9b001c9..c5ccedf 100644 --- a/tests/property/test_sources.py +++ b/tests/property/test_sources.py @@ -1,11 +1,16 @@ """Property-based tests for sources using hypothesis.""" import pytest -from hypothesis import given, strategies as st, settings +from hypothesis import given, settings +from hypothesis import strategies as st from hypothesis.strategies import composite -from nexla_sdk.models.sources.responses import Source, DataSetBrief, RunInfo -from nexla_sdk.models.sources.requests import SourceCreate, SourceUpdate, SourceCopyOptions +from nexla_sdk.models.sources.requests import ( + SourceCopyOptions, + SourceCreate, + SourceUpdate, +) +from nexla_sdk.models.sources.responses import DataSetBrief, RunInfo, Source # Custom strategies for generating test data @@ -15,26 +20,51 @@ def source_dict(draw): return { "id": draw(st.integers(min_value=1, max_value=999999)), # Avoid whitespace/control chars in names - "name": draw(st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=200)), - "status": draw(st.sampled_from(["ACTIVE", "PAUSED", "DRAFT", "DELETED", "ERROR", "INIT"])), - "source_type": draw(st.sampled_from(["s3", "postgres", "mysql", "api_push", "ftp", "gcs"])), + "name": draw( + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=200, + ) + ), + "status": draw( + st.sampled_from(["ACTIVE", "PAUSED", "DRAFT", "DELETED", "ERROR", "INIT"]) + ), + "source_type": draw( + st.sampled_from(["s3", "postgres", "mysql", "api_push", "ftp", "gcs"]) + ), "description": draw( st.one_of( st.none(), - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), max_size=1000), + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + max_size=1000, + ), ) ), - "ingest_method": draw(st.one_of(st.none(), st.sampled_from(["POLL", "API", "STREAMING"]))), - "source_format": draw(st.one_of(st.none(), st.sampled_from(["JSON", "CSV", "XML", "PARQUET"]))), + "ingest_method": draw( + st.one_of(st.none(), st.sampled_from(["POLL", "API", "STREAMING"])) + ), + "source_format": draw( + st.one_of(st.none(), st.sampled_from(["JSON", "CSV", "XML", "PARQUET"])) + ), "managed": draw(st.booleans()), "auto_generated": draw(st.booleans()), - "access_roles": draw(st.lists( - st.sampled_from(["owner", "admin", "collaborator", "operator"]), - min_size=1, max_size=4, unique=True - )), + "access_roles": draw( + st.lists( + st.sampled_from(["owner", "admin", "collaborator", "operator"]), + min_size=1, + max_size=4, + unique=True, + ) + ), "tags": draw( st.lists( - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=50), + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=50, + ), max_size=10, ) ), @@ -48,12 +78,22 @@ def source_create_dict(draw): """Generate random source creation data.""" return { # Avoid whitespace/control chars in names - "name": draw(st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=200)), - "source_type": draw(st.sampled_from(["s3", "postgres", "mysql", "api_push", "ftp", "gcs"])), + "name": draw( + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=200, + ) + ), + "source_type": draw( + st.sampled_from(["s3", "postgres", "mysql", "api_push", "ftp", "gcs"]) + ), "description": draw(st.one_of(st.none(), st.text(max_size=1000))), # Required field must be int "data_credentials_id": draw(st.integers(min_value=1, max_value=999999)), - "ingest_method": draw(st.one_of(st.none(), st.sampled_from(["POLL", "API", "STREAMING"]))), + "ingest_method": draw( + st.one_of(st.none(), st.sampled_from(["POLL", "API", "STREAMING"])) + ), } @@ -67,13 +107,20 @@ def dataset_brief_dict(draw): "name": draw( st.one_of( st.none(), - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=200), + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=200, + ), ) ), "description": draw( st.one_of( st.none(), - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), max_size=1000), + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + max_size=1000, + ), ) ), "version": draw(st.one_of(st.none(), st.integers(min_value=1, max_value=100))), @@ -85,80 +132,89 @@ def dataset_brief_dict(draw): @pytest.mark.unit class TestSourceModelProperties: """Property-based tests for Source model.""" - + @given(source_dict()) def test_source_model_handles_various_inputs(self, source_data): """Test that Source model handles various valid inputs correctly.""" # Act & Assert - Should not raise validation errors source = Source(**source_data) - + # Basic assertions assert source.id == source_data["id"] assert source.name == source_data["name"] assert source.status == source_data["status"] assert source.source_type == source_data["source_type"] assert source.access_roles == source_data["access_roles"] - + # Optional fields should be handled correctly assert source.description == source_data.get("description") assert source.managed == source_data.get("managed", False) assert source.auto_generated == source_data.get("auto_generated", False) - + # Lists should default to empty if None expected_tags = source_data.get("tags", []) if expected_tags is None: expected_tags = [] assert source.tags == expected_tags - + @given(source_dict()) def test_source_model_serialization(self, source_data): """Test that Source model can be serialized and deserialized.""" # Arrange source = Source(**source_data) - + # Act serialized = source.model_dump() deserialized = Source(**serialized) - + # Assert assert deserialized.id == source.id assert deserialized.name == source.name assert deserialized.status == source.status assert deserialized.source_type == source.source_type - + @given(st.lists(source_dict(), min_size=0, max_size=10)) def test_source_list_handling(self, sources_data): """Test handling lists of sources with various sizes.""" # Act sources = [Source(**data) for data in sources_data] - + # Assert assert len(sources) == len(sources_data) for i, source in enumerate(sources): assert source.id == sources_data[i]["id"] assert source.name == sources_data[i]["name"] - + @given(source_create_dict()) def test_source_create_model_properties(self, create_data): """Test SourceCreate model with various inputs.""" # Act & Assert - Should not raise validation errors source_create = SourceCreate(**create_data) - + assert source_create.name == create_data["name"] assert source_create.source_type == create_data["source_type"] assert source_create.description == create_data.get("description") - assert source_create.data_credentials_id == create_data.get("data_credentials_id") + assert source_create.data_credentials_id == create_data.get( + "data_credentials_id" + ) assert source_create.ingest_method == create_data.get("ingest_method") - + @given( st.one_of( st.none(), - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=200) + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=200, + ), ), st.one_of( st.none(), - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), max_size=1000) - ) + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + max_size=1000, + ), + ), ) def test_source_update_model_properties(self, name, description): """Test SourceUpdate model with various optional fields.""" @@ -174,23 +230,25 @@ def test_source_update_model_properties(self, name, description): # Assert - Account for str_strip_whitespace which may strip whitespace assert source_update.name == name assert source_update.description == description - + @given( st.booleans(), st.booleans(), st.one_of(st.none(), st.integers(min_value=1, max_value=999999)), - st.one_of(st.none(), st.integers(min_value=1, max_value=999999)) + st.one_of(st.none(), st.integers(min_value=1, max_value=999999)), ) - def test_source_copy_options_properties(self, reuse_creds, copy_access, owner_id, org_id): + def test_source_copy_options_properties( + self, reuse_creds, copy_access, owner_id, org_id + ): """Test SourceCopyOptions with various combinations.""" # Act options = SourceCopyOptions( reuse_data_credentials=reuse_creds, copy_access_controls=copy_access, owner_id=owner_id, - org_id=org_id + org_id=org_id, ) - + # Assert assert options.reuse_data_credentials == reuse_creds assert options.copy_access_controls == copy_access @@ -198,29 +256,29 @@ def test_source_copy_options_properties(self, reuse_creds, copy_access, owner_id assert options.org_id == org_id -@pytest.mark.unit +@pytest.mark.unit class TestDataSetBriefProperties: """Property-based tests for DataSetBrief model.""" - + @given(dataset_brief_dict()) def test_dataset_brief_model_properties(self, dataset_data): """Test DataSetBrief model with various inputs.""" # Act & Assert dataset = DataSetBrief(**dataset_data) - + assert dataset.id == dataset_data["id"] assert dataset.owner_id == dataset_data["owner_id"] assert dataset.org_id == dataset_data["org_id"] assert dataset.name == dataset_data.get("name") assert dataset.description == dataset_data.get("description") assert dataset.version == dataset_data.get("version") - + @given(st.lists(dataset_brief_dict(), min_size=0, max_size=5)) def test_multiple_datasets_handling(self, datasets_data): """Test handling multiple datasets.""" # Act datasets = [DataSetBrief(**data) for data in datasets_data] - + # Assert assert len(datasets) == len(datasets_data) for i, dataset in enumerate(datasets): @@ -230,20 +288,17 @@ def test_multiple_datasets_handling(self, datasets_data): @pytest.mark.unit class TestRunInfoProperties: """Property-based tests for RunInfo model.""" - - @given( - st.integers(min_value=1, max_value=999999), - st.datetimes() - ) + + @given(st.integers(min_value=1, max_value=999999), st.datetimes()) def test_run_info_model_properties(self, run_id, created_at): """Test RunInfo model with various inputs.""" # Act run_info = RunInfo(id=run_id, created_at=created_at) - + # Assert assert run_info.id == run_id assert run_info.created_at == created_at - + @given( st.lists( st.fixed_dictionaries( @@ -261,9 +316,13 @@ def test_multiple_run_infos(self, run_data_list): # Act run_infos = [] for run_data in run_data_list: - if isinstance(run_data, dict) and "id" in run_data and "created_at" in run_data: + if ( + isinstance(run_data, dict) + and "id" in run_data + and "created_at" in run_data + ): run_infos.append(RunInfo(**run_data)) - + # Assert assert len(run_infos) <= len(run_data_list) @@ -271,7 +330,7 @@ def test_multiple_run_infos(self, run_data_list): @pytest.mark.unit class TestSourceModelEdgeCases: """Test edge cases and boundary conditions for source models.""" - + @given( st.text( alphabet=st.characters(min_codepoint=33, max_codepoint=126), @@ -287,18 +346,22 @@ def test_source_name_variations(self, name): "name": name, "status": "ACTIVE", "source_type": "s3", - "access_roles": ["owner"] + "access_roles": ["owner"], } - + source = Source(**source_data) - + # Assert assert source.name == name assert len(source.name) >= 1 - + @given( st.lists( - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=50), + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=50, + ), min_size=0, max_size=20, unique=True, @@ -310,22 +373,26 @@ def test_source_tags_variations(self, tags): source_data = { "id": 123, "name": "Test Source", - "status": "ACTIVE", + "status": "ACTIVE", "source_type": "s3", "access_roles": ["owner"], - "tags": tags + "tags": tags, } - + source = Source(**source_data) - + # Assert assert source.tags == tags assert len(source.tags) == len(set(tags)) # Should maintain uniqueness - - @given(st.lists( - st.sampled_from(["owner", "admin", "collaborator", "operator"]), - min_size=1, max_size=4, unique=True - )) + + @given( + st.lists( + st.sampled_from(["owner", "admin", "collaborator", "operator"]), + min_size=1, + max_size=4, + unique=True, + ) + ) def test_access_roles_combinations(self, roles): """Test various access role combinations.""" # Act @@ -333,16 +400,16 @@ def test_access_roles_combinations(self, roles): "id": 123, "name": "Test Source", "status": "ACTIVE", - "source_type": "s3", - "access_roles": roles + "source_type": "s3", + "access_roles": roles, } - + source = Source(**source_data) - + # Assert assert source.access_roles == roles assert len(source.access_roles) >= 1 # Should have at least one role - + @given(st.one_of(st.none(), st.text(max_size=2000))) def test_description_length_handling(self, description): """Test handling descriptions of various lengths including None.""" @@ -353,22 +420,30 @@ def test_description_length_handling(self, description): "status": "ACTIVE", "source_type": "s3", "access_roles": ["owner"], - "description": description + "description": description, } - + source = Source(**source_data) - + # Assert assert source.description == description - + @settings(max_examples=50) @given( st.integers(min_value=1, max_value=999999), - st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=100), + st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=100, + ), st.sampled_from(["ACTIVE", "PAUSED", "DRAFT", "DELETED", "ERROR", "INIT"]), - st.sampled_from(["s3", "postgres", "mysql", "api_push", "ftp", "gcs", "bigquery"]) + st.sampled_from( + ["s3", "postgres", "mysql", "api_push", "ftp", "gcs", "bigquery"] + ), ) - def test_source_core_fields_combinations(self, source_id, name, status, source_type): + def test_source_core_fields_combinations( + self, source_id, name, status, source_type + ): """Test combinations of core required fields.""" # Act source_data = { @@ -376,11 +451,11 @@ def test_source_core_fields_combinations(self, source_id, name, status, source_t "name": name, "status": status, "source_type": source_type, - "access_roles": ["owner"] + "access_roles": ["owner"], } - + source = Source(**source_data) - + # Assert assert source.id == source_id assert source.name == name diff --git a/tests/property/test_teams.py b/tests/property/test_teams.py index b33e41e..276bfb1 100644 --- a/tests/property/test_teams.py +++ b/tests/property/test_teams.py @@ -1,14 +1,25 @@ """Property-based tests for TeamsResource.""" -from hypothesis import given, strategies as st +from hypothesis import given +from hypothesis import strategies as st + +from nexla_sdk.models.teams.requests import ( + TeamCreate, + TeamMemberList, + TeamMemberRequest, + TeamUpdate, +) from nexla_sdk.models.teams.responses import Team, TeamMember -from nexla_sdk.models.teams.requests import TeamCreate, TeamUpdate, TeamMemberRequest, TeamMemberList from tests.utils.mock_builders import MockResponseBuilder def generate_text_without_space(): """Generate text without space characters to avoid Pydantic str_strip_whitespace issues.""" - return st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=100) + return st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=100, + ) class TestTeamsPropertyTests: @@ -23,29 +34,30 @@ class TestTeamsPropertyTests: st.sampled_from(["owner", "admin", "collaborator", "operator", "member"]), min_size=1, max_size=3, - unique=True - ) + unique=True, + ), ) - def test_team_creation_with_various_inputs(self, mock_client, team_id, name, - description, member, access_roles): + def test_team_creation_with_various_inputs( + self, mock_client, team_id, name, description, member, access_roles + ): """Test team creation with various input combinations.""" client = mock_client - + # Create team data with generated values team_data = MockResponseBuilder.team( team_id=team_id, name=name, description=description, member=member, - access_roles=access_roles + access_roles=access_roles, ) - + # Mock the API response client.http_client.add_response("GET", f"/teams/{team_id}", team_data) - + # Test the response team = client.teams.get(team_id) - + assert isinstance(team, Team) assert team.id == team_id assert team.name == name @@ -56,104 +68,106 @@ def test_team_creation_with_various_inputs(self, mock_client, team_id, name, @given( members=st.lists( - st.fixed_dictionaries({ - 'id': st.integers(min_value=1, max_value=10000), - 'email': st.emails(), - 'admin': st.booleans() - }), + st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=10000), + "email": st.emails(), + "admin": st.booleans(), + } + ), min_size=0, - max_size=10 + max_size=10, ) ) def test_team_response_parsing_with_members(self, mock_client, members): """Test team response parsing with various member configurations.""" client = mock_client - - team_data = MockResponseBuilder.team( - team_id=123, - members=members - ) - + + team_data = MockResponseBuilder.team(team_id=123, members=members) + client.http_client.add_response("GET", "/teams/123", team_data) - + team = client.teams.get(123) - + assert isinstance(team, Team) assert len(team.members) == len(members) - + for i, member in enumerate(team.members): assert isinstance(member, TeamMember) - assert member.id == members[i]['id'] - assert member.email == members[i]['email'] - assert member.admin == members[i]['admin'] + assert member.id == members[i]["id"] + assert member.email == members[i]["email"] + assert member.admin == members[i]["admin"] @given( teams_count=st.integers(min_value=0, max_value=10), page=st.integers(min_value=1, max_value=100), per_page=st.integers(min_value=1, max_value=100), - access_role=st.one_of(st.none(), st.sampled_from(["owner", "member", "collaborator", "admin"])) + access_role=st.one_of( + st.none(), st.sampled_from(["owner", "member", "collaborator", "admin"]) + ), ) - def test_list_teams_with_various_parameters(self, mock_client, teams_count, - page, per_page, access_role): + def test_list_teams_with_various_parameters( + self, mock_client, teams_count, page, per_page, access_role + ): """Test listing teams with various parameter combinations.""" client = mock_client - + # Generate list of teams teams_data = [ - MockResponseBuilder.team(team_id=i+1) - for i in range(teams_count) + MockResponseBuilder.team(team_id=i + 1) for i in range(teams_count) ] - + client.http_client.add_response("GET", "/teams", teams_data) - + # Call with parameters kwargs = {"page": page, "per_page": per_page} if access_role: kwargs["access_role"] = access_role - + teams = client.teams.list(**kwargs) - + assert len(teams) == teams_count assert all(isinstance(team, Team) for team in teams) @given( name=st.one_of(st.none(), generate_text_without_space()), description=st.one_of(st.none(), generate_text_without_space()), - members_count=st.integers(min_value=0, max_value=5) + members_count=st.integers(min_value=0, max_value=5), ) - def test_team_update_with_various_fields(self, mock_client, name, description, - members_count): + def test_team_update_with_various_fields( + self, mock_client, name, description, members_count + ): """Test team update with various field combinations.""" client = mock_client - + # Create update request with only non-None values update_data = {} if name is not None: update_data["name"] = name if description is not None: update_data["description"] = description - + # Add some members if count > 0 if members_count > 0: update_data["members"] = [ TeamMemberRequest(email=f"user{i}@example.com", admin=(i % 2 == 0)) for i in range(members_count) ] - + request = TeamUpdate(**update_data) - + # Mock response response_data = MockResponseBuilder.team(team_id=123) if name is not None: response_data["name"] = name if description is not None: response_data["description"] = description - + client.http_client.add_response("PUT", "/teams/123", response_data) - + # Test the update team = client.teams.update(123, request) - + assert isinstance(team, Team) if name is not None: assert team.name == name @@ -162,28 +176,30 @@ def test_team_update_with_various_fields(self, mock_client, name, description, @given( team_id=st.integers(min_value=1, max_value=999999), - members_count=st.integers(min_value=0, max_value=10) + members_count=st.integers(min_value=0, max_value=10), ) def test_team_members_response_parsing(self, mock_client, team_id, members_count): """Test team members response parsing with various configurations.""" client = mock_client - + # Generate members data members_data = [] for i in range(members_count): - members_data.append(MockResponseBuilder.team_member( - user_id=i+1, - email=f"user{i}@example.com", - admin=(i % 2 == 0) - )) - - client.http_client.add_response("GET", f"/teams/{team_id}/members", members_data) - + members_data.append( + MockResponseBuilder.team_member( + user_id=i + 1, email=f"user{i}@example.com", admin=(i % 2 == 0) + ) + ) + + client.http_client.add_response( + "GET", f"/teams/{team_id}/members", members_data + ) + members = client.teams.get_members(team_id) - + assert len(members) == members_count assert all(isinstance(member, TeamMember) for member in members) - + for i, member in enumerate(members): assert member.id == i + 1 assert member.email == f"user{i}@example.com" @@ -191,125 +207,119 @@ def test_team_members_response_parsing(self, mock_client, team_id, members_count @given( add_members=st.lists( - st.fixed_dictionaries({ - 'email': st.emails(), - 'admin': st.booleans() - }), + st.fixed_dictionaries({"email": st.emails(), "admin": st.booleans()}), min_size=1, - max_size=5 + max_size=5, ) ) def test_add_members_with_various_configurations(self, mock_client, add_members): """Test adding members with various configurations.""" client = mock_client - + # Create member request list member_requests = [ - TeamMemberRequest(email=member['email'], admin=member['admin']) + TeamMemberRequest(email=member["email"], admin=member["admin"]) for member in add_members ] - + request_data = TeamMemberList(members=member_requests) - + # Mock response - existing members plus new ones response_data = [ - MockResponseBuilder.team_member(user_id=999, email="existing@example.com", admin=True) + MockResponseBuilder.team_member( + user_id=999, email="existing@example.com", admin=True + ) ] - + for i, member in enumerate(add_members): - response_data.append(MockResponseBuilder.team_member( - user_id=i+1000, - email=member['email'], - admin=member['admin'] - )) - + response_data.append( + MockResponseBuilder.team_member( + user_id=i + 1000, email=member["email"], admin=member["admin"] + ) + ) + client.http_client.add_response("PUT", "/teams/123/members", response_data) - + members = client.teams.add_members(123, request_data) - + assert len(members) == len(add_members) + 1 # existing + new assert all(isinstance(member, TeamMember) for member in members) @given( replace_members=st.lists( - st.fixed_dictionaries({ - 'email': st.emails(), - 'admin': st.booleans() - }), + st.fixed_dictionaries({"email": st.emails(), "admin": st.booleans()}), min_size=0, - max_size=5 + max_size=5, ) ) - def test_replace_members_with_various_configurations(self, mock_client, replace_members): + def test_replace_members_with_various_configurations( + self, mock_client, replace_members + ): """Test replacing members with various configurations.""" client = mock_client - + # Create member request list member_requests = [ - TeamMemberRequest(email=member['email'], admin=member['admin']) + TeamMemberRequest(email=member["email"], admin=member["admin"]) for member in replace_members ] - + request_data = TeamMemberList(members=member_requests) - + # Mock response - only the new members response_data = [] for i, member in enumerate(replace_members): - response_data.append(MockResponseBuilder.team_member( - user_id=i+1, - email=member['email'], - admin=member['admin'] - )) - + response_data.append( + MockResponseBuilder.team_member( + user_id=i + 1, email=member["email"], admin=member["admin"] + ) + ) + client.http_client.add_response("POST", "/teams/123/members", response_data) - + members = client.teams.replace_members(123, request_data) - + assert len(members) == len(replace_members) assert all(isinstance(member, TeamMember) for member in members) @given( member_specification=st.one_of( - st.fixed_dictionaries({'email': st.emails()}), - st.fixed_dictionaries({'id': st.integers(min_value=1, max_value=10000)}), - st.fixed_dictionaries({ - 'email': st.emails(), - 'id': st.integers(min_value=1, max_value=10000) - }) + st.fixed_dictionaries({"email": st.emails()}), + st.fixed_dictionaries({"id": st.integers(min_value=1, max_value=10000)}), + st.fixed_dictionaries( + {"email": st.emails(), "id": st.integers(min_value=1, max_value=10000)} + ), ), - admin=st.booleans() + admin=st.booleans(), ) def test_team_member_request_variations(self, member_specification, admin): """Test TeamMemberRequest with various identification methods.""" # Add admin to the specification - member_data = {**member_specification, 'admin': admin} - + member_data = {**member_specification, "admin": admin} + # Should not raise validation error request = TeamMemberRequest(**member_data) - - if 'email' in member_specification: - assert request.email == member_specification['email'] + + if "email" in member_specification: + assert request.email == member_specification["email"] else: assert request.email is None - - if 'id' in member_specification: - assert request.id == member_specification['id'] + + if "id" in member_specification: + assert request.id == member_specification["id"] else: assert request.id is None - + assert request.admin == admin @given( name=generate_text_without_space(), description=st.one_of(st.none(), generate_text_without_space()), initial_members=st.lists( - st.fixed_dictionaries({ - 'email': st.emails(), - 'admin': st.booleans() - }), + st.fixed_dictionaries({"email": st.emails(), "admin": st.booleans()}), min_size=0, - max_size=3 - ) + max_size=3, + ), ) def test_team_creation_request_validation(self, name, description, initial_members): """Test team creation request with various input combinations.""" @@ -317,104 +327,102 @@ def test_team_creation_request_validation(self, name, description, initial_membe create_data = {"name": name} if description is not None: create_data["description"] = description - + if initial_members: create_data["members"] = [ - TeamMemberRequest(email=member['email'], admin=member['admin']) + TeamMemberRequest(email=member["email"], admin=member["admin"]) for member in initial_members ] - + # Should not raise validation error request = TeamCreate(**create_data) - + assert request.name == name if description is not None: assert request.description == description - + if initial_members: assert len(request.members) == len(initial_members) for i, member in enumerate(request.members): - assert member.email == initial_members[i]['email'] - assert member.admin == initial_members[i]['admin'] + assert member.email == initial_members[i]["email"] + assert member.admin == initial_members[i]["admin"] @given( expand=st.booleans(), page=st.one_of(st.none(), st.integers(min_value=1, max_value=100)), - per_page=st.one_of(st.none(), st.integers(min_value=1, max_value=100)) + per_page=st.one_of(st.none(), st.integers(min_value=1, max_value=100)), ) - def test_list_teams_parameter_combinations(self, mock_client, expand, page, per_page): + def test_list_teams_parameter_combinations( + self, mock_client, expand, page, per_page + ): """Test listing teams with various parameter combinations.""" client = mock_client - + team_data = MockResponseBuilder.team(team_id=123) client.http_client.add_response("GET", "/teams", [team_data]) - + kwargs = {} if page is not None: kwargs["page"] = page if per_page is not None: kwargs["per_page"] = per_page - + teams = client.teams.list(**kwargs) - + assert len(teams) == 1 assert isinstance(teams[0], Team) @given( - remove_by=st.sampled_from(['email', 'id', 'both']), + remove_by=st.sampled_from(["email", "id", "both"]), members_to_remove=st.lists( - st.fixed_dictionaries({ - 'email': st.emails(), - 'id': st.integers(min_value=1, max_value=10000) - }), + st.fixed_dictionaries( + {"email": st.emails(), "id": st.integers(min_value=1, max_value=10000)} + ), min_size=1, - max_size=3 - ) + max_size=3, + ), ) - def test_remove_members_with_various_identifiers(self, mock_client, remove_by, - members_to_remove): + def test_remove_members_with_various_identifiers( + self, mock_client, remove_by, members_to_remove + ): """Test removing members using various identification methods.""" client = mock_client - + # Create removal request based on identification method member_requests = [] for member in members_to_remove: - if remove_by == 'email': - member_requests.append(TeamMemberRequest(email=member['email'])) - elif remove_by == 'id': - member_requests.append(TeamMemberRequest(id=member['id'])) + if remove_by == "email": + member_requests.append(TeamMemberRequest(email=member["email"])) + elif remove_by == "id": + member_requests.append(TeamMemberRequest(id=member["id"])) else: # both - member_requests.append(TeamMemberRequest( - email=member['email'], - id=member['id'] - )) - + member_requests.append( + TeamMemberRequest(email=member["email"], id=member["id"]) + ) + request_data = TeamMemberList(members=member_requests) - + # Mock response - remaining members (empty for simplicity) client.http_client.add_response("DELETE", "/teams/123/members", []) - + members = client.teams.remove_members(123, request_data) - + assert isinstance(members, list) # After removal, we expect the response (could be empty) @given( tags=st.lists( - generate_text_without_space(), - min_size=0, - max_size=5, - unique=True + generate_text_without_space(), min_size=0, max_size=5, unique=True ) ) def test_team_with_tags(self, mock_client, tags): """Test team response with various tag configurations.""" client = mock_client - + team_data = MockResponseBuilder.team(team_id=123, tags=tags) client.http_client.add_response("GET", "/teams/123", team_data) - + team = client.teams.get(123) - + assert isinstance(team, Team) - assert team.tags == tags \ No newline at end of file + assert team.tags == tags diff --git a/tests/property/test_users.py b/tests/property/test_users.py index c7b98d1..8425b2e 100644 --- a/tests/property/test_users.py +++ b/tests/property/test_users.py @@ -1,15 +1,27 @@ """Property-based tests for UsersResource.""" from datetime import date -from hypothesis import given, strategies as st, assume -from nexla_sdk.models.users.responses import User, UserExpanded, UserSettings, OrgMembership + +from hypothesis import assume, given +from hypothesis import strategies as st + from nexla_sdk.models.users.requests import UserCreate, UserUpdate +from nexla_sdk.models.users.responses import ( + OrgMembership, + User, + UserExpanded, + UserSettings, +) from tests.utils.mock_builders import MockResponseBuilder def generate_text_without_space(): """Generate text without space characters to avoid Pydantic str_strip_whitespace issues.""" - return st.text(alphabet=st.characters(min_codepoint=33, max_codepoint=126), min_size=1, max_size=100) + return st.text( + alphabet=st.characters(min_codepoint=33, max_codepoint=126), + min_size=1, + max_size=100, + ) class TestUsersPropertyTests: @@ -23,14 +35,23 @@ class TestUsersPropertyTests: impersonated=st.booleans(), user_tier=st.sampled_from(["FREE", "TRIAL", "PAID", "FREE_FOREVER"]), status=st.sampled_from(["ACTIVE", "DEACTIVATED", "SOURCE_COUNT_CAPPED"]), - account_locked=st.booleans() + account_locked=st.booleans(), ) - def test_user_creation_with_various_inputs(self, mock_client, user_id, email, - full_name, super_user, impersonated, - user_tier, status, account_locked): + def test_user_creation_with_various_inputs( + self, + mock_client, + user_id, + email, + full_name, + super_user, + impersonated, + user_tier, + status, + account_locked, + ): """Test user creation with various input combinations.""" client = mock_client - + # Create user data with generated values user_data = MockResponseBuilder.user( user_id=user_id, @@ -40,15 +61,15 @@ def test_user_creation_with_various_inputs(self, mock_client, user_id, email, impersonated=impersonated, user_tier=user_tier, status=status, - account_locked=account_locked + account_locked=account_locked, ) - + # Mock the API response client.http_client.add_response("GET", f"/users/{user_id}", user_data) - + # Test the response user = client.users.get(user_id) - + assert isinstance(user, User) assert user.id == user_id assert user.email == email @@ -61,67 +82,76 @@ def test_user_creation_with_various_inputs(self, mock_client, user_id, email, @given( org_memberships=st.lists( - st.fixed_dictionaries({ - 'id': st.integers(min_value=1, max_value=1000), - 'name': generate_text_without_space(), - 'is_admin': st.booleans(), - 'org_membership_status': st.sampled_from(["ACTIVE", "DEACTIVATED"]), - 'api_key': st.text(min_size=10, max_size=50) - }), + st.fixed_dictionaries( + { + "id": st.integers(min_value=1, max_value=1000), + "name": generate_text_without_space(), + "is_admin": st.booleans(), + "org_membership_status": st.sampled_from(["ACTIVE", "DEACTIVATED"]), + "api_key": st.text(min_size=10, max_size=50), + } + ), min_size=0, - max_size=5 + max_size=5, ) ) - def test_user_response_parsing_with_org_memberships(self, mock_client, org_memberships): + def test_user_response_parsing_with_org_memberships( + self, mock_client, org_memberships + ): """Test user response parsing with various org membership configurations.""" client = mock_client - + user_data = MockResponseBuilder.user( - user_id=123, - org_memberships=org_memberships + user_id=123, org_memberships=org_memberships ) - + client.http_client.add_response("GET", "/users/123", user_data) - + user = client.users.get(123) - + assert isinstance(user, User) assert len(user.org_memberships) == len(org_memberships) - + for i, membership in enumerate(user.org_memberships): assert isinstance(membership, OrgMembership) - assert membership.id == org_memberships[i]['id'] - assert membership.name == org_memberships[i]['name'] - assert membership.is_admin == org_memberships[i]['is_admin'] - assert membership.org_membership_status == org_memberships[i]['org_membership_status'] - assert membership.api_key == org_memberships[i]['api_key'] + assert membership.id == org_memberships[i]["id"] + assert membership.name == org_memberships[i]["name"] + assert membership.is_admin == org_memberships[i]["is_admin"] + assert ( + membership.org_membership_status + == org_memberships[i]["org_membership_status"] + ) + assert membership.api_key == org_memberships[i]["api_key"] @given( users_count=st.integers(min_value=0, max_value=10), page=st.integers(min_value=1, max_value=100), per_page=st.integers(min_value=1, max_value=100), - access_role=st.one_of(st.none(), st.sampled_from(["owner", "collaborator", "operator", "admin", "all"])) + access_role=st.one_of( + st.none(), + st.sampled_from(["owner", "collaborator", "operator", "admin", "all"]), + ), ) - def test_list_users_with_various_parameters(self, mock_client, users_count, - page, per_page, access_role): + def test_list_users_with_various_parameters( + self, mock_client, users_count, page, per_page, access_role + ): """Test listing users with various parameter combinations.""" client = mock_client - + # Generate list of users users_data = [ - MockResponseBuilder.user(user_id=i+1) - for i in range(users_count) + MockResponseBuilder.user(user_id=i + 1) for i in range(users_count) ] - + client.http_client.add_response("GET", "/users", users_data) - + # Call with parameters kwargs = {"page": page, "per_page": per_page} if access_role: kwargs["access_role"] = access_role - + users = client.users.list(**kwargs) - + assert len(users) == users_count assert all(isinstance(user, User) for user in users) @@ -130,13 +160,14 @@ def test_list_users_with_various_parameters(self, mock_client, users_count, email=st.one_of(st.none(), st.emails()), status=st.one_of(st.none(), st.sampled_from(["ACTIVE", "DEACTIVATED"])), user_tier=st.one_of(st.none(), st.sampled_from(["FREE", "TRIAL", "PAID"])), - password=st.one_of(st.none(), st.text(min_size=8, max_size=50)) + password=st.one_of(st.none(), st.text(min_size=8, max_size=50)), ) - def test_user_update_with_various_fields(self, mock_client, name, email, - status, user_tier, password): + def test_user_update_with_various_fields( + self, mock_client, name, email, status, user_tier, password + ): """Test user update with various field combinations.""" client = mock_client - + # Create update request with only non-None values update_data = {} if name is not None: @@ -149,9 +180,9 @@ def test_user_update_with_various_fields(self, mock_client, name, email, update_data["user_tier"] = user_tier if password is not None: update_data["password"] = password - + request = UserUpdate(**update_data) - + # Mock response response_data = MockResponseBuilder.user(user_id=123) if name is not None: @@ -162,12 +193,12 @@ def test_user_update_with_various_fields(self, mock_client, name, email, response_data["status"] = status if user_tier is not None: response_data["user_tier"] = user_tier - + client.http_client.add_response("PUT", "/users/123", response_data) - + # Test the update user = client.users.update(123, request) - + assert isinstance(user, User) if name is not None: assert user.full_name == name @@ -180,102 +211,102 @@ def test_user_update_with_various_fields(self, mock_client, name, email, @given( user_id=st.integers(min_value=1, max_value=999999), - settings_count=st.integers(min_value=0, max_value=5) + settings_count=st.integers(min_value=0, max_value=5), ) def test_user_settings_response_parsing(self, mock_client, user_id, settings_count): """Test user settings response parsing with various configurations.""" client = mock_client - + # Generate settings data settings_data = [] for i in range(settings_count): - settings_data.append({ - "id": f"setting_{i}", - "owner": {"id": user_id, "name": f"User {user_id}"}, - "org": {"id": 1, "name": "Test Org"}, - "user_settings_type": "general", - "settings": {"key": f"value_{i}"} - }) - + settings_data.append( + { + "id": f"setting_{i}", + "owner": {"id": user_id, "name": f"User {user_id}"}, + "org": {"id": 1, "name": "Test Org"}, + "user_settings_type": "general", + "settings": {"key": f"value_{i}"}, + } + ) + client.http_client.add_response("GET", "/user_settings", settings_data) - + settings = client.users.get_settings() - + assert len(settings) == settings_count assert all(isinstance(setting, UserSettings) for setting in settings) @given( - from_date=st.dates(min_value=date(2020, 1, 1), max_value=date(2025, 12, 31)).map(str), + from_date=st.dates( + min_value=date(2020, 1, 1), max_value=date(2025, 12, 31) + ).map(str), to_date=st.one_of( st.none(), st.dates(min_value=date(2020, 1, 1), max_value=date(2025, 12, 31)).map(str), ), org_id=st.one_of(st.none(), st.integers(min_value=1, max_value=1000)), ) - def test_account_metrics_with_various_parameters(self, mock_client, from_date, - to_date, org_id): + def test_account_metrics_with_various_parameters( + self, mock_client, from_date, to_date, org_id + ): """Test account metrics with various parameter combinations.""" # Ensure to_date is after from_date if both are provided if to_date is not None: assume(to_date >= from_date) - + client = mock_client - - metrics_data = { - "total_sources": 5, - "total_sinks": 3, - "total_records": 10000 - } - - client.http_client.add_response("GET", "/users/123/flows/account_metrics", metrics_data) - + + metrics_data = {"total_sources": 5, "total_sinks": 3, "total_records": 10000} + + client.http_client.add_response( + "GET", "/users/123/flows/account_metrics", metrics_data + ) + kwargs = {"from_date": from_date} if to_date is not None: kwargs["to_date"] = to_date if org_id is not None: kwargs["org_id"] = org_id - + metrics = client.users.get_account_metrics(123, **kwargs) - + assert isinstance(metrics, dict) assert "total_sources" in metrics @given( resource_type=st.sampled_from(["SOURCE", "SINK"]), - from_date=st.dates(min_value=date(2020, 1, 1), max_value=date(2025, 12, 31)).map(str), + from_date=st.dates( + min_value=date(2020, 1, 1), max_value=date(2025, 12, 31) + ).map(str), to_date=st.one_of( st.none(), st.dates(min_value=date(2020, 1, 1), max_value=date(2025, 12, 31)).map(str), ), org_id=st.one_of(st.none(), st.integers(min_value=1, max_value=1000)), ) - def test_daily_metrics_with_various_parameters(self, mock_client, resource_type, - from_date, to_date, org_id): + def test_daily_metrics_with_various_parameters( + self, mock_client, resource_type, from_date, to_date, org_id + ): """Test daily metrics with various parameter combinations.""" # Ensure to_date is after from_date if both are provided if to_date is not None: assume(to_date >= from_date) - + client = mock_client - - metrics_data = { - "daily_records": 1000, - "resource_type": resource_type - } - + + metrics_data = {"daily_records": 1000, "resource_type": resource_type} + client.http_client.add_response("GET", "/users/123/metrics", metrics_data) - - kwargs = { - "resource_type": resource_type, - "from_date": from_date - } + + kwargs = {"resource_type": resource_type, "from_date": from_date} if to_date is not None: kwargs["to_date"] = to_date if org_id is not None: kwargs["org_id"] = org_id - + metrics = client.users.get_daily_metrics(123, **kwargs) - + assert isinstance(metrics, dict) @given( @@ -283,26 +314,24 @@ def test_daily_metrics_with_various_parameters(self, mock_client, resource_type, email=st.emails(), default_org_id=st.one_of(st.none(), st.integers(min_value=1, max_value=1000)), status=st.one_of(st.none(), st.sampled_from(["ACTIVE", "DEACTIVATED"])), - user_tier=st.one_of(st.none(), st.sampled_from(["FREE", "TRIAL", "PAID"])) + user_tier=st.one_of(st.none(), st.sampled_from(["FREE", "TRIAL", "PAID"])), ) - def test_user_creation_request_validation(self, full_name, email, default_org_id, - status, user_tier): + def test_user_creation_request_validation( + self, full_name, email, default_org_id, status, user_tier + ): """Test user creation request with various input combinations.""" # Create request with only non-None values - create_data = { - "full_name": full_name, - "email": email - } + create_data = {"full_name": full_name, "email": email} if default_org_id is not None: create_data["default_org_id"] = default_org_id if status is not None: create_data["status"] = status if user_tier is not None: create_data["user_tier"] = user_tier - + # Should not raise validation error request = UserCreate(**create_data) - + assert request.full_name == full_name assert request.email == email if default_org_id is not None: @@ -315,29 +344,31 @@ def test_user_creation_request_validation(self, full_name, email, default_org_id @given( expand=st.booleans(), page=st.one_of(st.none(), st.integers(min_value=1, max_value=100)), - per_page=st.one_of(st.none(), st.integers(min_value=1, max_value=100)) + per_page=st.one_of(st.none(), st.integers(min_value=1, max_value=100)), ) - def test_list_users_parameter_combinations(self, mock_client, expand, page, per_page): + def test_list_users_parameter_combinations( + self, mock_client, expand, page, per_page + ): """Test listing users with various parameter combinations.""" client = mock_client - + user_data = MockResponseBuilder.user(user_id=123) - + if expand: client.http_client.add_response("GET", "/users?expand=1", [user_data]) else: client.http_client.add_response("GET", "/users", [user_data]) - + kwargs = {"expand": expand} if page is not None: kwargs["page"] = page if per_page is not None: kwargs["per_page"] = per_page - + users = client.users.list(**kwargs) - + assert len(users) == 1 if expand: assert isinstance(users[0], UserExpanded) else: - assert isinstance(users[0], User) + assert isinstance(users[0], User) diff --git a/tests/run_tests.py b/tests/run_tests.py index d6ad951..b88303a 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -5,37 +5,37 @@ Usage: # Run all unit tests (default) python tests/run_tests.py - + # Run specific test categories python tests/run_tests.py --unit python tests/run_tests.py --integration python tests/run_tests.py --property python tests/run_tests.py --models python tests/run_tests.py --performance - + # Run specific resources python tests/run_tests.py --resource credentials python tests/run_tests.py --resource sources - + # Run with various options python tests/run_tests.py --coverage python tests/run_tests.py --parallel python tests/run_tests.py --verbose python tests/run_tests.py --slow - + # Run specific test files python tests/run_tests.py tests/unit/test_credentials.py python tests/run_tests.py tests/integration/test_sources.py - + # Combine options python tests/run_tests.py --unit --resource credentials --coverage python tests/run_tests.py --integration --parallel --verbose """ -import sys -import os import argparse +import os import subprocess +import sys from pathlib import Path from typing import List @@ -45,66 +45,79 @@ # Test categories and their paths TEST_CATEGORIES = { - 'unit': 'tests/unit/', - 'integration': 'tests/integration/', - 'property': 'tests/property/', - 'models': 'tests/models/', - 'performance': 'tests/performance/' + "unit": "tests/unit/", + "integration": "tests/integration/", + "property": "tests/property/", + "models": "tests/models/", + "performance": "tests/performance/", } # Available resources RESOURCES = [ - 'credentials', 'sources', 'destinations', 'nexsets', 'lookups', - 'users', 'organizations', 'teams', 'projects', 'notifications', - 'flows', 'metrics', 'client', 'auth', 'http_client' + "credentials", + "sources", + "destinations", + "nexsets", + "lookups", + "users", + "organizations", + "teams", + "projects", + "notifications", + "flows", + "metrics", + "client", + "auth", + "http_client", ] # Test markers MARKERS = { - 'unit': 'unit', - 'integration': 'integration', - 'property': 'property', - 'performance': 'performance', - 'slow': 'slow', - 'requires_setup': 'requires_setup' + "unit": "unit", + "integration": "integration", + "property": "property", + "performance": "performance", + "slow": "slow", + "requires_setup": "requires_setup", } def check_environment(): """Check if the test environment is properly set up.""" print("🔍 Checking test environment...") - + # Check for required files required_files = [ - 'tests/conftest.py', - 'tests/utils/__init__.py', - 'pytest.ini', - 'requirements.txt' + "tests/conftest.py", + "tests/utils/__init__.py", + "pytest.ini", + "requirements.txt", ] - + missing_files = [] for file_path in required_files: if not Path(file_path).exists(): missing_files.append(file_path) - + if missing_files: print(f"❌ Missing required files: {missing_files}") return False - + # Check for test directories missing_dirs = [] for category, path in TEST_CATEGORIES.items(): if not Path(path).exists(): missing_dirs.append(path) - + if missing_dirs: print(f"❌ Missing test directories: {missing_dirs}") return False - + # Check for dependencies try: import importlib.util - required_deps = ['pytest', 'hypothesis', 'faker'] + + required_deps = ["pytest", "hypothesis", "faker"] for dep in required_deps: if importlib.util.find_spec(dep) is None: raise ImportError(f"Missing {dep}") @@ -113,7 +126,7 @@ def check_environment(): print(f"❌ Missing test dependencies: {e}") print("Run: pip install -r requirements.txt") return False - + print("✅ Test environment is properly set up") return True @@ -121,37 +134,38 @@ def check_environment(): def check_credentials(): """Check if integration test credentials are available.""" print("🔐 Checking integration test credentials...") - + # Check for .env file - env_file = Path('tests/.env') + env_file = Path("tests/.env") if not env_file.exists(): print("⚠️ No .env file found. Integration tests will be skipped.") print("Copy tests/env.template to tests/.env and fill in your credentials.") return False - + # Check for basic credentials from dotenv import load_dotenv + load_dotenv(env_file) - - service_key = os.getenv('NEXLA_TEST_SERVICE_KEY') - access_token = os.getenv('NEXLA_TEST_ACCESS_TOKEN') - + + service_key = os.getenv("NEXLA_TEST_SERVICE_KEY") + access_token = os.getenv("NEXLA_TEST_ACCESS_TOKEN") + if not service_key and not access_token: print("⚠️ No authentication credentials found in .env file.") print("Set NEXLA_TEST_SERVICE_KEY or NEXLA_TEST_ACCESS_TOKEN") return False - + print("✅ Integration test credentials are configured") return True def build_pytest_command(args: argparse.Namespace) -> List[str]: """Build the pytest command based on arguments.""" - cmd = ['python', '-m', 'pytest'] - + cmd = ["python", "-m", "pytest"] + # Add test paths test_paths = [] - + if args.files: # Specific files provided test_paths.extend(args.files) @@ -171,66 +185,68 @@ def build_pytest_command(args: argparse.Namespace) -> List[str]: test_paths.append(resource_file) else: # Default to unit tests - test_paths.append(TEST_CATEGORIES['unit']) - + test_paths.append(TEST_CATEGORIES["unit"]) + if test_paths: cmd.extend(test_paths) - + # Add markers markers = [] if args.unit: - markers.append(MARKERS['unit']) + markers.append(MARKERS["unit"]) if args.integration: - markers.append(MARKERS['integration']) + markers.append(MARKERS["integration"]) if args.property: - markers.append(MARKERS['property']) + markers.append(MARKERS["property"]) if args.performance: - markers.append(MARKERS['performance']) + markers.append(MARKERS["performance"]) if args.slow: - markers.append(MARKERS['slow']) + markers.append(MARKERS["slow"]) if args.requires_setup: - markers.append(MARKERS['requires_setup']) - + markers.append(MARKERS["requires_setup"]) + if markers: - cmd.extend(['-m', ' and '.join(markers)]) + cmd.extend(["-m", " and ".join(markers)]) elif not args.files and not args.category: # Default to unit tests if no markers specified - cmd.extend(['-m', MARKERS['unit']]) - + cmd.extend(["-m", MARKERS["unit"]]) + # Add coverage if args.coverage: - cmd.extend([ - '--cov=nexla_sdk', - '--cov-report=term-missing', - '--cov-report=html:htmlcov', - '--cov-report=xml', - '--cov-fail-under=85' - ]) - + cmd.extend( + [ + "--cov=nexla_sdk", + "--cov-report=term-missing", + "--cov-report=html:htmlcov", + "--cov-report=xml", + "--cov-fail-under=85", + ] + ) + # Add parallel execution if args.parallel: workers = args.workers or os.cpu_count() - cmd.extend(['-n', str(workers)]) - + cmd.extend(["-n", str(workers)]) + # Add verbosity if args.verbose: - cmd.append('-v') + cmd.append("-v") if args.quiet: - cmd.append('-q') - + cmd.append("-q") + # Add other options if args.stop_on_first_failure: - cmd.append('-x') + cmd.append("-x") if args.tb_style: - cmd.extend(['--tb', args.tb_style]) + cmd.extend(["--tb", args.tb_style]) if args.durations: - cmd.extend(['--durations', str(args.durations)]) - + cmd.extend(["--durations", str(args.durations)]) + # Only add hypothesis options if we're running property tests - if args.property or 'property' in str(test_paths): + if args.property or "property" in str(test_paths): if args.hypothesis_examples: - cmd.extend(['--hypothesis-max-examples', str(args.hypothesis_examples)]) - + cmd.extend(["--hypothesis-max-examples", str(args.hypothesis_examples)]) + return cmd @@ -239,18 +255,18 @@ def run_tests(args: argparse.Namespace) -> int: # Check environment if not check_environment(): return 1 - + # Check credentials for integration tests if args.integration and not check_credentials(): print("⚠️ Integration tests require credentials. Skipping.") return 0 - + # Build pytest command cmd = build_pytest_command(args) - + print(f"🚀 Running tests with command: {' '.join(cmd)}") print("=" * 80) - + # Run tests try: result = subprocess.run(cmd, cwd=project_root) @@ -267,22 +283,22 @@ def show_test_summary(): """Show a summary of available tests.""" print("📋 Nexla SDK Test Suite Summary") print("=" * 50) - + total_tests = 0 for category, path in TEST_CATEGORIES.items(): - test_files = list(Path(path).glob('test_*.py')) + test_files = list(Path(path).glob("test_*.py")) count = len(test_files) total_tests += count print(f"{category.title():12} tests: {count:2d} files in {path}") - + print(f"{'Total':12} tests: {total_tests:2d} files") print() - + print("📦 Available Resources:") for i, resource in enumerate(RESOURCES, 1): print(f"{i:2d}. {resource}") print() - + print("🏷️ Available Markers:") for name, marker in MARKERS.items(): print(f" {name:15} - {marker}") @@ -294,81 +310,123 @@ def main(): parser = argparse.ArgumentParser( description="Nexla SDK Test Runner", formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=__doc__ + epilog=__doc__, ) - + # Test categories - test_group = parser.add_argument_group('Test Categories') - test_group.add_argument('--unit', action='store_true', help='Run unit tests') - test_group.add_argument('--integration', action='store_true', help='Run integration tests') - test_group.add_argument('--property', action='store_true', help='Run property-based tests') - test_group.add_argument('--models', action='store_true', help='Run model validation tests') - test_group.add_argument('--performance', action='store_true', help='Run performance tests') - test_group.add_argument('--category', choices=TEST_CATEGORIES.keys(), help='Run specific test category') - + test_group = parser.add_argument_group("Test Categories") + test_group.add_argument("--unit", action="store_true", help="Run unit tests") + test_group.add_argument( + "--integration", action="store_true", help="Run integration tests" + ) + test_group.add_argument( + "--property", action="store_true", help="Run property-based tests" + ) + test_group.add_argument( + "--models", action="store_true", help="Run model validation tests" + ) + test_group.add_argument( + "--performance", action="store_true", help="Run performance tests" + ) + test_group.add_argument( + "--category", choices=TEST_CATEGORIES.keys(), help="Run specific test category" + ) + # Resource selection - resource_group = parser.add_argument_group('Resource Selection') - resource_group.add_argument('--resource', choices=RESOURCES, help='Run tests for specific resource') - resource_group.add_argument('--categories', nargs='+', choices=TEST_CATEGORIES.keys(), - default=list(TEST_CATEGORIES.keys()), - help='Categories to search for resource tests') - + resource_group = parser.add_argument_group("Resource Selection") + resource_group.add_argument( + "--resource", choices=RESOURCES, help="Run tests for specific resource" + ) + resource_group.add_argument( + "--categories", + nargs="+", + choices=TEST_CATEGORIES.keys(), + default=list(TEST_CATEGORIES.keys()), + help="Categories to search for resource tests", + ) + # Test markers - marker_group = parser.add_argument_group('Test Markers') - marker_group.add_argument('--slow', action='store_true', help='Include slow tests') - marker_group.add_argument('--requires-setup', action='store_true', help='Include tests requiring setup') - + marker_group = parser.add_argument_group("Test Markers") + marker_group.add_argument("--slow", action="store_true", help="Include slow tests") + marker_group.add_argument( + "--requires-setup", action="store_true", help="Include tests requiring setup" + ) + # Output options - output_group = parser.add_argument_group('Output Options') - output_group.add_argument('--coverage', action='store_true', help='Generate coverage report') - output_group.add_argument('--verbose', '-v', action='store_true', help='Verbose output') - output_group.add_argument('--quiet', '-q', action='store_true', help='Quiet output') - output_group.add_argument('--tb-style', choices=['short', 'long', 'no'], default='short', - help='Traceback style') - + output_group = parser.add_argument_group("Output Options") + output_group.add_argument( + "--coverage", action="store_true", help="Generate coverage report" + ) + output_group.add_argument( + "--verbose", "-v", action="store_true", help="Verbose output" + ) + output_group.add_argument("--quiet", "-q", action="store_true", help="Quiet output") + output_group.add_argument( + "--tb-style", + choices=["short", "long", "no"], + default="short", + help="Traceback style", + ) + # Execution options - exec_group = parser.add_argument_group('Execution Options') - exec_group.add_argument('--parallel', action='store_true', help='Run tests in parallel') - exec_group.add_argument('--workers', type=int, help='Number of parallel workers') - exec_group.add_argument('--stop-on-first-failure', '-x', action='store_true', - help='Stop on first failure') - exec_group.add_argument('--durations', type=int, default=10, - help='Show slowest N tests') - exec_group.add_argument('--hypothesis-examples', type=int, - help='Number of examples for hypothesis tests') - + exec_group = parser.add_argument_group("Execution Options") + exec_group.add_argument( + "--parallel", action="store_true", help="Run tests in parallel" + ) + exec_group.add_argument("--workers", type=int, help="Number of parallel workers") + exec_group.add_argument( + "--stop-on-first-failure", + "-x", + action="store_true", + help="Stop on first failure", + ) + exec_group.add_argument( + "--durations", type=int, default=10, help="Show slowest N tests" + ) + exec_group.add_argument( + "--hypothesis-examples", + type=int, + help="Number of examples for hypothesis tests", + ) + # Utility options - util_group = parser.add_argument_group('Utility Options') - util_group.add_argument('--check-env', action='store_true', help='Check test environment') - util_group.add_argument('--check-credentials', action='store_true', help='Check integration credentials') - util_group.add_argument('--summary', action='store_true', help='Show test summary') - util_group.add_argument('--list-resources', action='store_true', help='List available resources') - + util_group = parser.add_argument_group("Utility Options") + util_group.add_argument( + "--check-env", action="store_true", help="Check test environment" + ) + util_group.add_argument( + "--check-credentials", action="store_true", help="Check integration credentials" + ) + util_group.add_argument("--summary", action="store_true", help="Show test summary") + util_group.add_argument( + "--list-resources", action="store_true", help="List available resources" + ) + # File arguments - parser.add_argument('files', nargs='*', help='Specific test files to run') - + parser.add_argument("files", nargs="*", help="Specific test files to run") + args = parser.parse_args() - + # Handle utility options if args.check_env: return 0 if check_environment() else 1 - + if args.check_credentials: return 0 if check_credentials() else 1 - + if args.summary: show_test_summary() return 0 - + if args.list_resources: print("Available resources:") for resource in RESOURCES: print(f" - {resource}") return 0 - + # Run tests return run_tests(args) -if __name__ == '__main__': - sys.exit(main()) \ No newline at end of file +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_client_init.py b/tests/test_client_init.py index 0fb76be..f0f7ce3 100644 --- a/tests/test_client_init.py +++ b/tests/test_client_init.py @@ -11,7 +11,9 @@ def test_client_initialization_with_service_key(): client = NexlaClient( - service_key="test_service_key", base_url="http://localhost:8000", api_version="v1" + service_key="test_service_key", + base_url="http://localhost:8000", + api_version="v1", ) assert client.auth_handler.service_key == "test_service_key" assert client.api_url == "http://localhost:8000" @@ -26,7 +28,9 @@ def test_client_initialization_defaults(): def test_client_api_url_strips_trailing_slash(): - client = NexlaClient(service_key="test_service_key", base_url="http://localhost:8000/") + client = NexlaClient( + service_key="test_service_key", base_url="http://localhost:8000/" + ) assert client.api_url == "http://localhost:8000" diff --git a/tests/unit/test_approval_requests.py b/tests/unit/test_approval_requests.py index 30eb5b5..f5f2d6c 100644 --- a/tests/unit/test_approval_requests.py +++ b/tests/unit/test_approval_requests.py @@ -2,7 +2,6 @@ from nexla_sdk import NexlaClient - pytestmark = pytest.mark.unit @@ -28,7 +27,8 @@ def test_lists_and_actions(self, client, mock_http_client): assert ap.id == 2 mock_http_client.clear_responses() - mock_http_client.add_response("/approval_requests/2/reject", {"id": 2, "status": "rejected"}) + mock_http_client.add_response( + "/approval_requests/2/reject", {"id": 2, "status": "rejected"} + ) rj = client.approval_requests.reject(2, reason="not needed") assert rj.id == 2 - diff --git a/tests/unit/test_async_tasks.py b/tests/unit/test_async_tasks.py index 6df9574..e9905e4 100644 --- a/tests/unit/test_async_tasks.py +++ b/tests/unit/test_async_tasks.py @@ -4,7 +4,6 @@ from nexla_sdk.models.async_tasks.requests import AsyncTaskCreate from nexla_sdk.models.async_tasks.responses import AsyncTask, DownloadLink - pytestmark = pytest.mark.unit @@ -14,29 +13,37 @@ def client(mock_client: NexlaClient) -> NexlaClient: class TestAsyncTasksResource: - def test_list_types_create_get_result_download_ack_and_filters(self, client, mock_http_client): + def test_list_types_create_get_result_download_ack_and_filters( + self, client, mock_http_client + ): mock_http_client.add_response("/async_tasks", [{"id": 1, "status": "QUEUED"}]) tasks = client.async_tasks.list() assert isinstance(tasks[0], AsyncTask) mock_http_client.clear_responses() - mock_http_client.add_response("/async_tasks/types", ["BulkDeleteNotifications"]) + mock_http_client.add_response("/async_tasks/types", ["BulkDeleteNotifications"]) types = client.async_tasks.types() assert types[0] == "BulkDeleteNotifications" mock_http_client.clear_responses() - mock_http_client.add_response("/async_tasks/explain_arguments/BulkDeleteNotifications", {"args": []}) + mock_http_client.add_response( + "/async_tasks/explain_arguments/BulkDeleteNotifications", {"args": []} + ) exp = client.async_tasks.explain_arguments("BulkDeleteNotifications") assert "args" in exp mock_http_client.clear_responses() - payload = AsyncTaskCreate(type="BulkDeleteNotifications", arguments={"ids": [1, 2]}) + payload = AsyncTaskCreate( + type="BulkDeleteNotifications", arguments={"ids": [1, 2]} + ) mock_http_client.add_response("/async_tasks", {"id": 5, "status": "QUEUED"}) created = client.async_tasks.create(payload) assert isinstance(created, AsyncTask) mock_http_client.clear_responses() - mock_http_client.add_response("/async_tasks/of_type/BulkDeleteNotifications", [{"id": 5}]) + mock_http_client.add_response( + "/async_tasks/of_type/BulkDeleteNotifications", [{"id": 5}] + ) by_type = client.async_tasks.list_of_type("BulkDeleteNotifications") assert isinstance(by_type[0], AsyncTask) @@ -61,7 +68,9 @@ def test_list_types_create_get_result_download_ack_and_filters(self, client, moc assert isinstance(link1, str) mock_http_client.clear_responses() - mock_http_client.add_response("/async_tasks/5/download_link", {"url": "https://url"}) + mock_http_client.add_response( + "/async_tasks/5/download_link", {"url": "https://url"} + ) link2 = client.async_tasks.download_link(5) assert isinstance(link2, DownloadLink) @@ -74,4 +83,3 @@ def test_list_types_create_get_result_download_ack_and_filters(self, client, moc mock_http_client.add_response("/async_tasks/5", {"status": "deleted"}) deleted = client.async_tasks.delete(5) assert deleted.get("status") == "deleted" - diff --git a/tests/unit/test_attribute_transforms.py b/tests/unit/test_attribute_transforms.py index 47c798e..c77a998 100644 --- a/tests/unit/test_attribute_transforms.py +++ b/tests/unit/test_attribute_transforms.py @@ -1,10 +1,12 @@ import pytest from nexla_sdk import NexlaClient -from nexla_sdk.models.attribute_transforms.requests import AttributeTransformCreate, AttributeTransformUpdate +from nexla_sdk.models.attribute_transforms.requests import ( + AttributeTransformCreate, + AttributeTransformUpdate, +) from nexla_sdk.models.attribute_transforms.responses import AttributeTransform - pytestmark = pytest.mark.unit @@ -15,35 +17,48 @@ def client(mock_client: NexlaClient) -> NexlaClient: class TestAttributeTransformsResource: def test_list_public_get_crud(self, client, mock_http_client): - mock_http_client.add_response("/attribute_transforms", [{"id": 20, "name": "at"}]) + mock_http_client.add_response( + "/attribute_transforms", [{"id": 20, "name": "at"}] + ) out = client.attribute_transforms.list() assert isinstance(out[0], AttributeTransform) mock_http_client.clear_responses() - mock_http_client.add_response("/attribute_transforms/public", [{"id": 21, "name": "ap"}]) + mock_http_client.add_response( + "/attribute_transforms/public", [{"id": 21, "name": "ap"}] + ) pub = client.attribute_transforms.list_public() assert isinstance(pub[0], AttributeTransform) mock_http_client.clear_responses() - mock_http_client.add_response("/attribute_transforms/20", {"id": 20, "name": "at"}) + mock_http_client.add_response( + "/attribute_transforms/20", {"id": 20, "name": "at"} + ) got = client.attribute_transforms.get(20) assert isinstance(got, AttributeTransform) mock_http_client.clear_responses() create = AttributeTransformCreate( - name="at", output_type="json", code_type="python", code_encoding="utf-8", code="return x", + name="at", + output_type="json", + code_type="python", + code_encoding="utf-8", + code="return x", ) mock_http_client.add_response("/attribute_transforms", {"id": 22, "name": "at"}) created = client.attribute_transforms.create(create) assert isinstance(created, AttributeTransform) mock_http_client.clear_responses() - mock_http_client.add_response("/attribute_transforms/22", {"id": 22, "name": "at2"}) - upd = client.attribute_transforms.update(22, AttributeTransformUpdate(name="at2")) + mock_http_client.add_response( + "/attribute_transforms/22", {"id": 22, "name": "at2"} + ) + upd = client.attribute_transforms.update( + 22, AttributeTransformUpdate(name="at2") + ) assert upd.name == "at2" mock_http_client.clear_responses() mock_http_client.add_response("/attribute_transforms/22", {"status": "deleted"}) res = client.attribute_transforms.delete(22) assert res.get("status") == "deleted" - diff --git a/tests/unit/test_auth.py b/tests/unit/test_auth.py index 11be150..3bb67e7 100644 --- a/tests/unit/test_auth.py +++ b/tests/unit/test_auth.py @@ -3,21 +3,27 @@ """ import time + import pytest from nexla_sdk.auth import TokenAuthHandler from nexla_sdk.exceptions import AuthenticationError from tests.utils.fixtures import MockHTTPClient, create_auth_token_response - pytestmark = pytest.mark.unit def test_service_key_obtain_and_ensure_token(): mock_http = MockHTTPClient() - mock_http.add_response("/token", create_auth_token_response(access_token="tk-1", expires_in=60)) + mock_http.add_response( + "/token", create_auth_token_response(access_token="tk-1", expires_in=60) + ) - auth = TokenAuthHandler(service_key="sk-123", base_url="https://api.test/nexla-api", http_client=mock_http) + auth = TokenAuthHandler( + service_key="sk-123", + base_url="https://api.test/nexla-api", + http_client=mock_http, + ) # No token yet; ensure should obtain lazily token = auth.ensure_valid_token() @@ -39,7 +45,12 @@ def token_responder(_req): mock_http.add_response("/token", token_responder) - auth = TokenAuthHandler(service_key="sk-123", base_url="https://api.test/nexla-api", token_refresh_margin=30, http_client=mock_http) + auth = TokenAuthHandler( + service_key="sk-123", + base_url="https://api.test/nexla-api", + token_refresh_margin=30, + http_client=mock_http, + ) token1 = auth.ensure_valid_token() assert token1 == "tk-1" @@ -61,7 +72,9 @@ def test_direct_token_mode_no_refresh_allowed(): def test_execute_authenticated_request_retries_on_401_with_service_key(): mock_http = MockHTTPClient() # Initial token obtain - mock_http.add_response("/token", create_auth_token_response(access_token="tk-0", expires_in=3600)) + mock_http.add_response( + "/token", create_auth_token_response(access_token="tk-0", expires_in=3600) + ) # Endpoint that will fail once with 401 then succeed attempt = {"n": 0} @@ -71,28 +84,43 @@ def flappy(req): attempt["n"] += 1 if attempt["n"] == 1: from nexla_sdk.http_client import HttpClientError - raise HttpClientError("unauthorized", status_code=401, response={"error": "unauthorized"}) + + raise HttpClientError( + "unauthorized", status_code=401, response={"error": "unauthorized"} + ) return {"status": "ok"} return {"status": "unexpected"} mock_http.add_response("/widgets", flappy) - auth = TokenAuthHandler(service_key="sk-xyz", base_url="https://api.test/nexla-api", http_client=mock_http) + auth = TokenAuthHandler( + service_key="sk-xyz", + base_url="https://api.test/nexla-api", + http_client=mock_http, + ) - out = auth.execute_authenticated_request("GET", "https://api.test/nexla-api/widgets", headers={}) + out = auth.execute_authenticated_request( + "GET", "https://api.test/nexla-api/widgets", headers={} + ) assert out == {"status": "ok"} # Ensure we attempted an additional token obtain after 401 # First obtain occurred during first ensure; on 401 we call obtain again # There should be at least one POST /token request recorded - posts = [r for r in mock_http.requests if r["method"] == "POST" and "/token" in r["url"]] + posts = [ + r for r in mock_http.requests if r["method"] == "POST" and "/token" in r["url"] + ] assert len(posts) >= 1 def test_logout_clears_token_and_calls_endpoint(): mock_http = MockHTTPClient() - mock_http.add_response("/token", create_auth_token_response(access_token="tk", expires_in=3600)) + mock_http.add_response( + "/token", create_auth_token_response(access_token="tk", expires_in=3600) + ) mock_http.add_response("/token/logout", {"status": "ok"}) - auth = TokenAuthHandler(service_key="sk-1", base_url="https://api.test/nexla-api", http_client=mock_http) + auth = TokenAuthHandler( + service_key="sk-1", base_url="https://api.test/nexla-api", http_client=mock_http + ) # Obtain a token assert auth.ensure_valid_token() == "tk" diff --git a/tests/unit/test_code_containers.py b/tests/unit/test_code_containers.py index a76242f..dc699a0 100644 --- a/tests/unit/test_code_containers.py +++ b/tests/unit/test_code_containers.py @@ -1,10 +1,12 @@ import pytest from nexla_sdk import NexlaClient -from nexla_sdk.models.code_containers.requests import CodeContainerCreate, CodeContainerUpdate +from nexla_sdk.models.code_containers.requests import ( + CodeContainerCreate, + CodeContainerUpdate, +) from nexla_sdk.models.code_containers.responses import CodeContainer, CodeOperation - pytestmark = pytest.mark.unit @@ -20,7 +22,9 @@ def test_list_public_get_crud_copy(self, client, mock_http_client): assert isinstance(out[0], CodeContainer) mock_http_client.clear_responses() - mock_http_client.add_response("/code_containers/public", [{"id": 2, "name": "pub"}]) + mock_http_client.add_response( + "/code_containers/public", [{"id": 2, "name": "pub"}] + ) pub = client.code_containers.list_public() assert isinstance(pub[0], CodeContainer) @@ -31,7 +35,10 @@ def test_list_public_get_crud_copy(self, client, mock_http_client): mock_http_client.clear_responses() create = CodeContainerCreate( - name="cc", output_type="json", code_type="python", code_encoding="utf-8", + name="cc", + output_type="json", + code_type="python", + code_encoding="utf-8", code=[CodeOperation(operation="map", spec={})], ) mock_http_client.add_response("/code_containers", {"id": 3, "name": "cc"}) @@ -44,7 +51,9 @@ def test_list_public_get_crud_copy(self, client, mock_http_client): assert upd.name == "cc2" mock_http_client.clear_responses() - mock_http_client.add_response("/code_containers/3/copy", {"id": 4, "name": "cc-copy"}) + mock_http_client.add_response( + "/code_containers/3/copy", {"id": 4, "name": "cc-copy"} + ) cp = client.code_containers.copy(3) assert isinstance(cp, CodeContainer) @@ -52,4 +61,3 @@ def test_list_public_get_crud_copy(self, client, mock_http_client): mock_http_client.add_response("/code_containers/4", {"status": "deleted"}) res = client.code_containers.delete(4) assert res.get("status") == "deleted" - diff --git a/tests/unit/test_credentials.py b/tests/unit/test_credentials.py index 5158a20..379383b 100644 --- a/tests/unit/test_credentials.py +++ b/tests/unit/test_credentials.py @@ -5,307 +5,381 @@ from nexla_sdk.exceptions import ( AuthenticationError, - ServerError, - NotFoundError, - NexlaError, AuthorizationError, - ValidationError as SDKValidationError, - ResourceConflictError, + NexlaError, + NotFoundError, RateLimitError, + ResourceConflictError, + ServerError, ) +from nexla_sdk.exceptions import ValidationError as SDKValidationError from nexla_sdk.http_client import HttpClientError -from nexla_sdk.models.credentials.responses import Credential, ProbeTreeResponse, ProbeSampleResponse from nexla_sdk.models.credentials.requests import ( - CredentialCreate, ProbeTreeRequest, ProbeSampleRequest + CredentialCreate, + ProbeSampleRequest, + ProbeTreeRequest, +) +from nexla_sdk.models.credentials.responses import ( + Credential, + ProbeSampleResponse, + ProbeTreeResponse, ) from tests.utils import ( - MockResponseBuilder, create_http_error, assert_model_valid, - assert_model_list_valid + MockResponseBuilder, + assert_model_list_valid, + assert_model_valid, + create_http_error, ) @pytest.mark.unit class TestCredentialsResourceUnit: """Unit tests for CredentialsResource using mocks.""" - - def test_list_credentials_success(self, mock_client, mock_http_client, sample_credentials_list): + + def test_list_credentials_success( + self, mock_client, mock_http_client, sample_credentials_list + ): """Test listing credentials with successful response.""" # Arrange mock_http_client.add_response("/data_credentials", sample_credentials_list) - + # Act credentials = mock_client.credentials.list() - + # Assert assert len(credentials) == 3 assert_model_list_valid(credentials, Credential) mock_http_client.assert_request_made("GET", "/data_credentials") - + # Verify first credential structure first_credential = credentials[0] assert first_credential.id is not None assert first_credential.name is not None assert first_credential.credentials_type is not None - - def test_list_credentials_with_filters(self, mock_client, mock_http_client, sample_credentials_list): + + def test_list_credentials_with_filters( + self, mock_client, mock_http_client, sample_credentials_list + ): """Test listing credentials with filters.""" # Arrange mock_http_client.add_response("/data_credentials", sample_credentials_list) - + # Act credentials = mock_client.credentials.list( - credentials_type="s3", - access_role="owner", - page=1, - per_page=10 + credentials_type="s3", access_role="owner", page=1, per_page=10 ) - + # Assert assert len(credentials) == 3 - + # Verify request parameters request = mock_http_client.get_request() - assert "credentials_type=s3" in request["url"] or \ - request.get("params", {}).get("credentials_type") == "s3" - - def test_get_credential_success(self, mock_client, mock_http_client, sample_credential_response): + assert ( + "credentials_type=s3" in request["url"] + or request.get("params", {}).get("credentials_type") == "s3" + ) + + def test_get_credential_success( + self, mock_client, mock_http_client, sample_credential_response + ): """Test getting a single credential.""" # Arrange credential_id = 123 - mock_http_client.add_response(f"/data_credentials/{credential_id}", sample_credential_response) - + mock_http_client.add_response( + f"/data_credentials/{credential_id}", sample_credential_response + ) + # Act credential = mock_client.credentials.get(credential_id) - + # Assert assert_model_valid(credential, {"id": sample_credential_response["id"]}) - mock_http_client.assert_request_made("GET", f"/data_credentials/{credential_id}") - - def test_get_credential_with_expand(self, mock_client, mock_http_client, sample_credential_response): + mock_http_client.assert_request_made( + "GET", f"/data_credentials/{credential_id}" + ) + + def test_get_credential_with_expand( + self, mock_client, mock_http_client, sample_credential_response + ): """Test getting a credential with expand option.""" # Arrange credential_id = 123 - mock_http_client.add_response(f"/data_credentials/{credential_id}", sample_credential_response) - + mock_http_client.add_response( + f"/data_credentials/{credential_id}", sample_credential_response + ) + # Act credential = mock_client.credentials.get(credential_id, expand=True) - + # Assert assert isinstance(credential, Credential) request = mock_http_client.get_request() # Check for expand parameter - assert "expand=1" in request["url"] or request.get("params", {}).get("expand") == 1 - - def test_create_credential_success(self, mock_client, mock_http_client, sample_credential_data, sample_credential_response): + assert ( + "expand=1" in request["url"] or request.get("params", {}).get("expand") == 1 + ) + + def test_create_credential_success( + self, + mock_client, + mock_http_client, + sample_credential_data, + sample_credential_response, + ): """Test creating a credential successfully.""" # Arrange mock_http_client.add_response("/data_credentials", sample_credential_response) create_request = CredentialCreate(**sample_credential_data) - + # Act credential = mock_client.credentials.create(create_request) - + # Assert assert isinstance(credential, Credential) mock_http_client.assert_request_made("POST", "/data_credentials") - + # Verify the request body request = mock_http_client.get_request() assert "json" in request assert request["json"]["name"] == sample_credential_data["name"] - assert request["json"]["credentials_type"] == sample_credential_data["credentials_type"] - - def test_create_credential_with_dict(self, mock_client, mock_http_client, sample_credential_data, sample_credential_response): + assert ( + request["json"]["credentials_type"] + == sample_credential_data["credentials_type"] + ) + + def test_create_credential_with_dict( + self, + mock_client, + mock_http_client, + sample_credential_data, + sample_credential_response, + ): """Test creating a credential with dict input.""" # Arrange mock_http_client.add_response("/data_credentials", sample_credential_response) - + # Act credential = mock_client.credentials.create(sample_credential_data) - + # Assert assert isinstance(credential, Credential) mock_http_client.assert_request_made("POST", "/data_credentials") - - def test_update_credential_success(self, mock_client, mock_http_client, sample_credential_response): + + def test_update_credential_success( + self, mock_client, mock_http_client, sample_credential_response + ): """Test updating a credential.""" # Arrange credential_id = 123 - update_data = {"name": "Updated Credential Name", "description": "Updated description"} + update_data = { + "name": "Updated Credential Name", + "description": "Updated description", + } updated_response = sample_credential_response.copy() updated_response.update(update_data) - - mock_http_client.add_response(f"/data_credentials/{credential_id}", updated_response) - + + mock_http_client.add_response( + f"/data_credentials/{credential_id}", updated_response + ) + # Act credential = mock_client.credentials.update(credential_id, update_data) - + # Assert assert isinstance(credential, Credential) assert credential.name == update_data["name"] - mock_http_client.assert_request_made("PUT", f"/data_credentials/{credential_id}") - + mock_http_client.assert_request_made( + "PUT", f"/data_credentials/{credential_id}" + ) + def test_delete_credential_success(self, mock_client, mock_http_client): """Test deleting a credential.""" # Arrange credential_id = 123 delete_response = {"status": "success", "message": "Credential deleted"} - mock_http_client.add_response(f"/data_credentials/{credential_id}", delete_response) - + mock_http_client.add_response( + f"/data_credentials/{credential_id}", delete_response + ) + # Act result = mock_client.credentials.delete(credential_id) - + # Assert assert result["status"] == "success" - mock_http_client.assert_request_made("DELETE", f"/data_credentials/{credential_id}") - + mock_http_client.assert_request_made( + "DELETE", f"/data_credentials/{credential_id}" + ) + def test_probe_credential_success(self, mock_client, mock_http_client): """Test probing a credential successfully.""" # Arrange credential_id = 123 probe_response = MockResponseBuilder.probe_response() - mock_http_client.add_response(f"/data_credentials/{credential_id}/probe", probe_response) - + mock_http_client.add_response( + f"/data_credentials/{credential_id}/probe", probe_response + ) + # Act result = mock_client.credentials.probe(credential_id) - + # Assert assert result["status"] == "success" - mock_http_client.assert_request_made("GET", f"/data_credentials/{credential_id}/probe") - + mock_http_client.assert_request_made( + "GET", f"/data_credentials/{credential_id}/probe" + ) + def test_probe_credential_none_response(self, mock_client, mock_http_client): """Test probing a credential with None response.""" # Arrange credential_id = 123 mock_http_client.add_response(f"/data_credentials/{credential_id}/probe", None) - + # Act result = mock_client.credentials.probe(credential_id) - + # Assert assert result["status"] == "success" assert "Credential probe completed successfully" in result["message"] - - def test_probe_tree_success(self, mock_client, mock_http_client, sample_probe_tree_request): + + def test_probe_tree_success( + self, mock_client, mock_http_client, sample_probe_tree_request + ): """Test probing tree structure successfully.""" # Arrange credential_id = 123 tree_response = MockResponseBuilder.probe_tree_response("s3") - mock_http_client.add_response(f"/data_credentials/{credential_id}/probe/tree", tree_response) - + mock_http_client.add_response( + f"/data_credentials/{credential_id}/probe/tree", tree_response + ) + probe_request = ProbeTreeRequest(**sample_probe_tree_request) - + # Act result = mock_client.credentials.probe_tree(credential_id, probe_request) - + # Assert assert isinstance(result, ProbeTreeResponse) assert result.status == "ok" assert result.connection_type == "s3" - mock_http_client.assert_request_made("POST", f"/data_credentials/{credential_id}/probe/tree") - - def test_probe_sample_success(self, mock_client, mock_http_client, sample_probe_sample_request): + mock_http_client.assert_request_made( + "POST", f"/data_credentials/{credential_id}/probe/tree" + ) + + def test_probe_sample_success( + self, mock_client, mock_http_client, sample_probe_sample_request + ): """Test probing sample data successfully.""" # Arrange credential_id = 123 sample_response = MockResponseBuilder.probe_sample_response("s3") - mock_http_client.add_response(f"/data_credentials/{credential_id}/probe/sample", sample_response) - + mock_http_client.add_response( + f"/data_credentials/{credential_id}/probe/sample", sample_response + ) + probe_request = ProbeSampleRequest(**sample_probe_sample_request) - + # Act result = mock_client.credentials.probe_sample(credential_id, probe_request) - + # Assert assert isinstance(result, ProbeSampleResponse) assert result.status == "ok" assert result.connection_type == "s3" - mock_http_client.assert_request_made("POST", f"/data_credentials/{credential_id}/probe/sample") + mock_http_client.assert_request_made( + "POST", f"/data_credentials/{credential_id}/probe/sample" + ) @pytest.mark.unit class TestCredentialsErrorHandling: """Test error handling for credentials operations.""" - + def test_get_credential_not_found(self, mock_client, mock_http_client): """Test getting a non-existent credential.""" # Arrange credential_id = 999 error = create_http_error( - 404, + 404, "Credential not found", - {"resource_type": "credential", "resource_id": str(credential_id)} + {"resource_type": "credential", "resource_id": str(credential_id)}, ) - + # Set up mock to return error for the specific GET request mock_http_client.add_response(f"/data_credentials/{credential_id}", error) - + # Act & Assert with pytest.raises(NotFoundError) as exc_info: mock_client.credentials.get(credential_id) - + assert exc_info.value.resource_id == str(credential_id) - + def test_create_credential_validation_error(self, mock_client): """Test creating credential with invalid data.""" # Arrange - missing required fields invalid_data = {"name": ""} # Empty name - + # Act & Assert with pytest.raises(ValidationError): CredentialCreate(**invalid_data) - + def test_authentication_error_during_list(self, mock_client, mock_http_client): """Test handling authentication errors during API calls.""" # Arrange - auth_error = create_http_error(401, "Authentication failed. Check your service key.") - + auth_error = create_http_error( + 401, "Authentication failed. Check your service key." + ) + # Mock both the credentials list request AND the session token obtain request # to return 401 errors so the retry also fails mock_http_client.add_response("/data_credentials", auth_error) mock_http_client.add_response("/token", auth_error) - + # Act & Assert with pytest.raises(AuthenticationError): mock_client.credentials.list() - + def test_server_error_during_list(self, mock_client, mock_http_client): """Test handling server errors during API calls.""" # Arrange error = create_http_error(500, "Internal server error") mock_http_client.add_response("/data_credentials", error) - + # Act & Assert with pytest.raises(ServerError) as exc_info: mock_client.credentials.list() - + assert exc_info.value.status_code == 500 - - @pytest.mark.parametrize("status_code,expected_exception", [ - (400, SDKValidationError), - (403, AuthorizationError), - (404, NotFoundError), - (409, ResourceConflictError), - (429, RateLimitError), - (500, ServerError), - ]) - def test_various_http_errors_during_list(self, mock_client, mock_http_client, status_code, expected_exception): + + @pytest.mark.parametrize( + "status_code,expected_exception", + [ + (400, SDKValidationError), + (403, AuthorizationError), + (404, NotFoundError), + (409, ResourceConflictError), + (429, RateLimitError), + (500, ServerError), + ], + ) + def test_various_http_errors_during_list( + self, mock_client, mock_http_client, status_code, expected_exception + ): """Test handling of various HTTP error codes during list operations.""" # Arrange error = create_http_error(status_code, f"Error {status_code}") mock_http_client.add_response("/data_credentials", error) - + # Act & Assert with pytest.raises(expected_exception): mock_client.credentials.list() - + def test_network_error_simulation(self, mock_client, mock_http_client): """Test handling of network-level errors.""" - + # Arrange - simulate a network error network_error = HttpClientError("Connection timeout") mock_http_client.add_response("/data_credentials", network_error) - + # Act & Assert with pytest.raises(NexlaError): mock_client.credentials.list() @@ -314,63 +388,62 @@ def test_network_error_simulation(self, mock_client, mock_http_client): @pytest.mark.unit class TestCredentialsModels: """Test credential model validation and serialization.""" - + def test_credential_model_creation(self, sample_credential_response): """Test creating a Credential model from response data.""" # Act credential = Credential(**sample_credential_response) - + # Assert assert credential.id == sample_credential_response["id"] assert credential.name == sample_credential_response["name"] - assert credential.credentials_type == sample_credential_response["credentials_type"] - + assert ( + credential.credentials_type + == sample_credential_response["credentials_type"] + ) + def test_credential_model_with_missing_optional_fields(self): """Test creating a Credential model with minimal data.""" # Arrange - minimal_data = { - "id": 123, - "name": "Test Credential", - "credentials_type": "s3" - } - + minimal_data = {"id": 123, "name": "Test Credential", "credentials_type": "s3"} + # Act credential = Credential(**minimal_data) - + # Assert assert credential.id == 123 assert credential.name == "Test Credential" assert credential.credentials_type == "s3" assert credential.description is None assert credential.tags == [] # Default factory should provide empty list - + def test_credential_create_model_validation(self): """Test CredentialCreate model validation.""" # Valid data valid_data = { "name": "Test Credential", "credentials_type": "s3", - "credentials": {"access_key": "test", "secret_key": "test"} + "credentials": {"access_key": "test", "secret_key": "test"}, } - + # Act & Assert - should not raise credential_create = CredentialCreate(**valid_data) assert credential_create.name == "Test Credential" assert credential_create.credentials_type == "s3" - + def test_credential_create_missing_required_fields(self): """Test CredentialCreate with missing required fields.""" # Arrange - missing name invalid_data = {"credentials_type": "s3"} - + # Act & Assert with pytest.raises(ValidationError) as exc_info: CredentialCreate(**invalid_data) - + # Check that the error mentions the missing field error_details = str(exc_info.value) assert "name" in error_details - + def test_probe_tree_request_model(self): """Test ProbeTreeRequest model.""" # Test with file system data @@ -378,43 +451,43 @@ def test_probe_tree_request_model(self): probe_request = ProbeTreeRequest(**file_data) assert probe_request.depth == 3 assert probe_request.path == "/test/path" - + # Test with database data db_data = {"depth": 2, "database": "testdb", "table": "testtable"} probe_request = ProbeTreeRequest(**db_data) assert probe_request.depth == 2 assert probe_request.database == "testdb" assert probe_request.table == "testtable" - + def test_probe_sample_request_model(self): """Test ProbeSampleRequest model.""" # Test with path only path_data = {"path": "/test/file.json"} probe_request = ProbeSampleRequest(**path_data) assert probe_request.path == "/test/file.json" - + def test_model_serialization(self, sample_credential_response): """Test model serialization to dict and JSON.""" # Act credential = Credential(**sample_credential_response) - + # Test to_dict credential_dict = credential.to_dict() assert isinstance(credential_dict, dict) assert credential_dict["id"] == sample_credential_response["id"] - + # Test to_json credential_json = credential.to_json() assert isinstance(credential_json, str) assert str(sample_credential_response["id"]) in credential_json - + def test_model_string_representation(self, sample_credential_response): """Test model string representation.""" # Act credential = Credential(**sample_credential_response) - + # Assert str_repr = str(credential) assert "Credential" in str_repr assert str(credential.id) in str_repr - assert credential.name in str_repr + assert credential.name in str_repr diff --git a/tests/unit/test_data_schemas.py b/tests/unit/test_data_schemas.py index ca4ce1f..2a39e8f 100644 --- a/tests/unit/test_data_schemas.py +++ b/tests/unit/test_data_schemas.py @@ -3,7 +3,6 @@ from nexla_sdk import NexlaClient from nexla_sdk.models.common import LogEntry - pytestmark = pytest.mark.unit @@ -34,4 +33,3 @@ def test_audit_log(self, client, mock_http_client): out = client.data_schemas.get_audit_log(9) assert isinstance(out[0], LogEntry) mock_http_client.assert_request_made("GET", "/data_schemas/9/audit_log") - diff --git a/tests/unit/test_destinations.py b/tests/unit/test_destinations.py index 0296ae5..ebaa4b7 100644 --- a/tests/unit/test_destinations.py +++ b/tests/unit/test_destinations.py @@ -1,12 +1,17 @@ """Unit tests for destinations resource.""" + import pytest -from nexla_sdk.models.destinations.responses import Destination -from nexla_sdk.models.destinations.requests import DestinationCreate, DestinationUpdate, DestinationCopyOptions -from nexla_sdk.exceptions import ServerError, NotFoundError +from nexla_sdk.exceptions import NotFoundError, ServerError from nexla_sdk.http_client import HttpClientError +from nexla_sdk.models.destinations.requests import ( + DestinationCopyOptions, + DestinationCreate, + DestinationUpdate, +) +from nexla_sdk.models.destinations.responses import Destination +from tests.utils.assertions import assert_model_list_valid from tests.utils.mock_builders import MockResponseBuilder -from tests.utils.assertions import NexlaAssertions, assert_model_list_valid @pytest.mark.unit @@ -18,7 +23,7 @@ def test_list_destinations(self, mock_client): # Arrange mock_destinations = [ MockResponseBuilder.destination({"id": 1, "name": "Dest 1"}), - MockResponseBuilder.destination({"id": 2, "name": "Dest 2"}) + MockResponseBuilder.destination({"id": 2, "name": "Dest 2"}), ] mock_client.http_client.add_response("/data_sinks", mock_destinations) @@ -38,9 +43,7 @@ def test_list_destinations_with_parameters(self, mock_client): # Act destinations = mock_client.destinations.list( - page=2, - per_page=50, - access_role="owner" + page=2, per_page=50, access_role="owner" ) # Assert @@ -57,8 +60,12 @@ def test_get_destination(self, mock_client): """Test getting single destination.""" # Arrange destination_id = 12345 - mock_response = MockResponseBuilder.destination({"id": destination_id, "name": "Test Destination"}) - mock_client.http_client.add_response(f"/data_sinks/{destination_id}", mock_response) + mock_response = MockResponseBuilder.destination( + {"id": destination_id, "name": "Test Destination"} + ) + mock_client.http_client.add_response( + f"/data_sinks/{destination_id}", mock_response + ) # Act destination = mock_client.destinations.get(destination_id) @@ -67,18 +74,24 @@ def test_get_destination(self, mock_client): assert isinstance(destination, Destination) assert destination.id == destination_id assert destination.name == "Test Destination" - mock_client.http_client.assert_request_made("GET", f"/data_sinks/{destination_id}") + mock_client.http_client.assert_request_made( + "GET", f"/data_sinks/{destination_id}" + ) def test_get_destination_with_expand(self, mock_client): """Test getting destination with expand parameter.""" # Arrange destination_id = 12345 - mock_response = MockResponseBuilder.destination({ - "id": destination_id, - "name": "Test Destination", - "data_set": MockResponseBuilder.data_set_info() - }) - mock_client.http_client.add_response(f"/data_sinks/{destination_id}", mock_response) + mock_response = MockResponseBuilder.destination( + { + "id": destination_id, + "name": "Test Destination", + "data_set": MockResponseBuilder.data_set_info(), + } + ) + mock_client.http_client.add_response( + f"/data_sinks/{destination_id}", mock_response + ) # Act destination = mock_client.destinations.get(destination_id, expand=True) @@ -86,7 +99,9 @@ def test_get_destination_with_expand(self, mock_client): # Assert assert isinstance(destination, Destination) assert destination.id == destination_id - mock_client.http_client.assert_request_made("GET", f"/data_sinks/{destination_id}") + mock_client.http_client.assert_request_made( + "GET", f"/data_sinks/{destination_id}" + ) # Verify expand parameter was sent request = mock_client.http_client.get_last_request() @@ -100,13 +115,11 @@ def test_create_destination(self, mock_client): sink_type="s3", data_credentials_id=100, data_set_id=200, - description="Test description" + description="Test description", + ) + mock_response = MockResponseBuilder.destination( + {"id": 12345, "name": "Test Destination", "sink_type": "s3"} ) - mock_response = MockResponseBuilder.destination({ - "id": 12345, - "name": "Test Destination", - "sink_type": "s3" - }) mock_client.http_client.add_response("/data_sinks", mock_response) # Act @@ -128,14 +141,14 @@ def test_update_destination(self, mock_client): # Arrange destination_id = 12345 update_data = DestinationUpdate( - name="Updated Destination", - description="Updated description" + name="Updated Destination", description="Updated description" + ) + mock_response = MockResponseBuilder.destination( + {"id": destination_id, "name": "Updated Destination"} + ) + mock_client.http_client.add_response( + f"/data_sinks/{destination_id}", mock_response ) - mock_response = MockResponseBuilder.destination({ - "id": destination_id, - "name": "Updated Destination" - }) - mock_client.http_client.add_response(f"/data_sinks/{destination_id}", mock_response) # Act destination = mock_client.destinations.update(destination_id, update_data) @@ -143,30 +156,37 @@ def test_update_destination(self, mock_client): # Assert assert isinstance(destination, Destination) assert destination.name == "Updated Destination" - mock_client.http_client.assert_request_made("PUT", f"/data_sinks/{destination_id}") + mock_client.http_client.assert_request_made( + "PUT", f"/data_sinks/{destination_id}" + ) def test_delete_destination(self, mock_client): """Test deleting destination.""" # Arrange destination_id = 12345 - mock_client.http_client.add_response(f"/data_sinks/{destination_id}", {"status": "deleted"}) + mock_client.http_client.add_response( + f"/data_sinks/{destination_id}", {"status": "deleted"} + ) # Act result = mock_client.destinations.delete(destination_id) # Assert assert result == {"status": "deleted"} - mock_client.http_client.assert_request_made("DELETE", f"/data_sinks/{destination_id}") + mock_client.http_client.assert_request_made( + "DELETE", f"/data_sinks/{destination_id}" + ) def test_activate_destination(self, mock_client): """Test activating destination.""" # Arrange destination_id = 12345 - mock_response = MockResponseBuilder.destination({ - "id": destination_id, - "status": "ACTIVE" - }) - mock_client.http_client.add_response(f"/data_sinks/{destination_id}/activate", mock_response) + mock_response = MockResponseBuilder.destination( + {"id": destination_id, "status": "ACTIVE"} + ) + mock_client.http_client.add_response( + f"/data_sinks/{destination_id}/activate", mock_response + ) # Act destination = mock_client.destinations.activate(destination_id) @@ -174,17 +194,20 @@ def test_activate_destination(self, mock_client): # Assert assert isinstance(destination, Destination) assert destination.status == "ACTIVE" - mock_client.http_client.assert_request_made("PUT", f"/data_sinks/{destination_id}/activate") + mock_client.http_client.assert_request_made( + "PUT", f"/data_sinks/{destination_id}/activate" + ) def test_pause_destination(self, mock_client): """Test pausing destination.""" # Arrange destination_id = 12345 - mock_response = MockResponseBuilder.destination({ - "id": destination_id, - "status": "PAUSED" - }) - mock_client.http_client.add_response(f"/data_sinks/{destination_id}/pause", mock_response) + mock_response = MockResponseBuilder.destination( + {"id": destination_id, "status": "PAUSED"} + ) + mock_client.http_client.add_response( + f"/data_sinks/{destination_id}/pause", mock_response + ) # Act destination = mock_client.destinations.pause(destination_id) @@ -192,21 +215,23 @@ def test_pause_destination(self, mock_client): # Assert assert isinstance(destination, Destination) assert destination.status == "PAUSED" - mock_client.http_client.assert_request_made("PUT", f"/data_sinks/{destination_id}/pause") + mock_client.http_client.assert_request_made( + "PUT", f"/data_sinks/{destination_id}/pause" + ) def test_copy_destination(self, mock_client): """Test copying destination.""" # Arrange destination_id = 12345 copy_options = DestinationCopyOptions( - reuse_data_credentials=True, - copy_access_controls=False + reuse_data_credentials=True, copy_access_controls=False + ) + mock_response = MockResponseBuilder.destination( + {"id": 54321, "name": "Copied Destination"} + ) + mock_client.http_client.add_response( + f"/data_sinks/{destination_id}/copy", mock_response ) - mock_response = MockResponseBuilder.destination({ - "id": 54321, - "name": "Copied Destination" - }) - mock_client.http_client.add_response(f"/data_sinks/{destination_id}/copy", mock_response) # Act destination = mock_client.destinations.copy(destination_id, copy_options) @@ -214,7 +239,9 @@ def test_copy_destination(self, mock_client): # Assert assert isinstance(destination, Destination) assert destination.id == 54321 - mock_client.http_client.assert_request_made("POST", f"/data_sinks/{destination_id}/copy") + mock_client.http_client.assert_request_made( + "POST", f"/data_sinks/{destination_id}/copy" + ) def test_http_error_handling(self, mock_client): """Test HTTP error handling.""" @@ -224,8 +251,8 @@ def test_http_error_handling(self, mock_client): HttpClientError( "Server Error", status_code=500, - response={"message": "Internal server error"} - ) + response={"message": "Internal server error"}, + ), ) # Act & Assert @@ -243,8 +270,8 @@ def test_not_found_error(self, mock_client): HttpClientError( "Not found", status_code=404, - response={"message": "Destination not found"} - ) + response={"message": "Destination not found"}, + ), ) # Act & Assert diff --git a/tests/unit/test_doc_containers.py b/tests/unit/test_doc_containers.py index 9198cc5..acab2f1 100644 --- a/tests/unit/test_doc_containers.py +++ b/tests/unit/test_doc_containers.py @@ -3,7 +3,6 @@ from nexla_sdk import NexlaClient from nexla_sdk.models.common import LogEntry - pytestmark = pytest.mark.unit @@ -34,4 +33,3 @@ def test_audit_log(self, client, mock_http_client): out = client.doc_containers.get_audit_log(10) assert isinstance(out[0], LogEntry) mock_http_client.assert_request_made("GET", "/doc_containers/10/audit_log") - diff --git a/tests/unit/test_flows.py b/tests/unit/test_flows.py index 9413602..dc5f9c8 100644 --- a/tests/unit/test_flows.py +++ b/tests/unit/test_flows.py @@ -1,20 +1,24 @@ """Unit tests for flows resource.""" + +from unittest.mock import patch + import pytest -from unittest.mock import MagicMock, patch from nexla_sdk import NexlaClient -from nexla_sdk.models.flows.responses import ( - FlowResponse, FlowMetrics, FlowLogsResponse, FlowMetricsApiResponse, - DocsRecommendation -) -from nexla_sdk.models.flows.requests import FlowCopyOptions -from nexla_sdk.models.common import FlowNode from nexla_sdk.exceptions import ServerError from nexla_sdk.http_client import HttpClientError - +from nexla_sdk.models.common import FlowNode +from nexla_sdk.models.flows.requests import FlowCopyOptions +from nexla_sdk.models.flows.responses import ( + DocsRecommendation, + FlowLogsResponse, + FlowMetrics, + FlowMetricsApiResponse, + FlowResponse, +) +from tests.utils.assertions import NexlaAssertions from tests.utils.fixtures import MockHTTPClient from tests.utils.mock_builders import MockDataFactory, MockResponseBuilder -from tests.utils.assertions import NexlaAssertions pytestmark = pytest.mark.unit @@ -56,8 +60,7 @@ def test_flow_metrics_api_response_model(self): def test_docs_recommendation_model(self): """Test DocsRecommendation model.""" response_data = MockResponseBuilder.docs_recommendation_response( - recommendation="Test recommendation", - status="success" + recommendation="Test recommendation", status="success" ) response = DocsRecommendation.model_validate(response_data) assert response.recommendation == "Test recommendation" @@ -85,7 +88,9 @@ def mock_http_client(self) -> MockHTTPClient: def mock_client(self, mock_http_client) -> NexlaClient: """Create a test client with mocked HTTP and access token auth.""" # Use access_token to avoid token fetch call - with patch('nexla_sdk.client.RequestsHttpClient', return_value=mock_http_client): + with patch( + "nexla_sdk.client.RequestsHttpClient", return_value=mock_http_client + ): client = NexlaClient(access_token="test-access-token") client.http_client = mock_http_client return client @@ -132,7 +137,9 @@ def test_list_flows_with_params(self, mock_client, mock_http_client, mock_factor assert last_request["params"]["flows_only"] == 1 assert last_request["params"]["include_run_metrics"] == 1 - def test_list_flows_with_access_role(self, mock_client, mock_http_client, mock_factory): + def test_list_flows_with_access_role( + self, mock_client, mock_http_client, mock_factory + ): """Test listing flows with access_role parameter.""" # Arrange mock_response = mock_factory.create_mock_flow_response() @@ -171,7 +178,9 @@ def test_get_flow_by_resource(self, mock_client, mock_http_client, mock_factory) resource_type = "data_sources" resource_id = 5023 mock_response = mock_factory.create_mock_flow_response() - mock_http_client.add_response(f"/{resource_type}/{resource_id}/flow", mock_response) + mock_http_client.add_response( + f"/{resource_type}/{resource_id}/flow", mock_response + ) # Act flow = mock_client.flows.get_by_resource(resource_type, resource_id) @@ -257,7 +266,7 @@ def test_copy_flow(self, mock_client, mock_http_client, mock_factory): copy_access_controls=True, copy_dependent_data_flows=False, owner_id=123, - org_id=456 + org_id=456, ) mock_response = mock_factory.create_mock_flow_response() mock_http_client.add_response(f"/flows/{flow_id}/copy", mock_response) @@ -301,17 +310,15 @@ def test_delete_flow_active_error(self, mock_client, mock_http_client): error_response = { "data_sources": [5023], "data_sets": [5059, 5061, 5062], - "message": "Active flow resources must be paused before flow deletion!" + "message": "Active flow resources must be paused before flow deletion!", } # Mock the HTTP client to raise HttpClientError mock_http_client.add_error( f"/flows/{flow_id}", HttpClientError( - "Method not allowed", - status_code=405, - response=error_response - ) + "Method not allowed", status_code=405, response=error_response + ), ) # Act & Assert @@ -327,7 +334,9 @@ def test_delete_by_resource(self, mock_client, mock_http_client): resource_type = "data_sources" resource_id = 5023 mock_response = {"status": "ok"} - mock_http_client.add_response(f"/{resource_type}/{resource_id}/flow", mock_response) + mock_http_client.add_response( + f"/{resource_type}/{resource_id}/flow", mock_response + ) # Act result = mock_client.flows.delete_by_resource(resource_type, resource_id) @@ -346,10 +355,14 @@ def test_activate_by_resource(self, mock_client, mock_http_client, mock_factory) resource_type = "data_sets" resource_id = 5061 mock_response = mock_factory.create_mock_flow_response() - mock_http_client.add_response(f"/{resource_type}/{resource_id}/activate", mock_response) + mock_http_client.add_response( + f"/{resource_type}/{resource_id}/activate", mock_response + ) # Act - flow = mock_client.flows.activate_by_resource(resource_type, resource_id, all=True) + flow = mock_client.flows.activate_by_resource( + resource_type, resource_id, all=True + ) # Assert assert isinstance(flow, FlowResponse) @@ -366,7 +379,9 @@ def test_pause_by_resource(self, mock_client, mock_http_client, mock_factory): resource_type = "data_sinks" resource_id = 5029 mock_response = mock_factory.create_mock_flow_response() - mock_http_client.add_response(f"/{resource_type}/{resource_id}/pause", mock_response) + mock_http_client.add_response( + f"/{resource_type}/{resource_id}/pause", mock_response + ) # Act flow = mock_client.flows.pause_by_resource(resource_type, resource_id) @@ -402,11 +417,7 @@ def test_flow_node_parsing(self, mock_client, mock_http_client, mock_factory): """Test parsing of nested flow node structure.""" # Arrange # Create a deep flow structure - mock_response = { - "flows": [ - mock_factory.create_mock_flow_node(max_depth=4) - ] - } + mock_response = {"flows": [mock_factory.create_mock_flow_node(max_depth=4)]} mock_http_client.add_response("/flows", mock_response) # Act @@ -449,7 +460,7 @@ def test_validation_error_handling(self, mock_client, mock_http_client): { # Missing required 'id' field "parent_data_set_id": None, - "data_source": {"id": 123} + "data_source": {"id": 123}, } ] } @@ -457,6 +468,7 @@ def test_validation_error_handling(self, mock_client, mock_http_client): # Act & Assert from pydantic import ValidationError + with pytest.raises(ValidationError) as exc_info: mock_client.flows.list() @@ -471,16 +483,20 @@ def test_docs_recommendation_success(self, mock_client, mock_http_client): flow_id = 5059 mock_response = MockResponseBuilder.docs_recommendation_response( recommendation="This flow ingests data from S3 and transforms it.", - status="success" + status="success", + ) + mock_http_client.add_response( + f"/flows/{flow_id}/docs/recommendation", mock_response ) - mock_http_client.add_response(f"/flows/{flow_id}/docs/recommendation", mock_response) # Act result = mock_client.flows.docs_recommendation(flow_id) # Assert assert isinstance(result, DocsRecommendation) - assert result.recommendation == "This flow ingests data from S3 and transforms it." + assert ( + result.recommendation == "This flow ingests data from S3 and transforms it." + ) assert result.status == "success" # Verify request @@ -496,14 +512,16 @@ def test_get_logs_success(self, mock_client, mock_http_client): run_id = 12345 from_ts = 1704067200 mock_response = MockResponseBuilder.flow_logs_response(log_count=3) - mock_http_client.add_response(f"/data_flows/{resource_type}/{resource_id}/logs", mock_response) + mock_http_client.add_response( + f"/data_flows/{resource_type}/{resource_id}/logs", mock_response + ) # Act result = mock_client.flows.get_logs( resource_type=resource_type, resource_id=resource_id, run_id=run_id, - from_ts=from_ts + from_ts=from_ts, ) # Assert @@ -532,7 +550,7 @@ def test_get_logs_with_pagination(self, mock_client, mock_http_client): run_id=100, from_ts=1704067200, page=2, - per_page=25 + per_page=25, ) # Assert @@ -554,7 +572,7 @@ def test_get_logs_all_parameters(self, mock_client, mock_http_client): from_ts=1704067200, to_ts=1704153600, page=1, - per_page=50 + per_page=50, ) # Assert @@ -572,13 +590,13 @@ def test_get_metrics_success(self, mock_client, mock_http_client): resource_id = 5023 from_date = "2024-01-01" mock_response = MockResponseBuilder.flow_metrics_api_response() - mock_http_client.add_response(f"/data_flows/{resource_type}/{resource_id}/metrics", mock_response) + mock_http_client.add_response( + f"/data_flows/{resource_type}/{resource_id}/metrics", mock_response + ) # Act result = mock_client.flows.get_metrics( - resource_type=resource_type, - resource_id=resource_id, - from_date=from_date + resource_type=resource_type, resource_id=resource_id, from_date=from_date ) # Assert @@ -590,21 +608,25 @@ def test_get_metrics_success(self, mock_client, mock_http_client): # Verify request last_request = mock_http_client.get_last_request() assert last_request["method"] == "GET" - assert f"/data_flows/{resource_type}/{resource_id}/metrics" in last_request["url"] + assert ( + f"/data_flows/{resource_type}/{resource_id}/metrics" in last_request["url"] + ) assert last_request["params"]["from"] == from_date def test_get_metrics_with_groupby(self, mock_client, mock_http_client): """Test get_metrics with groupby parameter.""" # Arrange mock_response = MockResponseBuilder.flow_metrics_api_response() - mock_http_client.add_response("/data_flows/data_sets/5061/metrics", mock_response) + mock_http_client.add_response( + "/data_flows/data_sets/5061/metrics", mock_response + ) # Act mock_client.flows.get_metrics( resource_type="data_sets", resource_id=5061, from_date="2024-01-01", - groupby="runId" + groupby="runId", ) # Assert @@ -615,14 +637,16 @@ def test_get_metrics_with_orderby(self, mock_client, mock_http_client): """Test get_metrics with orderby parameter.""" # Arrange mock_response = MockResponseBuilder.flow_metrics_api_response() - mock_http_client.add_response("/data_flows/data_sets/5061/metrics", mock_response) + mock_http_client.add_response( + "/data_flows/data_sets/5061/metrics", mock_response + ) # Act mock_client.flows.get_metrics( resource_type="data_sets", resource_id=5061, from_date="2024-01-01", - orderby="created_at" + orderby="created_at", ) # Assert @@ -633,7 +657,9 @@ def test_get_metrics_all_parameters(self, mock_client, mock_http_client): """Test get_metrics with all parameters.""" # Arrange mock_response = MockResponseBuilder.flow_metrics_api_response() - mock_http_client.add_response("/data_flows/data_sinks/5029/metrics", mock_response) + mock_http_client.add_response( + "/data_flows/data_sinks/5029/metrics", mock_response + ) # Act mock_client.flows.get_metrics( @@ -644,7 +670,7 @@ def test_get_metrics_all_parameters(self, mock_client, mock_http_client): groupby="runId", orderby="created_at", page=2, - per_page=100 + per_page=100, ) # Assert diff --git a/tests/unit/test_genai.py b/tests/unit/test_genai.py index 5fc9a61..5fd9ecb 100644 --- a/tests/unit/test_genai.py +++ b/tests/unit/test_genai.py @@ -2,10 +2,15 @@ from nexla_sdk import NexlaClient from nexla_sdk.models.genai.requests import ( - GenAiConfigCreatePayload, GenAiConfigPayload, GenAiOrgSettingPayload, + GenAiConfigCreatePayload, + GenAiConfigPayload, + GenAiOrgSettingPayload, +) +from nexla_sdk.models.genai.responses import ( + ActiveConfigView, + GenAiConfig, + GenAiOrgSetting, ) -from nexla_sdk.models.genai.responses import GenAiConfig, GenAiOrgSetting, ActiveConfigView - pytestmark = pytest.mark.unit @@ -17,45 +22,66 @@ def client(mock_client: NexlaClient) -> NexlaClient: class TestGenAIResource: def test_configs_crud(self, client, mock_http_client): - mock_http_client.add_response("/gen_ai_integration_configs", [{"id": 10, "name": "OpenAI"}]) + mock_http_client.add_response( + "/gen_ai_integration_configs", [{"id": 10, "name": "OpenAI"}] + ) cfgs = client.genai.list_configs() assert isinstance(cfgs[0], GenAiConfig) and cfgs[0].id == 10 mock_http_client.clear_responses() - create_payload = GenAiConfigCreatePayload(name="OpenAI", type="genai_openai", config={"api_key": "x"}, data_credentials_id=1) - mock_http_client.add_response("/gen_ai_integration_configs", {"id": 11, "name": "OpenAI"}) + create_payload = GenAiConfigCreatePayload( + name="OpenAI", + type="genai_openai", + config={"api_key": "x"}, + data_credentials_id=1, + ) + mock_http_client.add_response( + "/gen_ai_integration_configs", {"id": 11, "name": "OpenAI"} + ) created = client.genai.create_config(create_payload) assert isinstance(created, GenAiConfig) and created.id == 11 mock_http_client.clear_responses() - mock_http_client.add_response("/gen_ai_integration_configs/11", {"id": 11, "name": "OpenAI"}) + mock_http_client.add_response( + "/gen_ai_integration_configs/11", {"id": 11, "name": "OpenAI"} + ) got = client.genai.get_config(11) assert got.id == 11 mock_http_client.clear_responses() update_payload = GenAiConfigPayload(description="desc") - mock_http_client.add_response("/gen_ai_integration_configs/11", {"id": 11, "name": "OpenAI-2"}) + mock_http_client.add_response( + "/gen_ai_integration_configs/11", {"id": 11, "name": "OpenAI-2"} + ) upd = client.genai.update_config(11, update_payload) assert upd.name == "OpenAI-2" mock_http_client.clear_responses() - mock_http_client.add_response("/gen_ai_integration_configs/11", {"status": "ok"}) + mock_http_client.add_response( + "/gen_ai_integration_configs/11", {"status": "ok"} + ) d = client.genai.delete_config(11) assert d.get("status") == "ok" def test_org_settings_and_active(self, client, mock_http_client): - mock_http_client.add_response("/gen_ai_org_settings", [{"id": 100, "gen_ai_usage": "all"}]) + mock_http_client.add_response( + "/gen_ai_org_settings", [{"id": 100, "gen_ai_usage": "all"}] + ) items = client.genai.list_org_settings(org_id=9, all=True) assert isinstance(items[0], GenAiOrgSetting) mock_http_client.clear_responses() payload = GenAiOrgSettingPayload(gen_ai_config_id=11, gen_ai_usage="all") - mock_http_client.add_response("/gen_ai_org_settings", {"id": 101, "gen_ai_usage": "all"}) + mock_http_client.add_response( + "/gen_ai_org_settings", {"id": 101, "gen_ai_usage": "all"} + ) created = client.genai.create_org_setting(payload) assert isinstance(created, GenAiOrgSetting) and created.id == 101 mock_http_client.clear_responses() - mock_http_client.add_response("/gen_ai_org_settings/101", {"id": 101, "gen_ai_usage": "all"}) + mock_http_client.add_response( + "/gen_ai_org_settings/101", {"id": 101, "gen_ai_usage": "all"} + ) got = client.genai.get_org_setting(101) assert got.id == 101 @@ -65,7 +91,9 @@ def test_org_settings_and_active(self, client, mock_http_client): assert d.get("status") == "ok" mock_http_client.clear_responses() - mock_http_client.add_response("/gen_ai_org_settings/active_config", {"gen_ai_usage": "all", "active_config": {}}) + mock_http_client.add_response( + "/gen_ai_org_settings/active_config", + {"gen_ai_usage": "all", "active_config": {}}, + ) view = client.genai.show_active_config("all") assert isinstance(view, ActiveConfigView) and view.gen_ai_usage == "all" - diff --git a/tests/unit/test_lookups.py b/tests/unit/test_lookups.py index 484da31..6df6572 100644 --- a/tests/unit/test_lookups.py +++ b/tests/unit/test_lookups.py @@ -1,13 +1,13 @@ """Unit tests for lookups resource.""" + import pytest from pydantic import ValidationError -from nexla_sdk.models.lookups.responses import Lookup -from nexla_sdk.models.lookups.requests import LookupCreate, LookupUpdate -from nexla_sdk.exceptions import ServerError, NotFoundError +from nexla_sdk.exceptions import NotFoundError from nexla_sdk.http_client import HttpClientError -from tests.utils.mock_builders import MockResponseBuilder, MockDataFactory -from tests.utils.assertions import NexlaAssertions, assert_model_list_valid +from nexla_sdk.models.lookups.requests import LookupCreate, LookupUpdate +from nexla_sdk.models.lookups.responses import Lookup +from tests.utils.mock_builders import MockDataFactory @pytest.mark.unit @@ -20,7 +20,7 @@ def test_list_lookups(self, mock_client): mock_factory = MockDataFactory() mock_lookups = [ mock_factory.create_mock_lookup(id=1001, name="Event Code Lookup"), - mock_factory.create_mock_lookup(id=1002, name="Status Code Lookup") + mock_factory.create_mock_lookup(id=1002, name="Status Code Lookup"), ] mock_client.http_client.add_response("/data_maps", mock_lookups) @@ -40,7 +40,9 @@ def test_list_lookups_with_parameters(self, mock_client): mock_client.http_client.add_response("/data_maps", mock_lookups) # Act - result = mock_client.lookups.list(page=2, per_page=50, access_role="collaborator") + result = mock_client.lookups.list( + page=2, per_page=50, access_role="collaborator" + ) # Assert assert len(result) == 1 @@ -57,7 +59,9 @@ def test_get_lookup(self, mock_client): # Arrange lookup_id = 1001 mock_factory = MockDataFactory() - mock_lookup = mock_factory.create_mock_lookup(id=lookup_id, name="Event Code Lookup") + mock_lookup = mock_factory.create_mock_lookup( + id=lookup_id, name="Event Code Lookup" + ) mock_client.http_client.add_response(f"/data_maps/{lookup_id}", mock_lookup) # Act @@ -96,7 +100,7 @@ def test_create_lookup(self, mock_client): map_primary_key="eventId", description="Maps event IDs to descriptions", data_defaults={"eventId": "Unknown", "description": "Unknown Event"}, - emit_data_default=True + emit_data_default=True, ) mock_factory = MockDataFactory() @@ -104,7 +108,7 @@ def test_create_lookup(self, mock_client): id=1003, name="New Event Lookup", data_type="string", - map_primary_key="eventId" + map_primary_key="eventId", ) mock_client.http_client.add_response("/data_maps", mock_lookup) @@ -129,14 +133,12 @@ def test_update_lookup(self, mock_client): update_data = LookupUpdate( name="Updated Event Lookup", description="Updated description", - emit_data_default=False + emit_data_default=False, ) mock_factory = MockDataFactory() mock_lookup = mock_factory.create_mock_lookup( - id=lookup_id, - name="Updated Event Lookup", - description="Updated description" + id=lookup_id, name="Updated Event Lookup", description="Updated description" ) mock_client.http_client.add_response(f"/data_maps/{lookup_id}", mock_lookup) @@ -152,7 +154,9 @@ def test_delete_lookup(self, mock_client): """Test deleting a lookup.""" # Arrange lookup_id = 1001 - mock_client.http_client.add_response(f"/data_maps/{lookup_id}", {"status": "deleted"}) + mock_client.http_client.add_response( + f"/data_maps/{lookup_id}", {"status": "deleted"} + ) # Act result = mock_client.lookups.delete(lookup_id) @@ -167,14 +171,16 @@ def test_upsert_entries(self, mock_client): lookup_id = 1001 entries = [ {"eventId": "001", "description": "Login", "category": "Auth"}, - {"eventId": "002", "description": "Logout", "category": "Auth"} + {"eventId": "002", "description": "Logout", "category": "Auth"}, ] mock_response = [ {"eventId": "001", "description": "Login", "category": "Auth"}, - {"eventId": "002", "description": "Logout", "category": "Auth"} + {"eventId": "002", "description": "Logout", "category": "Auth"}, ] - mock_client.http_client.add_response(f"/data_maps/{lookup_id}/entries", mock_response) + mock_client.http_client.add_response( + f"/data_maps/{lookup_id}/entries", mock_response + ) # Act result = mock_client.lookups.upsert_entries(lookup_id, entries) @@ -182,17 +188,19 @@ def test_upsert_entries(self, mock_client): # Assert assert result == mock_response assert len(result) == 2 - mock_client.http_client.assert_request_made("PUT", f"/data_maps/{lookup_id}/entries") + mock_client.http_client.assert_request_made( + "PUT", f"/data_maps/{lookup_id}/entries" + ) def test_get_entries_single_key(self, mock_client): """Test getting specific entries by single key.""" # Arrange lookup_id = 1001 entry_key = "001" - mock_response = [ - {"eventId": "001", "description": "Login", "category": "Auth"} - ] - mock_client.http_client.add_response(f"/data_maps/{lookup_id}/entries/{entry_key}", mock_response) + mock_response = [{"eventId": "001", "description": "Login", "category": "Auth"}] + mock_client.http_client.add_response( + f"/data_maps/{lookup_id}/entries/{entry_key}", mock_response + ) # Act result = mock_client.lookups.get_entries(lookup_id, entry_key) @@ -200,7 +208,9 @@ def test_get_entries_single_key(self, mock_client): # Assert assert result == mock_response assert len(result) == 1 - mock_client.http_client.assert_request_made("GET", f"/data_maps/{lookup_id}/entries/{entry_key}") + mock_client.http_client.assert_request_made( + "GET", f"/data_maps/{lookup_id}/entries/{entry_key}" + ) def test_get_entries_multiple_keys(self, mock_client): """Test getting specific entries by multiple keys.""" @@ -209,9 +219,11 @@ def test_get_entries_multiple_keys(self, mock_client): entry_keys = ["001", "002"] mock_response = [ {"eventId": "001", "description": "Login", "category": "Auth"}, - {"eventId": "002", "description": "Logout", "category": "Auth"} + {"eventId": "002", "description": "Logout", "category": "Auth"}, ] - mock_client.http_client.add_response(f"/data_maps/{lookup_id}/entries/001,002", mock_response) + mock_client.http_client.add_response( + f"/data_maps/{lookup_id}/entries/001,002", mock_response + ) # Act result = mock_client.lookups.get_entries(lookup_id, entry_keys) @@ -219,35 +231,45 @@ def test_get_entries_multiple_keys(self, mock_client): # Assert assert result == mock_response assert len(result) == 2 - mock_client.http_client.assert_request_made("GET", f"/data_maps/{lookup_id}/entries/001,002") + mock_client.http_client.assert_request_made( + "GET", f"/data_maps/{lookup_id}/entries/001,002" + ) def test_delete_entries_single_key(self, mock_client): """Test deleting specific entries by single key.""" # Arrange lookup_id = 1001 entry_key = "001" - mock_client.http_client.add_response(f"/data_maps/{lookup_id}/entries/{entry_key}", {"status": "deleted"}) + mock_client.http_client.add_response( + f"/data_maps/{lookup_id}/entries/{entry_key}", {"status": "deleted"} + ) # Act result = mock_client.lookups.delete_entries(lookup_id, entry_key) # Assert assert result == {"status": "deleted"} - mock_client.http_client.assert_request_made("DELETE", f"/data_maps/{lookup_id}/entries/{entry_key}") + mock_client.http_client.assert_request_made( + "DELETE", f"/data_maps/{lookup_id}/entries/{entry_key}" + ) def test_delete_entries_multiple_keys(self, mock_client): """Test deleting specific entries by multiple keys.""" # Arrange lookup_id = 1001 entry_keys = ["001", "002"] - mock_client.http_client.add_response(f"/data_maps/{lookup_id}/entries/001,002", {"status": "deleted"}) + mock_client.http_client.add_response( + f"/data_maps/{lookup_id}/entries/001,002", {"status": "deleted"} + ) # Act result = mock_client.lookups.delete_entries(lookup_id, entry_keys) # Assert assert result == {"status": "deleted"} - mock_client.http_client.assert_request_made("DELETE", f"/data_maps/{lookup_id}/entries/001,002") + mock_client.http_client.assert_request_made( + "DELETE", f"/data_maps/{lookup_id}/entries/001,002" + ) def test_http_error_handling(self, mock_client): """Test HTTP error handling.""" @@ -255,10 +277,8 @@ def test_http_error_handling(self, mock_client): mock_client.http_client.add_error( "/data_maps/9999", HttpClientError( - "Not found", - status_code=404, - response={"message": "Lookup not found"} - ) + "Not found", status_code=404, response={"message": "Lookup not found"} + ), ) # Act & Assert @@ -271,7 +291,7 @@ def test_validation_error_handling(self, mock_client): invalid_response = { # Missing required 'id' field "name": "Invalid Lookup", - "map_primary_key": "key" + "map_primary_key": "key", } mock_client.http_client.add_response("/data_maps/1001", invalid_response) diff --git a/tests/unit/test_marketplace.py b/tests/unit/test_marketplace.py index dc446f1..716c464 100644 --- a/tests/unit/test_marketplace.py +++ b/tests/unit/test_marketplace.py @@ -2,13 +2,15 @@ from nexla_sdk import NexlaClient from nexla_sdk.models.marketplace.requests import ( - MarketplaceDomainCreate, MarketplaceDomainsItemCreate, CustodiansPayload, + CustodiansPayload, + MarketplaceDomainCreate, + MarketplaceDomainsItemCreate, ) from nexla_sdk.models.marketplace.responses import ( - MarketplaceDomain, MarketplaceDomainsItem, + MarketplaceDomain, + MarketplaceDomainsItem, ) - pytestmark = pytest.mark.unit @@ -19,28 +21,38 @@ def client(mock_client: NexlaClient) -> NexlaClient: class TestMarketplaceResource: def test_domains_items_and_custodians(self, client, mock_http_client): - mock_http_client.add_response("/marketplace/domains", [{"id": 1, "name": "Dom"}]) + mock_http_client.add_response( + "/marketplace/domains", [{"id": 1, "name": "Dom"}] + ) doms = client.marketplace.list_domains() assert isinstance(doms[0], MarketplaceDomain) mock_http_client.clear_responses() payload = MarketplaceDomainCreate(name="New") - mock_http_client.add_response("/marketplace/domains", [{"id": 2, "name": "New"}]) + mock_http_client.add_response( + "/marketplace/domains", [{"id": 2, "name": "New"}] + ) doms_created = client.marketplace.create_domains(payload) assert isinstance(doms_created[0], MarketplaceDomain) mock_http_client.clear_responses() - mock_http_client.add_response("/marketplace/domains/for_org", [{"id": 1, "name": "Dom"}]) + mock_http_client.add_response( + "/marketplace/domains/for_org", [{"id": 1, "name": "Dom"}] + ) by_org = client.marketplace.get_domains_for_org(5) assert isinstance(by_org[0], MarketplaceDomain) mock_http_client.clear_responses() - mock_http_client.add_response("/marketplace/domains/2", {"id": 2, "name": "New"}) + mock_http_client.add_response( + "/marketplace/domains/2", {"id": 2, "name": "New"} + ) got = client.marketplace.get_domain(2) assert isinstance(got, MarketplaceDomain) mock_http_client.clear_responses() - mock_http_client.add_response("/marketplace/domains/2", {"id": 2, "name": "Upd"}) + mock_http_client.add_response( + "/marketplace/domains/2", {"id": 2, "name": "Upd"} + ) upd = client.marketplace.update_domain(2, payload) assert isinstance(upd, MarketplaceDomain) @@ -77,7 +89,8 @@ def test_domains_items_and_custodians(self, client, mock_http_client): assert isinstance(add_c, list) mock_http_client.clear_responses() - mock_http_client.add_response("/marketplace/domains/1/custodians", {"status": "ok"}) + mock_http_client.add_response( + "/marketplace/domains/1/custodians", {"status": "ok"} + ) rem_c = client.marketplace.remove_domain_custodians(1, cust_payload) assert rem_c.get("status") == "ok" - diff --git a/tests/unit/test_metrics.py b/tests/unit/test_metrics.py index fc1c598..9595afb 100644 --- a/tests/unit/test_metrics.py +++ b/tests/unit/test_metrics.py @@ -2,8 +2,7 @@ from nexla_sdk import NexlaClient from nexla_sdk.models.metrics.enums import ResourceType -from nexla_sdk.models.metrics.responses import MetricsResponse, MetricsByRunResponse - +from nexla_sdk.models.metrics.responses import MetricsByRunResponse, MetricsResponse pytestmark = pytest.mark.unit @@ -14,16 +13,34 @@ def client(mock_client: NexlaClient) -> NexlaClient: class TestMetricsResource: - def test_resource_metrics_rate_limits_and_flow_helpers(self, client, mock_http_client): + def test_resource_metrics_rate_limits_and_flow_helpers( + self, client, mock_http_client + ): mock_http_client.queue_response({"status": 200, "metrics": []}) - m = client.metrics.get_resource_daily_metrics(ResourceType.DATA_SOURCES.value, 42, from_date="2024-01-01", to_date="2024-01-31") + m = client.metrics.get_resource_daily_metrics( + ResourceType.DATA_SOURCES.value, + 42, + from_date="2024-01-01", + to_date="2024-01-31", + ) assert isinstance(m, MetricsResponse) mock_http_client.assert_request_made("GET", "/data_sources/42/metrics") - mock_http_client.queue_response({"status": 200, "metrics": {"data": [], "meta": {}}}) - br = client.metrics.get_resource_metrics_by_run(ResourceType.DATA_SOURCES.value, 42, groupby="runId", orderby="lastWritten", page=1, size=10) + mock_http_client.queue_response( + {"status": 200, "metrics": {"data": [], "meta": {}}} + ) + br = client.metrics.get_resource_metrics_by_run( + ResourceType.DATA_SOURCES.value, + 42, + groupby="runId", + orderby="lastWritten", + page=1, + size=10, + ) assert isinstance(br, MetricsByRunResponse) - mock_http_client.assert_request_made("GET", "/data_sources/42/metrics/run_summary") + mock_http_client.assert_request_made( + "GET", "/data_sources/42/metrics/run_summary" + ) mock_http_client.clear_responses() mock_http_client.add_response("/limits", {"rate_limit": {"limit": 1000}}) @@ -31,12 +48,32 @@ def test_resource_metrics_rate_limits_and_flow_helpers(self, client, mock_http_c assert "rate_limit" in rl mock_http_client.clear_responses() - mock_http_client.add_response("/data_flows/data_sources/1/metrics", {"status": "ok"}) - fm = client.metrics.get_flow_metrics("data_sources", 1, from_date="2024-01-01", to_date="2024-01-31", groupby="runId", orderby="lastWritten", page=1, per_page=50) + mock_http_client.add_response( + "/data_flows/data_sources/1/metrics", {"status": "ok"} + ) + fm = client.metrics.get_flow_metrics( + "data_sources", + 1, + from_date="2024-01-01", + to_date="2024-01-31", + groupby="runId", + orderby="lastWritten", + page=1, + per_page=50, + ) assert fm.get("status") == "ok" mock_http_client.clear_responses() - mock_http_client.add_response("/data_flows/data_sources/1/logs", {"status": "ok"}) - fl = client.metrics.get_flow_logs("data_sources", 1, run_id=123, from_ts=1000, to_ts=2000, page=1, per_page=100) + mock_http_client.add_response( + "/data_flows/data_sources/1/logs", {"status": "ok"} + ) + fl = client.metrics.get_flow_logs( + "data_sources", + 1, + run_id=123, + from_ts=1000, + to_ts=2000, + page=1, + per_page=100, + ) assert fl.get("status") == "ok" - diff --git a/tests/unit/test_nexsets.py b/tests/unit/test_nexsets.py index 8ede29b..98909c4 100644 --- a/tests/unit/test_nexsets.py +++ b/tests/unit/test_nexsets.py @@ -1,13 +1,17 @@ """Unit tests for nexsets resource.""" + import pytest from pydantic import ValidationError -from nexla_sdk.models.nexsets.responses import Nexset, NexsetSample -from nexla_sdk.models.nexsets.requests import NexsetCreate, NexsetUpdate, NexsetCopyOptions -from nexla_sdk.exceptions import ServerError, NotFoundError +from nexla_sdk.exceptions import NotFoundError, ServerError from nexla_sdk.http_client import HttpClientError +from nexla_sdk.models.nexsets.requests import ( + NexsetCopyOptions, + NexsetCreate, + NexsetUpdate, +) +from nexla_sdk.models.nexsets.responses import Nexset from tests.utils.mock_builders import MockDataFactory -from tests.utils.assertions import NexlaAssertions, assert_model_list_valid @pytest.mark.unit @@ -53,7 +57,9 @@ def test_get_nexset(self, mock_client): # Arrange nexset_id = 1001 mock_factory = MockDataFactory() - mock_response = mock_factory.create_mock_nexset(id=nexset_id, name="Test Dataset") + mock_response = mock_factory.create_mock_nexset( + id=nexset_id, name="Test Dataset" + ) mock_client.http_client.add_response(f"/data_sets/{nexset_id}", mock_response) # Act @@ -88,7 +94,7 @@ def test_create_nexset(self, mock_client): name="New Dataset", parent_data_set_id=2001, has_custom_transform=True, - description="Test dataset creation" + description="Test dataset creation", ) mock_factory = MockDataFactory() mock_response = mock_factory.create_mock_nexset(id=1001, name="New Dataset") @@ -112,11 +118,12 @@ def test_update_nexset(self, mock_client): # Arrange nexset_id = 1001 update_data = NexsetUpdate( - name="Updated Dataset", - description="Updated description" + name="Updated Dataset", description="Updated description" ) mock_factory = MockDataFactory() - mock_response = mock_factory.create_mock_nexset(id=nexset_id, name="Updated Dataset") + mock_response = mock_factory.create_mock_nexset( + id=nexset_id, name="Updated Dataset" + ) mock_client.http_client.add_response(f"/data_sets/{nexset_id}", mock_response) # Act @@ -147,7 +154,9 @@ def test_activate_nexset(self, mock_client): nexset_id = 1001 mock_factory = MockDataFactory() mock_response = mock_factory.create_mock_nexset(id=nexset_id, status="ACTIVE") - mock_client.http_client.add_response(f"/data_sets/{nexset_id}/activate", mock_response) + mock_client.http_client.add_response( + f"/data_sets/{nexset_id}/activate", mock_response + ) # Act nexset = mock_client.nexsets.activate(nexset_id) @@ -155,7 +164,9 @@ def test_activate_nexset(self, mock_client): # Assert assert isinstance(nexset, Nexset) assert nexset.status == "ACTIVE" - mock_client.http_client.assert_request_made("PUT", f"/data_sets/{nexset_id}/activate") + mock_client.http_client.assert_request_made( + "PUT", f"/data_sets/{nexset_id}/activate" + ) def test_pause_nexset(self, mock_client): """Test pausing nexset.""" @@ -163,7 +174,9 @@ def test_pause_nexset(self, mock_client): nexset_id = 1001 mock_factory = MockDataFactory() mock_response = mock_factory.create_mock_nexset(id=nexset_id, status="PAUSED") - mock_client.http_client.add_response(f"/data_sets/{nexset_id}/pause", mock_response) + mock_client.http_client.add_response( + f"/data_sets/{nexset_id}/pause", mock_response + ) # Act nexset = mock_client.nexsets.pause(nexset_id) @@ -171,7 +184,9 @@ def test_pause_nexset(self, mock_client): # Assert assert isinstance(nexset, Nexset) assert nexset.status == "PAUSED" - mock_client.http_client.assert_request_made("PUT", f"/data_sets/{nexset_id}/pause") + mock_client.http_client.assert_request_made( + "PUT", f"/data_sets/{nexset_id}/pause" + ) def test_get_samples(self, mock_client): """Test getting nexset samples.""" @@ -181,14 +196,20 @@ def test_get_samples(self, mock_client): mock_sample1 = mock_factory.create_mock_nexset_sample() mock_sample2 = mock_factory.create_mock_nexset_sample() mock_response = [mock_sample1, mock_sample2] - mock_client.http_client.add_response(f"/data_sets/{nexset_id}/samples", mock_response) + mock_client.http_client.add_response( + f"/data_sets/{nexset_id}/samples", mock_response + ) # Act - samples = mock_client.nexsets.get_samples(nexset_id, count=5, include_metadata=True) + samples = mock_client.nexsets.get_samples( + nexset_id, count=5, include_metadata=True + ) # Assert assert len(samples) == 2 - mock_client.http_client.assert_request_made("GET", f"/data_sets/{nexset_id}/samples") + mock_client.http_client.assert_request_made( + "GET", f"/data_sets/{nexset_id}/samples" + ) # Verify parameters request = mock_client.http_client.get_last_request() @@ -201,13 +222,17 @@ def test_get_samples_with_live_option(self, mock_client): nexset_id = 1001 mock_factory = MockDataFactory() mock_response = [mock_factory.create_mock_nexset_sample()] - mock_client.http_client.add_response(f"/data_sets/{nexset_id}/samples", mock_response) + mock_client.http_client.add_response( + f"/data_sets/{nexset_id}/samples", mock_response + ) # Act mock_client.nexsets.get_samples(nexset_id, live=True) # Assert - mock_client.http_client.assert_request_made("GET", f"/data_sets/{nexset_id}/samples") + mock_client.http_client.assert_request_made( + "GET", f"/data_sets/{nexset_id}/samples" + ) request = mock_client.http_client.get_last_request() assert request["params"].get("live") == True @@ -215,13 +240,14 @@ def test_copy_nexset(self, mock_client): """Test copying nexset.""" # Arrange nexset_id = 1001 - copy_options = NexsetCopyOptions( - copy_access_controls=True, - owner_id=123 - ) + copy_options = NexsetCopyOptions(copy_access_controls=True, owner_id=123) mock_factory = MockDataFactory() - mock_response = mock_factory.create_mock_nexset(id=1002, copied_from_id=nexset_id) - mock_client.http_client.add_response(f"/data_sets/{nexset_id}/copy", mock_response) + mock_response = mock_factory.create_mock_nexset( + id=1002, copied_from_id=nexset_id + ) + mock_client.http_client.add_response( + f"/data_sets/{nexset_id}/copy", mock_response + ) # Act copied_nexset = mock_client.nexsets.copy(nexset_id, copy_options) @@ -229,7 +255,9 @@ def test_copy_nexset(self, mock_client): # Assert assert isinstance(copied_nexset, Nexset) assert copied_nexset.id == 1002 - mock_client.http_client.assert_request_made("POST", f"/data_sets/{nexset_id}/copy") + mock_client.http_client.assert_request_made( + "POST", f"/data_sets/{nexset_id}/copy" + ) def test_copy_nexset_without_options(self, mock_client): """Test copying nexset without options.""" @@ -237,13 +265,17 @@ def test_copy_nexset_without_options(self, mock_client): nexset_id = 1001 mock_factory = MockDataFactory() mock_response = mock_factory.create_mock_nexset(id=1002) - mock_client.http_client.add_response(f"/data_sets/{nexset_id}/copy", mock_response) + mock_client.http_client.add_response( + f"/data_sets/{nexset_id}/copy", mock_response + ) # Act mock_client.nexsets.copy(nexset_id) # Assert - mock_client.http_client.assert_request_made("POST", f"/data_sets/{nexset_id}/copy") + mock_client.http_client.assert_request_made( + "POST", f"/data_sets/{nexset_id}/copy" + ) def test_http_error_handling(self, mock_client): """Test HTTP error handling.""" @@ -253,8 +285,8 @@ def test_http_error_handling(self, mock_client): HttpClientError( "Server Error", status_code=500, - response={"message": "Internal server error"} - ) + response={"message": "Internal server error"}, + ), ) # Act & Assert @@ -270,10 +302,8 @@ def test_not_found_error(self, mock_client): mock_client.http_client.add_error( f"/data_sets/{nexset_id}", HttpClientError( - "Not found", - status_code=404, - response={"message": "Nexset not found"} - ) + "Not found", status_code=404, response={"message": "Nexset not found"} + ), ) # Act & Assert diff --git a/tests/unit/test_notifications.py b/tests/unit/test_notifications.py index a1cb805..0939f46 100644 --- a/tests/unit/test_notifications.py +++ b/tests/unit/test_notifications.py @@ -2,15 +2,19 @@ from nexla_sdk import NexlaClient from nexla_sdk.models.notifications.requests import ( - NotificationChannelSettingCreate, NotificationChannelSettingUpdate, - NotificationSettingCreate, NotificationSettingUpdate, + NotificationChannelSettingCreate, + NotificationChannelSettingUpdate, + NotificationSettingCreate, + NotificationSettingUpdate, ) from nexla_sdk.models.notifications.responses import ( - Notification, NotificationType, NotificationChannelSetting, - NotificationSetting, NotificationCount, + Notification, + NotificationChannelSetting, + NotificationCount, + NotificationSetting, + NotificationType, ) - pytestmark = pytest.mark.unit @@ -21,18 +25,25 @@ def client(mock_client: NexlaClient) -> NexlaClient: class TestNotificationsResource: def test_notifications_listing_and_bulk_ops(self, client, mock_http_client): - mock_http_client.add_response("/notifications", [{ - "id": 1, - "owner": {"id": 1, "full_name": "A", "email": "a@b.com"}, - "org": {"id": 1, "name": "Org"}, - "access_roles": ["owner"], - "level": "ERROR", - "resource_id": 7, - "resource_type": "SOURCE", - "message_id": 2, - "message": "...", - }]) - out = client.notifications.list(read=0, level="ERROR", from_timestamp=1, to_timestamp=2, page=1, per_page=10) + mock_http_client.add_response( + "/notifications", + [ + { + "id": 1, + "owner": {"id": 1, "full_name": "A", "email": "a@b.com"}, + "org": {"id": 1, "name": "Org"}, + "access_roles": ["owner"], + "level": "ERROR", + "resource_id": 7, + "resource_type": "SOURCE", + "message_id": 2, + "message": "...", + } + ], + ) + out = client.notifications.list( + read=0, level="ERROR", from_timestamp=1, to_timestamp=2, page=1, per_page=10 + ) assert isinstance(out[0], Notification) mock_http_client.clear_responses() @@ -61,128 +72,192 @@ def test_notifications_listing_and_bulk_ops(self, client, mock_http_client): assert ur.get("status") == "ok" def test_notification_types_and_settings(self, client, mock_http_client): - mock_http_client.add_response("/notification_types", [{ - "id": 1, "name": "Flow", "description": "", "category": "SYSTEM", "default": True, - "status": True, "event_type": "X", "resource_type": "SOURCE" - }]) + mock_http_client.add_response( + "/notification_types", + [ + { + "id": 1, + "name": "Flow", + "description": "", + "category": "SYSTEM", + "default": True, + "status": True, + "event_type": "X", + "resource_type": "SOURCE", + } + ], + ) types = client.notifications.get_types(status="ACTIVE") assert isinstance(types[0], NotificationType) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_types/list", { - "id": 2, "name": "Flow", "description": "", "category": "SYSTEM", "default": True, - "status": True, "event_type": "X", "resource_type": "SOURCE" - }) + mock_http_client.add_response( + "/notification_types/list", + { + "id": 2, + "name": "Flow", + "description": "", + "category": "SYSTEM", + "default": True, + "status": True, + "event_type": "X", + "resource_type": "SOURCE", + }, + ) t = client.notifications.get_type(event_type="X", resource_type="SOURCE") assert isinstance(t, NotificationType) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_channel_settings", [{"id": 1, "owner_id": 1, "org_id": 1, "channel": "APP", "config": {}}]) + mock_http_client.add_response( + "/notification_channel_settings", + [{"id": 1, "owner_id": 1, "org_id": 1, "channel": "APP", "config": {}}], + ) ch = client.notifications.list_channel_settings() assert isinstance(ch[0], NotificationChannelSetting) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_channel_settings", {"id": 2, "owner_id": 1, "org_id": 1, "channel": "EMAIL", "config": {}}) - ch_created = client.notifications.create_channel_setting(NotificationChannelSettingCreate(channel="EMAIL", config={})) + mock_http_client.add_response( + "/notification_channel_settings", + {"id": 2, "owner_id": 1, "org_id": 1, "channel": "EMAIL", "config": {}}, + ) + ch_created = client.notifications.create_channel_setting( + NotificationChannelSettingCreate(channel="EMAIL", config={}) + ) assert isinstance(ch_created, NotificationChannelSetting) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_channel_settings/2", {"id": 2, "owner_id": 1, "org_id": 1, "channel": "EMAIL", "config": {}}) + mock_http_client.add_response( + "/notification_channel_settings/2", + {"id": 2, "owner_id": 1, "org_id": 1, "channel": "EMAIL", "config": {}}, + ) ch_get = client.notifications.get_channel_setting(2) assert isinstance(ch_get, NotificationChannelSetting) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_channel_settings/2", {"id": 2, "owner_id": 1, "org_id": 1, "channel": "EMAIL", "config": {"on": True}}) - ch_upd = client.notifications.update_channel_setting(2, NotificationChannelSettingUpdate(config={"on": True})) + mock_http_client.add_response( + "/notification_channel_settings/2", + { + "id": 2, + "owner_id": 1, + "org_id": 1, + "channel": "EMAIL", + "config": {"on": True}, + }, + ) + ch_upd = client.notifications.update_channel_setting( + 2, NotificationChannelSettingUpdate(config={"on": True}) + ) assert isinstance(ch_upd, NotificationChannelSetting) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_channel_settings/2", {"status": "deleted"}) + mock_http_client.add_response( + "/notification_channel_settings/2", {"status": "deleted"} + ) ch_del = client.notifications.delete_channel_setting(2) assert ch_del.get("status") == "deleted" mock_http_client.clear_responses() - mock_http_client.add_response("/notification_settings", [{ - "id": 1, - "org_id": 1, - "owner_id": 1, - "channel": "APP", - "notification_resource_type": "SOURCE", - "resource_id": 1, - "status": "ACTIVE", - "notification_type_id": 1, - "name": "n", - "description": "d", - "code": 0, - "category": "SYSTEM", - "event_type": "X", - "resource_type": "SOURCE", - "config": {}, - }]) - lst = client.notifications.list_settings(event_type="X", resource_type="SOURCE", status="ACTIVE") + mock_http_client.add_response( + "/notification_settings", + [ + { + "id": 1, + "org_id": 1, + "owner_id": 1, + "channel": "APP", + "notification_resource_type": "SOURCE", + "resource_id": 1, + "status": "ACTIVE", + "notification_type_id": 1, + "name": "n", + "description": "d", + "code": 0, + "category": "SYSTEM", + "event_type": "X", + "resource_type": "SOURCE", + "config": {}, + } + ], + ) + lst = client.notifications.list_settings( + event_type="X", resource_type="SOURCE", status="ACTIVE" + ) assert isinstance(lst[0], NotificationSetting) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_settings", { - "id": 2, - "org_id": 1, - "owner_id": 1, - "channel": "APP", - "notification_resource_type": "SOURCE", - "resource_id": 1, - "status": "ACTIVE", - "notification_type_id": 1, - "name": "n", - "description": "d", - "code": 0, - "category": "SYSTEM", - "event_type": "X", - "resource_type": "SOURCE", - "config": {}, - }) - st_created = client.notifications.create_setting(NotificationSettingCreate(channel="APP", notification_type_id=1, config={})) + mock_http_client.add_response( + "/notification_settings", + { + "id": 2, + "org_id": 1, + "owner_id": 1, + "channel": "APP", + "notification_resource_type": "SOURCE", + "resource_id": 1, + "status": "ACTIVE", + "notification_type_id": 1, + "name": "n", + "description": "d", + "code": 0, + "category": "SYSTEM", + "event_type": "X", + "resource_type": "SOURCE", + "config": {}, + }, + ) + st_created = client.notifications.create_setting( + NotificationSettingCreate(channel="APP", notification_type_id=1, config={}) + ) assert isinstance(st_created, NotificationSetting) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_settings/2", { - "id": 2, - "org_id": 1, - "owner_id": 1, - "channel": "APP", - "notification_resource_type": "SOURCE", - "resource_id": 1, - "status": "ACTIVE", - "notification_type_id": 1, - "name": "n", - "description": "d", - "code": 0, - "category": "SYSTEM", - "event_type": "X", - "resource_type": "SOURCE", - "config": {}, - }) + mock_http_client.add_response( + "/notification_settings/2", + { + "id": 2, + "org_id": 1, + "owner_id": 1, + "channel": "APP", + "notification_resource_type": "SOURCE", + "resource_id": 1, + "status": "ACTIVE", + "notification_type_id": 1, + "name": "n", + "description": "d", + "code": 0, + "category": "SYSTEM", + "event_type": "X", + "resource_type": "SOURCE", + "config": {}, + }, + ) st_get = client.notifications.get_setting(2) assert isinstance(st_get, NotificationSetting) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_settings/2", { - "id": 2, - "org_id": 1, - "owner_id": 1, - "channel": "APP", - "notification_resource_type": "SOURCE", - "resource_id": 1, - "status": "PAUSED", - "notification_type_id": 1, - "name": "n", - "description": "d", - "code": 0, - "category": "SYSTEM", - "event_type": "X", - "resource_type": "SOURCE", - "config": {}, - }) - st_upd = client.notifications.update_setting(2, NotificationSettingUpdate(status="PAUSED")) + mock_http_client.add_response( + "/notification_settings/2", + { + "id": 2, + "org_id": 1, + "owner_id": 1, + "channel": "APP", + "notification_resource_type": "SOURCE", + "resource_id": 1, + "status": "PAUSED", + "notification_type_id": 1, + "name": "n", + "description": "d", + "code": 0, + "category": "SYSTEM", + "event_type": "X", + "resource_type": "SOURCE", + "config": {}, + }, + ) + st_upd = client.notifications.update_setting( + 2, NotificationSettingUpdate(status="PAUSED") + ) assert isinstance(st_upd, NotificationSetting) mock_http_client.clear_responses() @@ -191,12 +266,17 @@ def test_notification_types_and_settings(self, client, mock_http_client): assert st_del.get("status") == "deleted" mock_http_client.clear_responses() - mock_http_client.add_response("/notification_settings/notification_types/1", [st_get.model_dump()]) + mock_http_client.add_response( + "/notification_settings/notification_types/1", [st_get.model_dump()] + ) lst2 = client.notifications.get_settings_by_type(1, expand=True) assert isinstance(lst2[0], NotificationSetting) mock_http_client.clear_responses() - mock_http_client.add_response("/notification_settings/SOURCE/1", [st_get.model_dump()]) - lst3 = client.notifications.get_resource_settings("SOURCE", 1, expand=True, filter_overridden=True, notification_type_id=1) + mock_http_client.add_response( + "/notification_settings/SOURCE/1", [st_get.model_dump()] + ) + lst3 = client.notifications.get_resource_settings( + "SOURCE", 1, expand=True, filter_overridden=True, notification_type_id=1 + ) assert isinstance(lst3[0], NotificationSetting) - diff --git a/tests/unit/test_org_auth_configs.py b/tests/unit/test_org_auth_configs.py index 825bbce..7b286f5 100644 --- a/tests/unit/test_org_auth_configs.py +++ b/tests/unit/test_org_auth_configs.py @@ -4,7 +4,6 @@ from nexla_sdk.models.org_auth_configs.requests import AuthConfigPayload from nexla_sdk.models.org_auth_configs.responses import AuthConfig - pytestmark = pytest.mark.unit @@ -39,7 +38,9 @@ def test_list_get_list_all_and_crud(self, client, mock_http_client): mock_http_client.add_response("/api_auth_configs", created) res = client.org_auth_configs.create(payload) assert isinstance(res, AuthConfig) and res.id == 2 - mock_http_client.assert_request_made("POST", "/api_auth_configs", json=payload.model_dump(exclude_none=True)) + mock_http_client.assert_request_made( + "POST", "/api_auth_configs", json=payload.model_dump(exclude_none=True) + ) mock_http_client.clear_responses() updated = {"id": 2, "name": "Okta-2", "protocol": "saml"} @@ -53,4 +54,3 @@ def test_list_get_list_all_and_crud(self, client, mock_http_client): del_res = client.org_auth_configs.delete(2) assert del_res.get("status") == "deleted" mock_http_client.assert_request_made("DELETE", "/api_auth_configs/2") - diff --git a/tests/unit/test_organizations.py b/tests/unit/test_organizations.py index 0e0c2c8..8681455 100644 --- a/tests/unit/test_organizations.py +++ b/tests/unit/test_organizations.py @@ -2,10 +2,10 @@ from nexla_sdk.models.organizations.requests import ( OrganizationCreate, - OrgMemberUpdate, - OrgMemberList, + OrgMemberActivateDeactivateRequest, OrgMemberDelete, - OrgMemberActivateDeactivateRequest + OrgMemberList, + OrgMemberUpdate, ) from tests.utils.assertions import NexlaAssertions from tests.utils.mock_builders import MockResponseBuilder @@ -19,9 +19,9 @@ def test_list_organizations(self, mock_client, assertions: NexlaAssertions): # Arrange mock_orgs = [ MockResponseBuilder.organization(org_id=1, name="Org 1"), - MockResponseBuilder.organization(org_id=2, name="Org 2") + MockResponseBuilder.organization(org_id=2, name="Org 2"), ] - mock_client.http_client.add_response('/orgs', mock_orgs) + mock_client.http_client.add_response("/orgs", mock_orgs) # Act orgs = mock_client.organizations.list() @@ -38,7 +38,7 @@ def test_get_organization(self, mock_client, assertions: NexlaAssertions): # Arrange org_id = 123 mock_response = MockResponseBuilder.organization(org_id=org_id, name="Test Org") - mock_client.http_client.add_response(f'/orgs/{org_id}', mock_response) + mock_client.http_client.add_response(f"/orgs/{org_id}", mock_response) # Act org = mock_client.organizations.get(org_id) @@ -47,8 +47,8 @@ def test_get_organization(self, mock_client, assertions: NexlaAssertions): assert org.id == org_id assert org.name == "Test Org" last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'GET' - assert f'/orgs/{org_id}' in last_request['url'] + assert last_request["method"] == "GET" + assert f"/orgs/{org_id}" in last_request["url"] def test_create_organization(self, mock_client, assertions: NexlaAssertions): """Test creating an organization.""" @@ -56,10 +56,12 @@ def test_create_organization(self, mock_client, assertions: NexlaAssertions): create_data = OrganizationCreate( name="New Test Org", owner={"full_name": "Test Owner", "email": "owner@test.com"}, - email_domain="test.com" + email_domain="test.com", ) - mock_response = MockResponseBuilder.organization(name="New Test Org", org_id=123) - mock_client.http_client.add_response('/orgs', mock_response) + mock_response = MockResponseBuilder.organization( + name="New Test Org", org_id=123 + ) + mock_client.http_client.add_response("/orgs", mock_response) # Act org = mock_client.organizations.create(create_data) @@ -68,17 +70,19 @@ def test_create_organization(self, mock_client, assertions: NexlaAssertions): assert org.name == "New Test Org" assert org.id == 123 last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'POST' - assert '/orgs' in last_request['url'] - assert last_request['json'] == create_data.model_dump(exclude_none=True) + assert last_request["method"] == "POST" + assert "/orgs" in last_request["url"] + assert last_request["json"] == create_data.model_dump(exclude_none=True) def test_update_organization(self, mock_client, assertions: NexlaAssertions): """Test updating an organization.""" # Arrange org_id = 123 update_data = {"name": "Updated Org Name"} - mock_response = MockResponseBuilder.organization(org_id=org_id, name="Updated Org Name") - mock_client.http_client.add_response(f'/orgs/{org_id}', mock_response) + mock_response = MockResponseBuilder.organization( + org_id=org_id, name="Updated Org Name" + ) + mock_client.http_client.add_response(f"/orgs/{org_id}", mock_response) # Act org = mock_client.organizations.update(org_id, update_data) @@ -87,9 +91,9 @@ def test_update_organization(self, mock_client, assertions: NexlaAssertions): assert org.id == org_id assert org.name == "Updated Org Name" last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'PUT' - assert f'/orgs/{org_id}' in last_request['url'] - assert last_request['json'] == update_data + assert last_request["method"] == "PUT" + assert f"/orgs/{org_id}" in last_request["url"] + assert last_request["json"] == update_data def test_get_members(self, mock_client, assertions: NexlaAssertions): """Test getting organization members.""" @@ -97,9 +101,9 @@ def test_get_members(self, mock_client, assertions: NexlaAssertions): org_id = 123 mock_members = [ MockResponseBuilder.org_member(member_id=1, email="member1@test.com"), - MockResponseBuilder.org_member(member_id=2, email="member2@test.com") + MockResponseBuilder.org_member(member_id=2, email="member2@test.com"), ] - mock_client.http_client.add_response(f'/orgs/{org_id}/members', mock_members) + mock_client.http_client.add_response(f"/orgs/{org_id}/members", mock_members) # Act members = mock_client.organizations.get_members(org_id) @@ -109,8 +113,8 @@ def test_get_members(self, mock_client, assertions: NexlaAssertions): for member, mock_member in zip(members, mock_members): assertions.assert_org_member_response(member, mock_member) last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'GET' - assert f'/orgs/{org_id}/members' in last_request['url'] + assert last_request["method"] == "GET" + assert f"/orgs/{org_id}/members" in last_request["url"] def test_update_members(self, mock_client, assertions: NexlaAssertions): """Test updating organization members.""" @@ -118,15 +122,19 @@ def test_update_members(self, mock_client, assertions: NexlaAssertions): org_id = 123 update_list = OrgMemberList( members=[ - OrgMemberUpdate(email="new.member@test.com", full_name="New Member", admin=False), - OrgMemberUpdate(id=1, admin=True) + OrgMemberUpdate( + email="new.member@test.com", full_name="New Member", admin=False + ), + OrgMemberUpdate(id=1, admin=True), ] ) mock_response = [ MockResponseBuilder.org_member(member_id=1, is_admin=True), - MockResponseBuilder.org_member(member_id=3, email="new.member@test.com", is_admin=False) + MockResponseBuilder.org_member( + member_id=3, email="new.member@test.com", is_admin=False + ), ] - mock_client.http_client.add_response(f'/orgs/{org_id}/members', mock_response) + mock_client.http_client.add_response(f"/orgs/{org_id}/members", mock_response) # Act members = mock_client.organizations.update_members(org_id, update_list) @@ -134,19 +142,17 @@ def test_update_members(self, mock_client, assertions: NexlaAssertions): # Assert assert len(members) == 2 last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'PUT' - assert f'/orgs/{org_id}/members' in last_request['url'] - assert last_request['json'] == update_list.model_dump(exclude_none=True) + assert last_request["method"] == "PUT" + assert f"/orgs/{org_id}/members" in last_request["url"] + assert last_request["json"] == update_list.model_dump(exclude_none=True) def test_delete_members(self, mock_client): """Test deleting organization members.""" # Arrange org_id = 123 - delete_list = OrgMemberDelete( - members=[{"email": "member1@test.com"}] - ) + delete_list = OrgMemberDelete(members=[{"email": "member1@test.com"}]) mock_client.http_client.add_response( - f'/orgs/{org_id}/members', {"status": "success"} + f"/orgs/{org_id}/members", {"status": "success"} ) # Act @@ -155,9 +161,9 @@ def test_delete_members(self, mock_client): # Assert assert response == {"status": "success"} last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'DELETE' - assert f'/orgs/{org_id}/members' in last_request['url'] - assert last_request['json'] == delete_list.model_dump(exclude_none=True) + assert last_request["method"] == "DELETE" + assert f"/orgs/{org_id}/members" in last_request["url"] + assert last_request["json"] == delete_list.model_dump(exclude_none=True) def test_deactivate_members(self, mock_client, assertions: NexlaAssertions): """Test deactivating organization members.""" @@ -167,9 +173,15 @@ def test_deactivate_members(self, mock_client, assertions: NexlaAssertions): members=[{"email": "member1@test.com"}] ) mock_response = [ - MockResponseBuilder.org_member(member_id=1, email="member1@test.com", org_membership_status="DEACTIVATED") + MockResponseBuilder.org_member( + member_id=1, + email="member1@test.com", + org_membership_status="DEACTIVATED", + ) ] - mock_client.http_client.add_response(f'/orgs/{org_id}/members/deactivate', mock_response) + mock_client.http_client.add_response( + f"/orgs/{org_id}/members/deactivate", mock_response + ) # Act members = mock_client.organizations.deactivate_members(org_id, deactivate_list) @@ -177,9 +189,9 @@ def test_deactivate_members(self, mock_client, assertions: NexlaAssertions): # Assert assert members[0].org_membership_status == "DEACTIVATED" last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'PUT' - assert f'/orgs/{org_id}/members/deactivate' in last_request['url'] - assert last_request['json'] == deactivate_list.model_dump(exclude_none=True) + assert last_request["method"] == "PUT" + assert f"/orgs/{org_id}/members/deactivate" in last_request["url"] + assert last_request["json"] == deactivate_list.model_dump(exclude_none=True) def test_activate_members(self, mock_client, assertions: NexlaAssertions): """Test activating organization members.""" @@ -189,9 +201,13 @@ def test_activate_members(self, mock_client, assertions: NexlaAssertions): members=[{"email": "member1@test.com"}] ) mock_response = [ - MockResponseBuilder.org_member(member_id=1, email="member1@test.com", org_membership_status="ACTIVE") + MockResponseBuilder.org_member( + member_id=1, email="member1@test.com", org_membership_status="ACTIVE" + ) ] - mock_client.http_client.add_response(f'/orgs/{org_id}/members/activate', mock_response) + mock_client.http_client.add_response( + f"/orgs/{org_id}/members/activate", mock_response + ) # Act members = mock_client.organizations.activate_members(org_id, activate_list) @@ -199,16 +215,18 @@ def test_activate_members(self, mock_client, assertions: NexlaAssertions): # Assert assert members[0].org_membership_status == "ACTIVE" last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'PUT' - assert f'/orgs/{org_id}/members/activate' in last_request['url'] - assert last_request['json'] == activate_list.model_dump(exclude_none=True) + assert last_request["method"] == "PUT" + assert f"/orgs/{org_id}/members/activate" in last_request["url"] + assert last_request["json"] == activate_list.model_dump(exclude_none=True) def test_get_account_summary(self, mock_client): """Test getting the account summary for an organization.""" # Arrange org_id = 123 mock_summary = MockResponseBuilder.account_summary(org_id=org_id) - mock_client.http_client.add_response(f'/orgs/{org_id}/account_summary', mock_summary) + mock_client.http_client.add_response( + f"/orgs/{org_id}/account_summary", mock_summary + ) # Act summary = mock_client.organizations.get_account_summary(org_id) @@ -217,14 +235,14 @@ def test_get_account_summary(self, mock_client): assert summary.org_id == org_id assert "data_sources" in summary.model_dump() last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'GET' - assert f'/orgs/{org_id}/account_summary' in last_request['url'] + assert last_request["method"] == "GET" + assert f"/orgs/{org_id}/account_summary" in last_request["url"] def test_get_current_account_summary(self, mock_client): """Test getting the account summary for the current organization.""" # Arrange mock_summary = MockResponseBuilder.account_summary(org_id=1) - mock_client.http_client.add_response('/orgs/account_summary', mock_summary) + mock_client.http_client.add_response("/orgs/account_summary", mock_summary) # Act summary = mock_client.organizations.get_current_account_summary() @@ -232,8 +250,8 @@ def test_get_current_account_summary(self, mock_client): # Assert assert "data_sources" in summary.model_dump() last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'GET' - assert '/orgs/account_summary' in last_request['url'] + assert last_request["method"] == "GET" + assert "/orgs/account_summary" in last_request["url"] def test_get_audit_log(self, mock_client): """Test getting the audit log for an organization.""" @@ -241,9 +259,9 @@ def test_get_audit_log(self, mock_client): org_id = 123 mock_log = [ MockResponseBuilder.audit_log_entry(), - MockResponseBuilder.audit_log_entry() + MockResponseBuilder.audit_log_entry(), ] - mock_client.http_client.add_response(f'/orgs/{org_id}/audit_log', mock_log) + mock_client.http_client.add_response(f"/orgs/{org_id}/audit_log", mock_log) # Act audit_log = mock_client.organizations.get_audit_log(org_id, per_page=10) @@ -251,6 +269,6 @@ def test_get_audit_log(self, mock_client): # Assert assert len(audit_log) == 2 last_request = mock_client.http_client.get_last_request() - assert last_request['method'] == 'GET' - assert f'/orgs/{org_id}/audit_log' in last_request['url'] - assert last_request['params'] == {'per_page': 10} \ No newline at end of file + assert last_request["method"] == "GET" + assert f"/orgs/{org_id}/audit_log" in last_request["url"] + assert last_request["params"] == {"per_page": 10} diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py index d53f18d..595b154 100644 --- a/tests/unit/test_projects.py +++ b/tests/unit/test_projects.py @@ -1,13 +1,19 @@ """Unit tests for projects resource.""" + import pytest from pydantic import ValidationError -from nexla_sdk.models.projects.responses import Project, ProjectDataFlow -from nexla_sdk.models.projects.requests import ProjectCreate, ProjectUpdate, ProjectFlowList, ProjectFlowIdentifier -from nexla_sdk.models.flows.responses import FlowResponse -from nexla_sdk.exceptions import ServerError, NotFoundError +from nexla_sdk.exceptions import NotFoundError, ServerError from nexla_sdk.http_client import HttpClientError -from tests.utils.mock_builders import MockResponseBuilder, MockDataFactory +from nexla_sdk.models.flows.responses import FlowResponse +from nexla_sdk.models.projects.requests import ( + ProjectCreate, + ProjectFlowIdentifier, + ProjectFlowList, + ProjectUpdate, +) +from nexla_sdk.models.projects.responses import Project, ProjectDataFlow +from tests.utils.mock_builders import MockDataFactory, MockResponseBuilder @pytest.mark.unit @@ -35,7 +41,9 @@ def test_list_projects_with_parameters(self, mock_client): mock_client.http_client.add_response("/projects", mock_data) # Act - projects = mock_client.projects.list(page=2, per_page=10, access_role="collaborator") + projects = mock_client.projects.list( + page=2, per_page=10, access_role="collaborator" + ) # Assert assert len(projects) == 1 @@ -52,8 +60,12 @@ def test_list_projects_with_expand(self, mock_client): # Arrange factory = MockDataFactory() project_data = factory.create_mock_project() - project_data['data_flows'] = [factory.create_mock_project_data_flow() for _ in range(2)] - project_data['flows'] = [factory.create_mock_project_data_flow() for _ in range(2)] + project_data["data_flows"] = [ + factory.create_mock_project_data_flow() for _ in range(2) + ] + project_data["flows"] = [ + factory.create_mock_project_data_flow() for _ in range(2) + ] mock_client.http_client.add_response("/projects", [project_data]) # Act @@ -113,8 +125,8 @@ def test_create_project(self, mock_client): description="Test project description", data_flows=[ ProjectFlowIdentifier(data_source_id=123), - ProjectFlowIdentifier(data_set_id=456) - ] + ProjectFlowIdentifier(data_set_id=456), + ], ) # Act @@ -132,12 +144,13 @@ def test_update_project(self, mock_client): """Test updating a project.""" # Arrange project_id = 123 - mock_data = MockResponseBuilder.project(project_id=project_id, name="Updated Project") + mock_data = MockResponseBuilder.project( + project_id=project_id, name="Updated Project" + ) mock_client.http_client.add_response(f"/projects/{project_id}", mock_data) update_data = ProjectUpdate( - name="Updated Project", - description="Updated description" + name="Updated Project", description="Updated description" ) # Act @@ -152,7 +165,9 @@ def test_delete_project(self, mock_client): """Test deleting a project.""" # Arrange project_id = 123 - mock_client.http_client.add_response(f"/projects/{project_id}", {"status": "deleted"}) + mock_client.http_client.add_response( + f"/projects/{project_id}", {"status": "deleted"} + ) # Act result = mock_client.projects.delete(project_id) @@ -173,7 +188,9 @@ def test_get_flows(self, mock_client): # Assert assert isinstance(flows, FlowResponse) - mock_client.http_client.assert_request_made("GET", f"/projects/{project_id}/flows") + mock_client.http_client.assert_request_made( + "GET", f"/projects/{project_id}/flows" + ) def test_search_flows(self, mock_client): """Test searching flows in a project.""" @@ -181,14 +198,18 @@ def test_search_flows(self, mock_client): project_id = 123 filters = [{"field": "name", "operator": "contains", "value": "test"}] mock_data = MockResponseBuilder.flow_response() - mock_client.http_client.add_response(f"/projects/{project_id}/flows/search", mock_data) + mock_client.http_client.add_response( + f"/projects/{project_id}/flows/search", mock_data + ) # Act flows = mock_client.projects.search_flows(project_id, filters) # Assert assert isinstance(flows, FlowResponse) - mock_client.http_client.assert_request_made("POST", f"/projects/{project_id}/flows/search") + mock_client.http_client.assert_request_made( + "POST", f"/projects/{project_id}/flows/search" + ) def test_add_data_flows(self, mock_client): """Test adding data flows to a project.""" @@ -201,7 +222,7 @@ def test_add_data_flows(self, mock_client): flows = ProjectFlowList( data_flows=[ ProjectFlowIdentifier(data_source_id=456), - ProjectFlowIdentifier(data_set_id=789) + ProjectFlowIdentifier(data_set_id=789), ] ) @@ -212,7 +233,9 @@ def test_add_data_flows(self, mock_client): assert isinstance(result, list) assert len(result) == 2 assert all(isinstance(flow, ProjectDataFlow) for flow in result) - mock_client.http_client.assert_request_made("PUT", f"/projects/{project_id}/flows") + mock_client.http_client.assert_request_made( + "PUT", f"/projects/{project_id}/flows" + ) def test_replace_data_flows(self, mock_client): """Test replacing data flows in a project.""" @@ -222,9 +245,7 @@ def test_replace_data_flows(self, mock_client): mock_data = [factory.create_mock_project_data_flow()] mock_client.http_client.add_response(f"/projects/{project_id}/flows", mock_data) - flows = ProjectFlowList( - data_flows=[ProjectFlowIdentifier(data_source_id=999)] - ) + flows = ProjectFlowList(data_flows=[ProjectFlowIdentifier(data_source_id=999)]) # Act result = mock_client.projects.replace_data_flows(project_id, flows) @@ -232,7 +253,9 @@ def test_replace_data_flows(self, mock_client): # Assert assert isinstance(result, list) assert len(result) == 1 - mock_client.http_client.assert_request_made("POST", f"/projects/{project_id}/flows") + mock_client.http_client.assert_request_made( + "POST", f"/projects/{project_id}/flows" + ) def test_remove_data_flows(self, mock_client): """Test removing data flows from a project.""" @@ -242,9 +265,7 @@ def test_remove_data_flows(self, mock_client): mock_data = [factory.create_mock_project_data_flow()] mock_client.http_client.add_response(f"/projects/{project_id}/flows", mock_data) - flows = ProjectFlowList( - data_flows=[ProjectFlowIdentifier(data_source_id=456)] - ) + flows = ProjectFlowList(data_flows=[ProjectFlowIdentifier(data_source_id=456)]) # Act result = mock_client.projects.remove_data_flows(project_id, flows) @@ -252,7 +273,9 @@ def test_remove_data_flows(self, mock_client): # Assert assert isinstance(result, list) assert len(result) == 1 - mock_client.http_client.assert_request_made("DELETE", f"/projects/{project_id}/flows") + mock_client.http_client.assert_request_made( + "DELETE", f"/projects/{project_id}/flows" + ) def test_remove_all_data_flows(self, mock_client): """Test removing all data flows from a project.""" @@ -266,7 +289,9 @@ def test_remove_all_data_flows(self, mock_client): # Assert assert isinstance(result, list) assert len(result) == 0 - mock_client.http_client.assert_request_made("DELETE", f"/projects/{project_id}/flows") + mock_client.http_client.assert_request_made( + "DELETE", f"/projects/{project_id}/flows" + ) def test_backward_compatibility_add_flows(self, mock_client): """Test backward compatibility add_flows method.""" @@ -276,9 +301,7 @@ def test_backward_compatibility_add_flows(self, mock_client): mock_data = [factory.create_mock_project_data_flow()] mock_client.http_client.add_response(f"/projects/{project_id}/flows", mock_data) - flows = ProjectFlowList( - data_flows=[ProjectFlowIdentifier(data_source_id=123)] - ) + flows = ProjectFlowList(data_flows=[ProjectFlowIdentifier(data_source_id=123)]) # Act result = mock_client.projects.add_flows(project_id, flows) @@ -286,7 +309,9 @@ def test_backward_compatibility_add_flows(self, mock_client): # Assert assert isinstance(result, list) assert len(result) == 1 - mock_client.http_client.assert_request_made("PUT", f"/projects/{project_id}/flows") + mock_client.http_client.assert_request_made( + "PUT", f"/projects/{project_id}/flows" + ) def test_backward_compatibility_replace_flows(self, mock_client): """Test backward compatibility replace_flows method.""" @@ -296,9 +321,7 @@ def test_backward_compatibility_replace_flows(self, mock_client): mock_data = [factory.create_mock_project_data_flow()] mock_client.http_client.add_response(f"/projects/{project_id}/flows", mock_data) - flows = ProjectFlowList( - data_flows=[ProjectFlowIdentifier(data_source_id=123)] - ) + flows = ProjectFlowList(data_flows=[ProjectFlowIdentifier(data_source_id=123)]) # Act result = mock_client.projects.replace_flows(project_id, flows) @@ -306,7 +329,9 @@ def test_backward_compatibility_replace_flows(self, mock_client): # Assert assert isinstance(result, list) assert len(result) == 1 - mock_client.http_client.assert_request_made("POST", f"/projects/{project_id}/flows") + mock_client.http_client.assert_request_made( + "POST", f"/projects/{project_id}/flows" + ) def test_backward_compatibility_remove_flows(self, mock_client): """Test backward compatibility remove_flows method.""" @@ -316,9 +341,7 @@ def test_backward_compatibility_remove_flows(self, mock_client): mock_data = [factory.create_mock_project_data_flow()] mock_client.http_client.add_response(f"/projects/{project_id}/flows", mock_data) - flows = ProjectFlowList( - data_flows=[ProjectFlowIdentifier(data_source_id=123)] - ) + flows = ProjectFlowList(data_flows=[ProjectFlowIdentifier(data_source_id=123)]) # Act result = mock_client.projects.remove_flows(project_id, flows) @@ -326,7 +349,9 @@ def test_backward_compatibility_remove_flows(self, mock_client): # Assert assert isinstance(result, list) assert len(result) == 1 - mock_client.http_client.assert_request_made("DELETE", f"/projects/{project_id}/flows") + mock_client.http_client.assert_request_made( + "DELETE", f"/projects/{project_id}/flows" + ) def test_http_error_handling(self, mock_client): """Test HTTP error handling.""" @@ -336,8 +361,8 @@ def test_http_error_handling(self, mock_client): HttpClientError( "Server Error", status_code=500, - response={"message": "Internal server error"} - ) + response={"message": "Internal server error"}, + ), ) # Act & Assert @@ -355,8 +380,8 @@ def test_not_found_error_handling(self, mock_client): HttpClientError( "Project not found", status_code=404, - response={"message": "Project not found"} - ) + response={"message": "Project not found"}, + ), ) # Act & Assert diff --git a/tests/unit/test_runtimes.py b/tests/unit/test_runtimes.py index 3e75b13..5570743 100644 --- a/tests/unit/test_runtimes.py +++ b/tests/unit/test_runtimes.py @@ -4,7 +4,6 @@ from nexla_sdk.models.runtimes.requests import RuntimeCreate, RuntimeUpdate from nexla_sdk.models.runtimes.responses import Runtime - pytestmark = pytest.mark.unit @@ -48,4 +47,3 @@ def test_crud_and_state(self, client, mock_http_client): mock_http_client.add_response("/runtimes/2", {"status": "deleted"}) d = client.runtimes.delete(2) assert d.get("status") == "deleted" - diff --git a/tests/unit/test_self_signup.py b/tests/unit/test_self_signup.py index 1b156c3..fb199c9 100644 --- a/tests/unit/test_self_signup.py +++ b/tests/unit/test_self_signup.py @@ -1,8 +1,7 @@ import pytest from nexla_sdk import NexlaClient -from nexla_sdk.models.self_signup.responses import SelfSignupRequest, BlockedDomain - +from nexla_sdk.models.self_signup.responses import BlockedDomain, SelfSignupRequest pytestmark = pytest.mark.unit @@ -24,32 +23,43 @@ def test_signup_and_verify(self, client, mock_http_client): assert res2.get("status") == "verified" def test_admin_endpoints(self, client, mock_http_client): - mock_http_client.add_response("/self_signup_requests", [{"id": 1, "email": "x@y.com"}]) + mock_http_client.add_response( + "/self_signup_requests", [{"id": 1, "email": "x@y.com"}] + ) reqs = client.self_signup.list_requests() assert isinstance(reqs[0], SelfSignupRequest) mock_http_client.clear_responses() - mock_http_client.add_response("/self_signup_requests/1/approve", {"id": 1, "status": "approved"}) + mock_http_client.add_response( + "/self_signup_requests/1/approve", {"id": 1, "status": "approved"} + ) approved = client.self_signup.approve_request("1") assert isinstance(approved, SelfSignupRequest) and approved.id == 1 mock_http_client.clear_responses() - mock_http_client.add_response("/self_signup_blocked_domains", [{"id": 1, "domain": "example.com"}]) + mock_http_client.add_response( + "/self_signup_blocked_domains", [{"id": 1, "domain": "example.com"}] + ) domains = client.self_signup.list_blocked_domains() assert isinstance(domains[0], BlockedDomain) mock_http_client.clear_responses() - mock_http_client.add_response("/self_signup_blocked_domains", {"id": 2, "domain": "bad.com"}) + mock_http_client.add_response( + "/self_signup_blocked_domains", {"id": 2, "domain": "bad.com"} + ) added = client.self_signup.add_blocked_domain("bad.com") assert isinstance(added, BlockedDomain) and added.id == 2 mock_http_client.clear_responses() - mock_http_client.add_response("/self_signup_blocked_domains/2", {"id": 2, "domain": "worse.com"}) + mock_http_client.add_response( + "/self_signup_blocked_domains/2", {"id": 2, "domain": "worse.com"} + ) updated = client.self_signup.update_blocked_domain("2", "worse.com") assert isinstance(updated, BlockedDomain) and updated.domain == "worse.com" mock_http_client.clear_responses() - mock_http_client.add_response("/self_signup_blocked_domains/2", {"status": "deleted"}) + mock_http_client.add_response( + "/self_signup_blocked_domains/2", {"status": "deleted"} + ) deleted = client.self_signup.delete_blocked_domain("2") assert deleted.get("status") == "deleted" - diff --git a/tests/unit/test_sources.py b/tests/unit/test_sources.py index d3ce534..368583c 100644 --- a/tests/unit/test_sources.py +++ b/tests/unit/test_sources.py @@ -3,91 +3,87 @@ import pytest from pydantic import ValidationError -from nexla_sdk.exceptions import ( - ServerError, - NotFoundError, - ValidationError as SDKValidationError, +from nexla_sdk.exceptions import NotFoundError, ServerError +from nexla_sdk.exceptions import ValidationError as SDKValidationError +from nexla_sdk.models.sources.requests import ( + SourceCopyOptions, + SourceCreate, + SourceUpdate, ) -from nexla_sdk.models.sources.responses import Source, DataSetBrief, RunInfo -from nexla_sdk.models.sources.requests import SourceCreate, SourceUpdate, SourceCopyOptions +from nexla_sdk.models.sources.responses import DataSetBrief, RunInfo, Source from tests.utils import ( - MockResponseBuilder, create_http_error, assert_model_valid, - assert_model_list_valid + MockResponseBuilder, + assert_model_list_valid, + assert_model_valid, + create_http_error, ) @pytest.mark.unit class TestSourcesModels: """Test sources model validation and serialization.""" - + def test_source_model_with_all_fields(self): """Test Source model with all fields populated.""" source_data = MockResponseBuilder.source() source = Source(**source_data) - assert_model_valid(source, {"id": source_data["id"], "name": source_data["name"]}) - + assert_model_valid( + source, {"id": source_data["id"], "name": source_data["name"]} + ) + def test_source_model_with_minimal_fields(self): """Test Source model with only required fields.""" minimal_data = { "id": 123, "name": "Test Source", "status": "ACTIVE", - "source_type": "s3" + "source_type": "s3", } source = Source(**minimal_data) assert source.id == 123 assert source.name == "Test Source" assert source.data_sets == [] assert source.tags == [] - + def test_source_model_with_credentials(self): """Test Source model with embedded credentials.""" source_data = MockResponseBuilder.source( - source_id=456, - include_credentials=True + source_id=456, include_credentials=True ) source = Source(**source_data) assert source.data_credentials is not None assert source.data_credentials.id == source_data["data_credentials"]["id"] - + def test_source_model_with_datasets(self): """Test Source model with embedded datasets.""" - source_data = MockResponseBuilder.source( - source_id=789, - include_datasets=True - ) + source_data = MockResponseBuilder.source(source_id=789, include_datasets=True) source = Source(**source_data) assert len(source.data_sets) > 0 assert isinstance(source.data_sets[0], DataSetBrief) - + def test_source_create_model(self): """Test SourceCreate request model.""" create_data = { "name": "New Source", "source_type": "postgres", - "data_credentials_id": 123 + "data_credentials_id": 123, } source_create = SourceCreate(**create_data) assert source_create.name == "New Source" assert source_create.source_type == "postgres" assert source_create.data_credentials_id == 123 - + def test_source_update_model(self): """Test SourceUpdate request model.""" - update_data = { - "name": "Updated Source", - "description": "Updated description" - } + update_data = {"name": "Updated Source", "description": "Updated description"} source_update = SourceUpdate(**update_data) assert source_update.name == "Updated Source" assert source_update.description == "Updated description" - + def test_source_copy_options_model(self): """Test SourceCopyOptions model.""" options = SourceCopyOptions( - reuse_data_credentials=True, - copy_access_controls=False, - owner_id=456 + reuse_data_credentials=True, copy_access_controls=False, owner_id=456 ) assert options.reuse_data_credentials is True assert options.copy_access_controls is False @@ -97,272 +93,258 @@ def test_source_copy_options_model(self): @pytest.mark.unit class TestSourcesResourceUnit: """Unit tests for SourcesResource using mocks.""" - + def test_list_sources_success(self, mock_client, mock_http_client): """Test successful sources listing.""" # Arrange mock_sources = [ MockResponseBuilder.source(source_id=1), - MockResponseBuilder.source(source_id=2) + MockResponseBuilder.source(source_id=2), ] mock_http_client.add_response("/data_sources", mock_sources) - + # Act sources = mock_client.sources.list() - + # Assert assert len(sources) == 2 assert_model_list_valid(sources, Source) mock_http_client.assert_request_made("GET", "/data_sources") - + def test_list_sources_with_filters(self, mock_client, mock_http_client): """Test sources listing with filter parameters.""" # Arrange mock_sources = [MockResponseBuilder.source(source_id=1)] mock_http_client.add_response("/data_sources", mock_sources) - + # Act - sources = mock_client.sources.list( - page=1, - per_page=10, - access_role="owner" - ) - + sources = mock_client.sources.list(page=1, per_page=10, access_role="owner") + # Assert assert len(sources) == 1 mock_http_client.assert_request_made( - "GET", "/data_sources", - params={"page": 1, "per_page": 10, "access_role": "owner"} + "GET", + "/data_sources", + params={"page": 1, "per_page": 10, "access_role": "owner"}, ) - + def test_get_source_success(self, mock_client, mock_http_client): """Test successful source retrieval.""" # Arrange source_id = 123 mock_source = MockResponseBuilder.source(source_id=source_id) mock_http_client.add_response(f"/data_sources/{source_id}", mock_source) - + # Act source = mock_client.sources.get(source_id) - + # Assert assert_model_valid(source, {"id": source_id}) mock_http_client.assert_request_made("GET", f"/data_sources/{source_id}") - + def test_get_source_with_expand(self, mock_client, mock_http_client): """Test source retrieval with expand parameter.""" # Arrange source_id = 456 mock_source = MockResponseBuilder.source( - source_id=source_id, - include_datasets=True, - include_credentials=True + source_id=source_id, include_datasets=True, include_credentials=True ) mock_http_client.add_response(f"/data_sources/{source_id}", mock_source) - + # Act source = mock_client.sources.get(source_id, expand=True) - + # Assert assert_model_valid(source, {"id": source_id}) assert source.data_credentials is not None assert len(source.data_sets) > 0 mock_http_client.assert_request_made( - "GET", f"/data_sources/{source_id}", - params={"expand": 1} + "GET", f"/data_sources/{source_id}", params={"expand": 1} ) - + def test_create_source_success(self, mock_client, mock_http_client): """Test successful source creation.""" # Arrange create_data = SourceCreate( - name="New Test Source", - source_type="s3", - data_credentials_id=789 + name="New Test Source", source_type="s3", data_credentials_id=789 ) mock_response = MockResponseBuilder.source( - source_id=999, - name="New Test Source" + source_id=999, name="New Test Source" ) mock_http_client.add_response("/data_sources", mock_response) - + # Act source = mock_client.sources.create(create_data) - + # Assert assert_model_valid(source, {"id": 999, "name": "New Test Source"}) mock_http_client.assert_request_made( - "POST", "/data_sources", + "POST", + "/data_sources", json={ "name": "New Test Source", - "source_type": "s3", - "data_credentials_id": 789 - } + "source_type": "s3", + "data_credentials_id": 789, + }, ) - + def test_update_source_success(self, mock_client, mock_http_client): """Test successful source update.""" # Arrange source_id = 555 update_data = SourceUpdate( - name="Updated Source Name", - description="Updated description" + name="Updated Source Name", description="Updated description" ) mock_response = MockResponseBuilder.source( - source_id=source_id, - name="Updated Source Name" + source_id=source_id, name="Updated Source Name" ) mock_http_client.add_response(f"/data_sources/{source_id}", mock_response) - + # Act source = mock_client.sources.update(source_id, update_data) - + # Assert assert_model_valid(source, {"id": source_id, "name": "Updated Source Name"}) mock_http_client.assert_request_made( - "PUT", f"/data_sources/{source_id}", - json={"name": "Updated Source Name", "description": "Updated description"} + "PUT", + f"/data_sources/{source_id}", + json={"name": "Updated Source Name", "description": "Updated description"}, ) - + def test_delete_source_success(self, mock_client, mock_http_client): """Test successful source deletion.""" # Arrange source_id = 777 - mock_http_client.add_response(f"/data_sources/{source_id}", {"status": "deleted"}) - + mock_http_client.add_response( + f"/data_sources/{source_id}", {"status": "deleted"} + ) + # Act response = mock_client.sources.delete(source_id) - + # Assert assert response["status"] == "deleted" mock_http_client.assert_request_made("DELETE", f"/data_sources/{source_id}") - + def test_activate_source_success(self, mock_client, mock_http_client): """Test successful source activation.""" # Arrange source_id = 888 - mock_response = MockResponseBuilder.source( - source_id=source_id, - status="ACTIVE" + mock_response = MockResponseBuilder.source(source_id=source_id, status="ACTIVE") + mock_http_client.add_response( + f"/data_sources/{source_id}/activate", mock_response ) - mock_http_client.add_response(f"/data_sources/{source_id}/activate", mock_response) - + # Act source = mock_client.sources.activate(source_id) - + # Assert assert_model_valid(source, {"id": source_id, "status": "ACTIVE"}) - mock_http_client.assert_request_made("PUT", f"/data_sources/{source_id}/activate") - + mock_http_client.assert_request_made( + "PUT", f"/data_sources/{source_id}/activate" + ) + def test_pause_source_success(self, mock_client, mock_http_client): """Test successful source pause.""" # Arrange source_id = 999 - mock_response = MockResponseBuilder.source( - source_id=source_id, - status="PAUSED" - ) + mock_response = MockResponseBuilder.source(source_id=source_id, status="PAUSED") mock_http_client.add_response(f"/data_sources/{source_id}/pause", mock_response) - + # Act source = mock_client.sources.pause(source_id) - + # Assert assert_model_valid(source, {"id": source_id, "status": "PAUSED"}) mock_http_client.assert_request_made("PUT", f"/data_sources/{source_id}/pause") - + def test_copy_source_success(self, mock_client, mock_http_client): """Test successful source copy.""" # Arrange source_id = 111 copy_options = SourceCopyOptions( - reuse_data_credentials=True, - copy_access_controls=False - ) - mock_response = MockResponseBuilder.source( - source_id=222, - name="Copied Source" + reuse_data_credentials=True, copy_access_controls=False ) + mock_response = MockResponseBuilder.source(source_id=222, name="Copied Source") mock_http_client.add_response(f"/data_sources/{source_id}/copy", mock_response) - + # Act copied_source = mock_client.sources.copy(source_id, copy_options) - + # Assert assert_model_valid(copied_source, {"id": 222, "name": "Copied Source"}) mock_http_client.assert_request_made( - "POST", f"/data_sources/{source_id}/copy", - json={ - "reuse_data_credentials": True, - "copy_access_controls": False - } + "POST", + f"/data_sources/{source_id}/copy", + json={"reuse_data_credentials": True, "copy_access_controls": False}, ) @pytest.mark.unit class TestSourcesErrorHandling: """Test error handling for sources operations.""" - + def test_get_source_not_found(self, mock_client, mock_http_client): """Test getting a non-existent source.""" # Arrange source_id = 999 error = create_http_error( - 404, + 404, "Source not found", - {"resource_type": "source", "resource_id": str(source_id)} + {"resource_type": "source", "resource_id": str(source_id)}, ) mock_http_client.add_response(f"/data_sources/{source_id}", error) - + # Act & Assert with pytest.raises(NotFoundError) as exc_info: mock_client.sources.get(source_id) - + assert "Source not found" in str(exc_info.value) assert exc_info.value.resource_type == "source" assert exc_info.value.resource_id == str(source_id) - + def test_create_source_validation_error(self, mock_client, mock_http_client): """Test source creation with invalid data.""" # Arrange error = create_http_error( 400, "Validation failed", - {"field": "source_type", "message": "Invalid source type"} + {"field": "source_type", "message": "Invalid source type"}, ) mock_http_client.add_response("/data_sources", error) - + # Act & Assert with pytest.raises(SDKValidationError) as exc_info: mock_client.sources.create({"invalid": "data"}) - + assert exc_info.value.status_code == 400 assert "Validation failed" in str(exc_info.value) - + def test_update_source_unauthorized(self, mock_client, mock_http_client): """Test updating source without permission.""" # Arrange source_id = 123 error = create_http_error(403, "Insufficient permissions") mock_http_client.add_response(f"/data_sources/{source_id}", error) - + # Act & Assert from nexla_sdk.exceptions import AuthorizationError + with pytest.raises(AuthorizationError) as exc_info: mock_client.sources.update(source_id, {"name": "New Name"}) - + assert exc_info.value.status_code == 403 - + def test_server_error_during_list(self, mock_client, mock_http_client): """Test handling server error during list operation.""" # Arrange error = create_http_error(500, "Internal server error") mock_http_client.add_response("/data_sources", error) - + # Act & Assert with pytest.raises(ServerError) as exc_info: mock_client.sources.list() - + assert exc_info.value.status_code == 500 assert "Internal server error" in str(exc_info.value) @@ -370,7 +352,7 @@ def test_server_error_during_list(self, mock_client, mock_http_client): @pytest.mark.unit class TestSourcesValidation: """Test sources model validation edge cases.""" - + def test_source_model_handles_none_values(self): """Test that Source model handles None values gracefully.""" source_data = { @@ -381,31 +363,28 @@ def test_source_model_handles_none_values(self): "description": None, "data_credentials": None, "data_sets": None, - "tags": None + "tags": None, } source = Source(**source_data) assert source.description is None assert source.data_credentials is None assert source.data_sets == [] # Should default to empty list assert source.tags == [] # Should default to empty list - + def test_source_create_requires_name(self): """Test that SourceCreate requires name field.""" with pytest.raises(ValidationError): SourceCreate(source_type="s3") # Missing required name - + def test_source_create_validates_enum_fields(self): """Test that enum fields are validated in SourceCreate.""" # This should work with valid source_type - valid_create = SourceCreate( - name="Test", - source_type="s3" - ) + valid_create = SourceCreate(name="Test", source_type="s3") assert valid_create.source_type == "s3" - + # Invalid source types should be handled by the enum validation # The actual validation depends on the SourceType enum implementation - + def test_data_set_brief_model(self): """Test DataSetBrief model validation.""" dataset_data = { @@ -413,19 +392,17 @@ def test_data_set_brief_model(self): "owner_id": 123, "org_id": 789, "name": "Test Dataset", - "description": "Test description" + "description": "Test description", } dataset = DataSetBrief(**dataset_data) assert dataset.id == 456 assert dataset.name == "Test Dataset" - + def test_run_info_model(self): """Test RunInfo model validation.""" from datetime import datetime - run_data = { - "id": 789, - "created_at": "2023-01-01T12:00:00Z" - } + + run_data = {"id": 789, "created_at": "2023-01-01T12:00:00Z"} run_info = RunInfo(**run_data) assert run_info.id == 789 - assert isinstance(run_info.created_at, datetime) + assert isinstance(run_info.created_at, datetime) diff --git a/tests/unit/test_teams.py b/tests/unit/test_teams.py index e45a84e..d83be1e 100644 --- a/tests/unit/test_teams.py +++ b/tests/unit/test_teams.py @@ -1,12 +1,18 @@ """Unit tests for TeamsResource.""" import pytest -from nexla_sdk.exceptions import ServerError, NotFoundError, ValidationError -from nexla_sdk.models.teams.responses import Team, TeamMember -from nexla_sdk.models.teams.requests import TeamCreate, TeamUpdate, TeamMemberRequest, TeamMemberList + +from nexla_sdk.exceptions import NotFoundError, ServerError, ValidationError from nexla_sdk.http_client import HttpClientError -from tests.utils.mock_builders import MockResponseBuilder +from nexla_sdk.models.teams.requests import ( + TeamCreate, + TeamMemberList, + TeamMemberRequest, + TeamUpdate, +) +from nexla_sdk.models.teams.responses import Team, TeamMember from tests.utils.assertions import NexlaAssertions +from tests.utils.mock_builders import MockResponseBuilder class TestTeamsUnitTests: @@ -18,9 +24,9 @@ def test_list_teams_success(self, mock_client): team_data = MockResponseBuilder.team() team_data["id"] = 123 client.http_client.add_response("/teams", [team_data]) - + teams = client.teams.list() - + assert len(teams) == 1 assert isinstance(teams[0], Team) NexlaAssertions.assert_team_response(teams[0], team_data) @@ -35,11 +41,13 @@ def test_list_teams_with_access_role_member(self, mock_client): team_data2.update({"id": 124, "member": True}) team_data = [team_data1, team_data2] client.http_client.add_response("/teams", team_data) - + teams = client.teams.list(access_role="member") - + assert len(teams) == 2 - client.http_client.assert_request_made("GET", "/teams", params={"access_role": "member"}) + client.http_client.assert_request_made( + "GET", "/teams", params={"access_role": "member"} + ) def test_list_teams_with_pagination(self, mock_client): """Test listing teams with pagination parameters.""" @@ -47,10 +55,12 @@ def test_list_teams_with_pagination(self, mock_client): team_data = MockResponseBuilder.team() team_data["id"] = 123 client.http_client.add_response("/teams", [team_data]) - + client.teams.list(page=2, per_page=50) - - client.http_client.assert_request_made("GET", "/teams", params={"page": 2, "per_page": 50}) + + client.http_client.assert_request_made( + "GET", "/teams", params={"page": 2, "per_page": 50} + ) def test_get_team_success(self, mock_client): """Test successful getting of a team.""" @@ -58,9 +68,9 @@ def test_get_team_success(self, mock_client): team_data = MockResponseBuilder.team() team_data["id"] = 123 client.http_client.add_response("/teams/123", team_data) - + team = client.teams.get(123) - + assert isinstance(team, Team) NexlaAssertions.assert_team_response(team, team_data) client.http_client.assert_request_made("GET", "/teams/123") @@ -71,18 +81,24 @@ def test_get_team_with_expand(self, mock_client): team_data = MockResponseBuilder.team() team_data["id"] = 123 client.http_client.add_response("/teams/123", team_data) - + team = client.teams.get(123, expand=True) - + assert isinstance(team, Team) - client.http_client.assert_request_made("GET", "/teams/123", params={"expand": 1}) + client.http_client.assert_request_made( + "GET", "/teams/123", params={"expand": 1} + ) def test_get_team_not_found(self, mock_client): """Test getting a non-existent team.""" client = mock_client - client.http_client.add_error("/teams/999", - HttpClientError("Not found", status_code=404, response={"message": "Team not found"})) - + client.http_client.add_error( + "/teams/999", + HttpClientError( + "Not found", status_code=404, response={"message": "Team not found"} + ), + ) + with pytest.raises(NotFoundError): client.teams.get(999) @@ -92,27 +108,21 @@ def test_create_team_success(self, mock_client): request_data = TeamCreate( name="Test Team", description="A test team", - members=[ - TeamMemberRequest(email="test@example.com", admin=True) - ] + members=[TeamMemberRequest(email="test@example.com", admin=True)], ) response_data = MockResponseBuilder.team() - response_data.update({ - "id": 123, - "name": "Test Team", - "description": "A test team", - "members": [ - { - "id": 456, - "email": "test@example.com", - "admin": True - } - ] - }) + response_data.update( + { + "id": 123, + "name": "Test Team", + "description": "A test team", + "members": [{"id": 456, "email": "test@example.com", "admin": True}], + } + ) client.http_client.add_response("/teams", response_data) - + team = client.teams.create(request_data) - + assert isinstance(team, Team) assert team.name == "Test Team" assert team.description == "A test team" @@ -124,11 +134,16 @@ def test_create_team_validation_error(self, mock_client): client = mock_client request_data = TeamCreate( name="", # Invalid empty name - description="A test team" + description="A test team", + ) + client.http_client.add_error( + "/teams", + HttpClientError( + "Validation failed", + status_code=400, + response={"message": "Team name cannot be empty"}, + ), ) - client.http_client.add_error("/teams", - HttpClientError("Validation failed", status_code=400, - response={"message": "Team name cannot be empty"})) with pytest.raises(ValidationError): client.teams.create(request_data) @@ -138,14 +153,14 @@ def test_update_team_success(self, mock_client): client = mock_client request_data = TeamUpdate( name="Updated Team", - members=[TeamMemberRequest(email="new@example.com", admin=False)] + members=[TeamMemberRequest(email="new@example.com", admin=False)], ) response_data = MockResponseBuilder.team() response_data.update({"id": 123, "name": "Updated Team"}) client.http_client.add_response("/teams/123", response_data) - + team = client.teams.update(123, request_data) - + assert isinstance(team, Team) assert team.name == "Updated Team" client.http_client.assert_request_made("PUT", "/teams/123") @@ -154,9 +169,9 @@ def test_delete_team_success(self, mock_client): """Test successful deletion of a team.""" client = mock_client client.http_client.add_response("/teams/123", {"status": "deleted"}) - + result = client.teams.delete(123) - + assert result["status"] == "deleted" client.http_client.assert_request_made("DELETE", "/teams/123") @@ -169,9 +184,9 @@ def test_get_members_success(self, mock_client): member2.update({"id": 457, "email": "user2@example.com", "admin": False}) members_data = [member1, member2] client.http_client.add_response("/teams/123/members", members_data) - + members = client.teams.get_members(123) - + assert len(members) == 2 assert all(isinstance(member, TeamMember) for member in members) assert members[0].email == "user1@example.com" @@ -186,18 +201,24 @@ def test_add_members_success(self, mock_client): request_data = TeamMemberList( members=[ TeamMemberRequest(email="new1@example.com", admin=True), - TeamMemberRequest(id=789, admin=False) + TeamMemberRequest(id=789, admin=False), ] ) response_data = [ - MockResponseBuilder.team_member(user_id=456, email="existing@example.com", admin=True), - MockResponseBuilder.team_member(user_id=789, email="new1@example.com", admin=True), - MockResponseBuilder.team_member(user_id=790, email="new2@example.com", admin=False) + MockResponseBuilder.team_member( + user_id=456, email="existing@example.com", admin=True + ), + MockResponseBuilder.team_member( + user_id=789, email="new1@example.com", admin=True + ), + MockResponseBuilder.team_member( + user_id=790, email="new2@example.com", admin=False + ), ] client.http_client.add_response("/teams/123/members", response_data) - + members = client.teams.add_members(123, request_data) - + assert len(members) == 3 assert all(isinstance(member, TeamMember) for member in members) client.http_client.assert_request_made("PUT", "/teams/123/members") @@ -209,12 +230,14 @@ def test_replace_members_success(self, mock_client): members=[TeamMemberRequest(email="only@example.com", admin=True)] ) response_data = [ - MockResponseBuilder.team_member(user_id=999, email="only@example.com", admin=True) + MockResponseBuilder.team_member( + user_id=999, email="only@example.com", admin=True + ) ] client.http_client.add_response("/teams/123/members", response_data) - + members = client.teams.replace_members(123, request_data) - + assert len(members) == 1 assert members[0].email == "only@example.com" client.http_client.assert_request_made("POST", "/teams/123/members") @@ -226,12 +249,14 @@ def test_remove_members_success(self, mock_client): members=[TeamMemberRequest(email="remove@example.com")] ) response_data = [ - MockResponseBuilder.team_member(user_id=456, email="remaining@example.com", admin=False) + MockResponseBuilder.team_member( + user_id=456, email="remaining@example.com", admin=False + ) ] client.http_client.add_response("/teams/123/members", response_data) - + members = client.teams.remove_members(123, request_data) - + assert len(members) == 1 assert members[0].email == "remaining@example.com" client.http_client.assert_request_made("DELETE", "/teams/123/members") @@ -240,9 +265,9 @@ def test_remove_all_members_success(self, mock_client): """Test successful removal of all team members.""" client = mock_client client.http_client.add_response("/teams/123/members", []) - + members = client.teams.remove_members(123) # No members specified = remove all - + assert len(members) == 0 client.http_client.assert_request_made("DELETE", "/teams/123/members") @@ -252,15 +277,21 @@ def test_team_with_complex_members(self, mock_client): team_data = MockResponseBuilder.team( team_id=123, members=[ - MockResponseBuilder.team_member(user_id=1, email="admin@example.com", admin=True), - MockResponseBuilder.team_member(user_id=2, email="user@example.com", admin=False), - MockResponseBuilder.team_member(user_id=3, email="another@example.com", admin=True) - ] + MockResponseBuilder.team_member( + user_id=1, email="admin@example.com", admin=True + ), + MockResponseBuilder.team_member( + user_id=2, email="user@example.com", admin=False + ), + MockResponseBuilder.team_member( + user_id=3, email="another@example.com", admin=True + ), + ], ) client.http_client.add_response("/teams/123", team_data) - + team = client.teams.get(123) - + assert len(team.members) == 3 admin_members = [m for m in team.members if m.admin] regular_members = [m for m in team.members if not m.admin] @@ -270,30 +301,34 @@ def test_team_with_complex_members(self, mock_client): def test_http_error_handling(self, mock_client): """Test proper HTTP error handling.""" client = mock_client - client.http_client.add_error("/teams", - HttpClientError("Server Error", status_code=500, response={"message": "Internal error"})) - + client.http_client.add_error( + "/teams", + HttpClientError( + "Server Error", status_code=500, response={"message": "Internal error"} + ), + ) + with pytest.raises(ServerError) as exc_info: client.teams.list() - + assert exc_info.value.status_code == 500 def test_empty_list_response(self, mock_client): """Test handling of empty list response.""" client = mock_client client.http_client.add_response("/teams", []) - + teams = client.teams.list() - + assert teams == [] def test_empty_members_list(self, mock_client): """Test handling of empty members list.""" client = mock_client client.http_client.add_response("/teams/123/members", []) - + members = client.teams.get_members(123) - + assert members == [] def test_team_member_request_validation(self, mock_client): @@ -303,13 +338,13 @@ def test_team_member_request_validation(self, mock_client): assert request1.email == "test@example.com" assert request1.admin assert request1.id is None - + # Valid with ID request2 = TeamMemberRequest(id=123, admin=False) assert request2.id == 123 assert not request2.admin assert request2.email is None - + # Valid with both (API allows this) request3 = TeamMemberRequest(id=123, email="test@example.com", admin=True) assert request3.id == 123 @@ -323,9 +358,9 @@ def test_create_team_minimal_data(self, mock_client): response_data = MockResponseBuilder.team() response_data.update({"id": 123, "name": "Minimal Team"}) client.http_client.add_response("/teams", response_data) - + team = client.teams.create(request_data) - + assert isinstance(team, Team) assert team.name == "Minimal Team" @@ -333,24 +368,31 @@ def test_update_team_partial_data(self, mock_client): """Test updating team with partial data.""" client = mock_client request_data = TeamUpdate(description="New description only") - response_data = MockResponseBuilder.team(team_id=123, description="New description only") + response_data = MockResponseBuilder.team( + team_id=123, description="New description only" + ) client.http_client.add_response("/teams/123", response_data) - + team = client.teams.update(123, request_data) - + assert isinstance(team, Team) assert team.description == "New description only" def test_member_management_error_handling(self, mock_client): """Test error handling in member management operations.""" client = mock_client - client.http_client.add_error("/teams/123/members", - HttpClientError("Member not found", status_code=404, - response={"message": "User not found"})) - + client.http_client.add_error( + "/teams/123/members", + HttpClientError( + "Member not found", + status_code=404, + response={"message": "User not found"}, + ), + ) + request_data = TeamMemberList( members=[TeamMemberRequest(email="nonexistent@example.com")] ) - + with pytest.raises(NotFoundError): - client.teams.add_members(123, request_data) \ No newline at end of file + client.teams.add_members(123, request_data) diff --git a/tests/unit/test_transforms.py b/tests/unit/test_transforms.py index 221acd7..eeb25d3 100644 --- a/tests/unit/test_transforms.py +++ b/tests/unit/test_transforms.py @@ -4,7 +4,6 @@ from nexla_sdk.models.transforms.requests import TransformCreate, TransformUpdate from nexla_sdk.models.transforms.responses import Transform, TransformCodeOp - pytestmark = pytest.mark.unit @@ -31,7 +30,10 @@ def test_list_public_get_crud_copy(self, client, mock_http_client): mock_http_client.clear_responses() create = TransformCreate( - name="t", output_type="json", code_type="python", code_encoding="utf-8", + name="t", + output_type="json", + code_type="python", + code_encoding="utf-8", code=[TransformCodeOp(operation="map", spec={})], ) mock_http_client.add_response("/transforms", {"id": 12, "name": "t"}) @@ -44,7 +46,9 @@ def test_list_public_get_crud_copy(self, client, mock_http_client): assert upd.name == "t2" mock_http_client.clear_responses() - mock_http_client.add_response("/transforms/12/copy", {"id": 13, "name": "t-copy"}) + mock_http_client.add_response( + "/transforms/12/copy", {"id": 13, "name": "t-copy"} + ) cp = client.transforms.copy(12) assert isinstance(cp, Transform) @@ -52,4 +56,3 @@ def test_list_public_get_crud_copy(self, client, mock_http_client): mock_http_client.add_response("/transforms/13", {"status": "deleted"}) res = client.transforms.delete(13) assert res.get("status") == "deleted" - diff --git a/tests/unit/test_users.py b/tests/unit/test_users.py index 7ba78e2..1c369e9 100644 --- a/tests/unit/test_users.py +++ b/tests/unit/test_users.py @@ -1,12 +1,13 @@ """Unit tests for UsersResource.""" import pytest -from nexla_sdk.exceptions import ServerError, NotFoundError, ValidationError -from nexla_sdk.models.users.responses import User, UserExpanded, UserSettings -from nexla_sdk.models.users.requests import UserCreate, UserUpdate + +from nexla_sdk.exceptions import NotFoundError, ServerError, ValidationError from nexla_sdk.http_client import HttpClientError -from tests.utils.mock_builders import MockResponseBuilder +from nexla_sdk.models.users.requests import UserCreate, UserUpdate +from nexla_sdk.models.users.responses import User, UserExpanded, UserSettings from tests.utils.assertions import NexlaAssertions +from tests.utils.mock_builders import MockResponseBuilder class TestUsersUnitTests: @@ -18,9 +19,9 @@ def test_list_users_success(self, mock_client): user_data = MockResponseBuilder.user() user_data["id"] = 123 client.http_client.add_response("/users", [user_data]) - + users = client.users.list() - + assert len(users) == 1 assert isinstance(users[0], User) NexlaAssertions.assert_user_response(users[0], user_data) @@ -35,11 +36,13 @@ def test_list_users_with_access_role_all(self, mock_client): user_data2["id"] = 124 user_data = [user_data1, user_data2] client.http_client.add_response("/users", user_data) - + users = client.users.list(access_role="all") - + assert len(users) == 2 - client.http_client.assert_request_made("GET", "/users", params={"access_role": "all"}) + client.http_client.assert_request_made( + "GET", "/users", params={"access_role": "all"} + ) def test_list_users_with_expand(self, mock_client): """Test listing users with expand parameter.""" @@ -47,9 +50,9 @@ def test_list_users_with_expand(self, mock_client): user_data = MockResponseBuilder.user() user_data["id"] = 123 client.http_client.add_response("/users?expand=1", [user_data]) - + users = client.users.list(expand=True) - + assert len(users) == 1 assert isinstance(users[0], UserExpanded) @@ -59,10 +62,12 @@ def test_list_users_with_pagination(self, mock_client): user_data = MockResponseBuilder.user() user_data["id"] = 123 client.http_client.add_response("/users", [user_data]) - + client.users.list(page=2, per_page=50) - - client.http_client.assert_request_made("GET", "/users", params={"page": 2, "per_page": 50}) + + client.http_client.assert_request_made( + "GET", "/users", params={"page": 2, "per_page": 50} + ) def test_get_user_success(self, mock_client): """Test successful getting of a user.""" @@ -70,9 +75,9 @@ def test_get_user_success(self, mock_client): user_data = MockResponseBuilder.user() user_data["id"] = 123 client.http_client.add_response("/users/123", user_data) - + user = client.users.get(123) - + assert isinstance(user, User) NexlaAssertions.assert_user_response(user, user_data) client.http_client.assert_request_made("GET", "/users/123") @@ -83,37 +88,36 @@ def test_get_user_with_expand(self, mock_client): user_data = MockResponseBuilder.user() user_data["id"] = 123 client.http_client.add_response("/users/123?expand=1", user_data) - + user = client.users.get(123, expand=True) - + assert isinstance(user, UserExpanded) def test_get_user_not_found(self, mock_client): """Test getting a non-existent user.""" client = mock_client - client.http_client.add_error("/users/999", - HttpClientError("Not found", status_code=404, response={"message": "User not found"})) - + client.http_client.add_error( + "/users/999", + HttpClientError( + "Not found", status_code=404, response={"message": "User not found"} + ), + ) + with pytest.raises(NotFoundError): client.users.get(999) def test_create_user_success(self, mock_client): """Test successful creation of a user.""" client = mock_client - request_data = UserCreate( - full_name="Test User", - email="test@example.com" - ) + request_data = UserCreate(full_name="Test User", email="test@example.com") response_data = MockResponseBuilder.user() - response_data.update({ - "id": 123, - "full_name": "Test User", - "email": "test@example.com" - }) + response_data.update( + {"id": 123, "full_name": "Test User", "email": "test@example.com"} + ) client.http_client.add_response("/users", response_data) - + user = client.users.create(request_data) - + assert isinstance(user, User) assert user.full_name == "Test User" assert user.email == "test@example.com" @@ -122,13 +126,15 @@ def test_create_user_success(self, mock_client): def test_create_user_validation_error(self, mock_client): """Test user creation with validation error.""" client = mock_client - request_data = UserCreate( - full_name="Test User", - email="invalid-email" + request_data = UserCreate(full_name="Test User", email="invalid-email") + client.http_client.add_error( + "/users", + HttpClientError( + "Validation failed", + status_code=400, + response={"message": "Invalid email format"}, + ), ) - client.http_client.add_error("/users", - HttpClientError("Validation failed", status_code=400, - response={"message": "Invalid email format"})) with pytest.raises(ValidationError): client.users.create(request_data) @@ -140,9 +146,9 @@ def test_update_user_success(self, mock_client): response_data = MockResponseBuilder.user() response_data.update({"id": 123, "full_name": "Updated User"}) client.http_client.add_response("/users/123", response_data) - + user = client.users.update(123, request_data) - + assert isinstance(user, User) client.http_client.assert_request_made("PUT", "/users/123") @@ -150,26 +156,28 @@ def test_delete_user_success(self, mock_client): """Test successful deletion of a user.""" client = mock_client client.http_client.add_response("/users/123", {"status": "deleted"}) - + result = client.users.delete(123) - + assert result["status"] == "deleted" client.http_client.assert_request_made("DELETE", "/users/123") def test_get_settings_success(self, mock_client): """Test successful getting of user settings.""" client = mock_client - settings_data = [{ - "id": "setting1", - "owner": {"id": 123, "name": "Test User"}, - "org": {"id": 1, "name": "Test Org"}, - "user_settings_type": "general", - "settings": {"theme": "dark"} - }] + settings_data = [ + { + "id": "setting1", + "owner": {"id": 123, "name": "Test User"}, + "org": {"id": 1, "name": "Test Org"}, + "user_settings_type": "general", + "settings": {"theme": "dark"}, + } + ] client.http_client.add_response("/user_settings", settings_data) - + settings = client.users.get_settings() - + assert len(settings) == 1 assert isinstance(settings[0], UserSettings) client.http_client.assert_request_made("GET", "/user_settings") @@ -179,30 +187,34 @@ def test_get_quarantine_settings_success(self, mock_client): client = mock_client settings_data = {"enabled": True, "path": "/quarantine"} client.http_client.add_response("/users/123/quarantine_settings", settings_data) - + settings = client.users.get_quarantine_settings(123) - + assert settings["enabled"] client.http_client.assert_request_made("GET", "/users/123/quarantine_settings") def test_http_error_handling(self, mock_client): """Test proper HTTP error handling.""" client = mock_client - client.http_client.add_error("/users", - HttpClientError("Server Error", status_code=500, response={"message": "Internal error"})) - + client.http_client.add_error( + "/users", + HttpClientError( + "Server Error", status_code=500, response={"message": "Internal error"} + ), + ) + with pytest.raises(ServerError) as exc_info: client.users.list() - + assert exc_info.value.status_code == 500 def test_empty_list_response(self, mock_client): """Test handling of empty list response.""" client = mock_client client.http_client.add_response("/users", []) - + users = client.users.list() - + assert users == [] def test_user_with_org_memberships(self, mock_client): @@ -213,13 +225,12 @@ def test_user_with_org_memberships(self, mock_client): org_membership1["id"] = 1 org_membership2 = MockResponseBuilder.org_membership() org_membership2["id"] = 2 - user_data.update({ - "id": 123, - "org_memberships": [org_membership1, org_membership2] - }) + user_data.update( + {"id": 123, "org_memberships": [org_membership1, org_membership2]} + ) client.http_client.add_response("/users/123", user_data) - + user = client.users.get(123) - + assert len(user.org_memberships) == 2 - assert user.org_memberships[0].api_key is not None \ No newline at end of file + assert user.org_memberships[0].api_key is not None diff --git a/tests/unit/test_webhooks.py b/tests/unit/test_webhooks.py index c519e67..7f512c4 100644 --- a/tests/unit/test_webhooks.py +++ b/tests/unit/test_webhooks.py @@ -1,13 +1,14 @@ """Unit tests for webhooks resource.""" -import pytest + import base64 -from unittest.mock import MagicMock -from nexla_sdk.resources.webhooks import WebhooksResource -from nexla_sdk.models.webhooks.requests import WebhookSendOptions -from nexla_sdk.models.webhooks.responses import WebhookResponse +import pytest + from nexla_sdk.exceptions import NexlaError from nexla_sdk.http_client import HttpClientError +from nexla_sdk.models.webhooks.requests import WebhookSendOptions +from nexla_sdk.models.webhooks.responses import WebhookResponse +from nexla_sdk.resources.webhooks import WebhooksResource from tests.utils.fixtures import MockHTTPClient from tests.utils.mock_builders import MockResponseBuilder @@ -20,9 +21,7 @@ class TestWebhooksResourceModels: def test_webhook_send_options_model(self): """Test WebhookSendOptions model with all fields.""" options = WebhookSendOptions( - include_headers=True, - include_url_params=True, - force_schema_detection=True + include_headers=True, include_url_params=True, force_schema_detection=True ) assert options.include_headers is True assert options.include_url_params is True @@ -78,8 +77,7 @@ def test_send_one_record_success(self): record = {"event": "page_view", "user_id": 123} response = webhooks.send_one_record( - webhook_url="https://api.nexla.com/webhook/abc123", - record=record + webhook_url="https://api.nexla.com/webhook/abc123", record=record ) assert isinstance(response, WebhookResponse) @@ -101,15 +99,13 @@ def test_send_one_record_with_options(self): webhooks = WebhooksResource(api_key="test-api-key", http_client=http_client) options = WebhookSendOptions( - include_headers=True, - include_url_params=True, - force_schema_detection=True + include_headers=True, include_url_params=True, force_schema_detection=True ) response = webhooks.send_one_record( webhook_url="https://api.nexla.com/webhook/abc123", record={"event": "click"}, - options=options + options=options, ) assert isinstance(response, WebhookResponse) @@ -131,7 +127,7 @@ def test_send_one_record_query_auth(self): webhooks.send_one_record( webhook_url="https://api.nexla.com/webhook/abc123", record={"data": "test"}, - auth_method="query" + auth_method="query", ) last_request = http_client.get_last_request() @@ -150,7 +146,7 @@ def test_send_one_record_header_auth(self): webhooks.send_one_record( webhook_url="https://api.nexla.com/webhook/abc123", record={"data": "test"}, - auth_method="header" + auth_method="header", ) last_request = http_client.get_last_request() @@ -175,12 +171,11 @@ def test_send_many_records_success(self): records = [ {"event": "page_view", "page": "/home"}, {"event": "page_view", "page": "/about"}, - {"event": "click", "button": "signup"} + {"event": "click", "button": "signup"}, ] response = webhooks.send_many_records( - webhook_url="https://api.nexla.com/webhook/abc123", - records=records + webhook_url="https://api.nexla.com/webhook/abc123", records=records ) assert isinstance(response, WebhookResponse) @@ -201,8 +196,7 @@ def test_send_many_records_empty_list(self): webhooks = WebhooksResource(api_key="test-api-key", http_client=http_client) response = webhooks.send_many_records( - webhook_url="https://api.nexla.com/webhook/abc123", - records=[] + webhook_url="https://api.nexla.com/webhook/abc123", records=[] ) assert isinstance(response, WebhookResponse) @@ -219,16 +213,14 @@ def test_send_many_records_with_all_options(self): webhooks = WebhooksResource(api_key="test-api-key", http_client=http_client) options = WebhookSendOptions( - include_headers=True, - include_url_params=True, - force_schema_detection=True + include_headers=True, include_url_params=True, force_schema_detection=True ) webhooks.send_many_records( webhook_url="https://api.nexla.com/webhook/abc123", records=[{"id": 1}, {"id": 2}], options=options, - auth_method="header" + auth_method="header", ) last_request = http_client.get_last_request() @@ -253,8 +245,8 @@ def test_send_one_record_network_error(self): HttpClientError( message="Connection refused", status_code=500, - response={"error": "Server error"} - ) + response={"error": "Server error"}, + ), ) webhooks = WebhooksResource(api_key="test-api-key", http_client=http_client) @@ -262,7 +254,7 @@ def test_send_one_record_network_error(self): with pytest.raises(NexlaError) as exc_info: webhooks.send_one_record( webhook_url="https://api.nexla.com/webhook/abc123", - record={"data": "test"} + record={"data": "test"}, ) assert "Webhook request failed" in str(exc_info.value) @@ -275,16 +267,15 @@ def test_send_many_records_network_error(self): HttpClientError( message="Timeout", status_code=504, - response={"error": "Gateway timeout"} - ) + response={"error": "Gateway timeout"}, + ), ) webhooks = WebhooksResource(api_key="test-api-key", http_client=http_client) with pytest.raises(NexlaError) as exc_info: webhooks.send_many_records( - webhook_url="https://api.nexla.com/webhook/abc123", - records=[{"id": 1}] + webhook_url="https://api.nexla.com/webhook/abc123", records=[{"id": 1}] ) assert "Webhook request failed" in str(exc_info.value) @@ -297,8 +288,8 @@ def test_error_includes_context(self): HttpClientError( message="Bad Request", status_code=400, - response={"error": "Invalid payload"} - ) + response={"error": "Invalid payload"}, + ), ) webhooks = WebhooksResource(api_key="test-api-key", http_client=http_client) @@ -306,8 +297,7 @@ def test_error_includes_context(self): with pytest.raises(NexlaError) as exc_info: webhooks.send_one_record( - webhook_url=webhook_url, - record={"invalid": "data"} + webhook_url=webhook_url, record={"invalid": "data"} ) error = exc_info.value diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index 21172e0..dd4923a 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -1,55 +1,79 @@ """Test utilities for Nexla SDK testing.""" -from .mock_builders import ( - MockResponseBuilder, MockDataFactory, - credential_list, source_list, destination_list, lookup_list, - user_list, team_list, project_list -) -from .fixtures import MockHTTPClient, create_mock_response, create_http_error, create_paginated_response from .assertions import ( - assert_api_call_made, assert_model_valid, assert_model_list_valid, - assert_validation_error, assert_credential_structure, assert_source_structure, - assert_destination_structure, assert_nexset_structure, assert_lookup_structure, - assert_user_structure, assert_organization_structure, assert_team_structure, - assert_project_structure, assert_notification_structure, - assert_probe_response_structure, assert_error_response_structure, - assert_paginated_response_structure, assert_metrics_response_structure, - assert_flow_response_structure, assert_datetime_field_valid, assert_list_field_valid + assert_api_call_made, + assert_credential_structure, + assert_datetime_field_valid, + assert_destination_structure, + assert_error_response_structure, + assert_flow_response_structure, + assert_list_field_valid, + assert_lookup_structure, + assert_metrics_response_structure, + assert_model_list_valid, + assert_model_valid, + assert_nexset_structure, + assert_notification_structure, + assert_organization_structure, + assert_paginated_response_structure, + assert_probe_response_structure, + assert_project_structure, + assert_source_structure, + assert_team_structure, + assert_user_structure, + assert_validation_error, +) +from .fixtures import ( + MockHTTPClient, + create_http_error, + create_mock_response, + create_paginated_response, +) +from .mock_builders import ( + MockDataFactory, + MockResponseBuilder, + credential_list, + destination_list, + lookup_list, + project_list, + source_list, + team_list, + user_list, ) __all__ = [ - 'MockResponseBuilder', - 'MockDataFactory', - 'MockHTTPClient', - 'create_mock_response', - 'create_http_error', - 'create_paginated_response', - 'assert_api_call_made', - 'assert_model_valid', - 'assert_model_list_valid', - 'assert_validation_error', - 'assert_credential_structure', - 'assert_source_structure', - 'assert_destination_structure', - 'assert_nexset_structure', - 'assert_lookup_structure', - 'assert_user_structure', - 'assert_organization_structure', - 'assert_team_structure', - 'assert_project_structure', - 'assert_notification_structure', - 'assert_probe_response_structure', - 'assert_error_response_structure', - 'assert_paginated_response_structure', - 'assert_metrics_response_structure', - 'assert_flow_response_structure', - 'assert_datetime_field_valid', - 'assert_list_field_valid', - 'credential_list', - 'source_list', - 'destination_list', - 'lookup_list', - 'user_list', - 'team_list', - 'project_list' -] \ No newline at end of file + "MockResponseBuilder", + "MockDataFactory", + "MockHTTPClient", + "create_mock_response", + "create_http_error", + "create_paginated_response", + "assert_api_call_made", + "assert_model_valid", + "assert_model_list_valid", + "assert_validation_error", + "assert_credential_structure", + "assert_source_structure", + "assert_destination_structure", + "assert_nexset_structure", + "assert_lookup_structure", + "assert_user_structure", + "assert_organization_structure", + "assert_team_structure", + "assert_project_structure", + "assert_notification_structure", + "assert_probe_response_structure", + "assert_error_response_structure", + "assert_paginated_response_structure", + "assert_metrics_response_structure", + "assert_flow_response_structure", + "assert_datetime_field_valid", + "assert_list_field_valid", + "credential_list", + "source_list", + "destination_list", + "lookup_list", + "user_list", + "team_list", + "project_list", +] diff --git a/tests/utils/assertions.py b/tests/utils/assertions.py index efe1166..a78bf1e 100644 --- a/tests/utils/assertions.py +++ b/tests/utils/assertions.py @@ -1,16 +1,22 @@ """Custom assertions for testing.""" from typing import Any, Dict, List, Optional, Type + from pydantic import ValidationError + from nexla_sdk.models.base import BaseModel -from nexla_sdk.models.destinations.responses import Destination, DataSetInfo, DataMapInfo -from nexla_sdk.models.flows.responses import FlowResponse, FlowMetrics from nexla_sdk.models.common import FlowNode +from nexla_sdk.models.destinations.responses import ( + DataMapInfo, + DataSetInfo, + Destination, +) +from nexla_sdk.models.flows.responses import FlowMetrics, FlowResponse from nexla_sdk.models.lookups.responses import Lookup -from nexla_sdk.models.sources.responses import Source from nexla_sdk.models.nexsets.responses import Nexset -from nexla_sdk.models.projects.responses import Project, ProjectDataFlow from nexla_sdk.models.organizations.responses import Organization, OrgMember +from nexla_sdk.models.projects.responses import Project, ProjectDataFlow +from nexla_sdk.models.sources.responses import Source def assert_api_call_made(mock_http_client, method: str, url_pattern: str, **kwargs): @@ -18,17 +24,23 @@ def assert_api_call_made(mock_http_client, method: str, url_pattern: str, **kwar mock_http_client.assert_request_made(method, url_pattern, **kwargs) -def assert_model_valid(model_instance: BaseModel, expected_fields: Optional[Dict[str, Any]] = None): +def assert_model_valid( + model_instance: BaseModel, expected_fields: Optional[Dict[str, Any]] = None +): """Assert that a model instance is valid and optionally check specific fields.""" # Check that it's a valid model instance - assert isinstance(model_instance, BaseModel), f"Expected BaseModel instance, got {type(model_instance)}" - + assert isinstance( + model_instance, BaseModel + ), f"Expected BaseModel instance, got {type(model_instance)}" + # Check that required fields are present and have expected values if expected_fields: for field_name, expected_value in expected_fields.items(): actual_value = getattr(model_instance, field_name, None) - assert actual_value == expected_value, f"Expected {field_name}={expected_value}, got {actual_value}" - + assert ( + actual_value == expected_value + ), f"Expected {field_name}={expected_value}, got {actual_value}" + # Ensure the model can be serialized (no validation errors) try: model_instance.model_dump() @@ -39,9 +51,11 @@ def assert_model_valid(model_instance: BaseModel, expected_fields: Optional[Dict def assert_model_list_valid(model_list: List[BaseModel], model_class: Type[BaseModel]): """Assert that a list contains valid model instances of the expected type.""" assert isinstance(model_list, list), f"Expected list, got {type(model_list)}" - + for i, item in enumerate(model_list): - assert isinstance(item, model_class), f"Item {i} is not of type {model_class.__name__}: {type(item)}" + assert isinstance( + item, model_class + ), f"Item {i} is not of type {model_class.__name__}: {type(item)}" assert_model_valid(item) @@ -52,7 +66,9 @@ def assert_validation_error(func, error_message_contains: Optional[str] = None): raise AssertionError("Expected ValidationError to be raised") except ValidationError as e: if error_message_contains: - assert error_message_contains in str(e), f"Error message should contain '{error_message_contains}': {e}" + assert error_message_contains in str( + e + ), f"Error message should contain '{error_message_contains}': {e}" except Exception as e: raise AssertionError(f"Expected ValidationError, got {type(e).__name__}: {e}") @@ -63,17 +79,19 @@ def assert_credential_structure(credential_data: Dict[str, Any]): required_fields = ["id", "name", "credentials_type", "owner", "org", "access_roles"] for field in required_fields: assert field in credential_data, f"Credential missing required field: {field}" - + # Check owner structure owner = credential_data["owner"] assert "id" in owner and "full_name" in owner, "Owner missing required fields" - + # Check org structure org = credential_data["org"] assert "id" in org and "name" in org, "Organization missing required fields" - + # Check access roles - assert isinstance(credential_data["access_roles"], list), "access_roles should be a list" + assert isinstance( + credential_data["access_roles"], list + ), "access_roles should be a list" assert len(credential_data["access_roles"]) > 0, "access_roles should not be empty" @@ -83,30 +101,34 @@ def assert_source_structure(source_data: Dict[str, Any]): required_fields = ["id", "name", "status", "source_type", "access_roles"] for field in required_fields: assert field in source_data, f"Source missing required field: {field}" - + # Check valid status values valid_statuses = ["ACTIVE", "PAUSED", "DRAFT", "DELETED", "ERROR", "INIT"] - assert source_data["status"] in valid_statuses, f"Invalid status: {source_data['status']}" - + assert ( + source_data["status"] in valid_statuses + ), f"Invalid status: {source_data['status']}" + # Check owner structure if present if "owner" in source_data and source_data["owner"]: owner = source_data["owner"] assert "id" in owner and "full_name" in owner, "Owner missing required fields" - + # Check org structure if present if "org" in source_data and source_data["org"]: org = source_data["org"] assert "id" in org and "name" in org, "Organization missing required fields" - + # Check access roles - assert isinstance(source_data["access_roles"], list), "access_roles should be a list" - + assert isinstance( + source_data["access_roles"], list + ), "access_roles should be a list" + # Check data_sets if present if "data_sets" in source_data and source_data["data_sets"]: assert isinstance(source_data["data_sets"], list), "data_sets should be a list" for dataset in source_data["data_sets"]: assert "id" in dataset, "Dataset missing required id field" - + # Check data_credentials if present if "data_credentials" in source_data and source_data["data_credentials"]: assert_credential_structure(source_data["data_credentials"]) @@ -118,16 +140,18 @@ def assert_destination_structure(destination_data: Dict[str, Any]): required_fields = ["id", "name", "status", "sink_type", "access_roles"] for field in required_fields: assert field in destination_data, f"Destination missing required field: {field}" - + # Check valid status values valid_statuses = ["ACTIVE", "PAUSED", "DRAFT", "DELETED", "ERROR"] - assert destination_data["status"] in valid_statuses, f"Invalid status: {destination_data['status']}" - + assert ( + destination_data["status"] in valid_statuses + ), f"Invalid status: {destination_data['status']}" + # Check owner structure if present if "owner" in destination_data and destination_data["owner"]: owner = destination_data["owner"] assert "id" in owner and "full_name" in owner, "Owner missing required fields" - + # Check org structure if present if "org" in destination_data and destination_data["org"]: org = destination_data["org"] @@ -140,28 +164,38 @@ def assert_nexset_structure(nexset_data: Dict[str, Any]): required_fields = ["id", "access_roles"] for field in required_fields: assert field in nexset_data, f"Nexset missing required field: {field}" - + # Check owner structure if present if "owner" in nexset_data and nexset_data["owner"]: owner = nexset_data["owner"] assert "id" in owner and "full_name" in owner, "Owner missing required fields" - + # Check data_sinks if present if "data_sinks" in nexset_data and nexset_data["data_sinks"]: - assert isinstance(nexset_data["data_sinks"], list), "data_sinks should be a list" + assert isinstance( + nexset_data["data_sinks"], list + ), "data_sinks should be a list" def assert_lookup_structure(lookup_data: Dict[str, Any]): """Assert that lookup data has the expected structure.""" # Required fields - required_fields = ["id", "name", "description", "map_primary_key", "owner", "org", "access_roles"] + required_fields = [ + "id", + "name", + "description", + "map_primary_key", + "owner", + "org", + "access_roles", + ] for field in required_fields: assert field in lookup_data, f"Lookup missing required field: {field}" - + # Check owner structure owner = lookup_data["owner"] assert "id" in owner and "full_name" in owner, "Owner missing required fields" - + # Check org structure org = lookup_data["org"] assert "id" in org and "name" in org, "Organization missing required fields" @@ -173,14 +207,18 @@ def assert_user_structure(user_data: Dict[str, Any]): required_fields = ["id", "email", "full_name", "default_org", "status"] for field in required_fields: assert field in user_data, f"User missing required field: {field}" - + # Check default_org structure default_org = user_data["default_org"] - assert "id" in default_org and "name" in default_org, "Default org missing required fields" - + assert ( + "id" in default_org and "name" in default_org + ), "Default org missing required fields" + # Check org_memberships if present if "org_memberships" in user_data: - assert isinstance(user_data["org_memberships"], list), "org_memberships should be a list" + assert isinstance( + user_data["org_memberships"], list + ), "org_memberships should be a list" def assert_organization_structure(org_data: Dict[str, Any]): @@ -189,7 +227,7 @@ def assert_organization_structure(org_data: Dict[str, Any]): required_fields = ["id", "name", "email_domain", "access_roles", "owner", "status"] for field in required_fields: assert field in org_data, f"Organization missing required field: {field}" - + # Check owner structure owner = org_data["owner"] assert_user_structure(owner) @@ -201,11 +239,11 @@ def assert_team_structure(team_data: Dict[str, Any]): required_fields = ["id", "name", "description", "owner", "org", "access_roles"] for field in required_fields: assert field in team_data, f"Team missing required field: {field}" - + # Check owner structure owner = team_data["owner"] assert "id" in owner and "full_name" in owner, "Owner missing required fields" - + # Check members if present if "members" in team_data: assert isinstance(team_data["members"], list), "members should be a list" @@ -217,11 +255,13 @@ def assert_project_structure(project_data: Dict[str, Any]): required_fields = ["id", "owner", "org", "name", "description", "access_roles"] for field in required_fields: assert field in project_data, f"Project missing required field: {field}" - + # Check data_flows if present if "data_flows" in project_data: - assert isinstance(project_data["data_flows"], list), "data_flows should be a list" - + assert isinstance( + project_data["data_flows"], list + ), "data_flows should be a list" + # Check flows if present if "flows" in project_data: assert isinstance(project_data["flows"], list), "flows should be a list" @@ -230,13 +270,26 @@ def assert_project_structure(project_data: Dict[str, Any]): def assert_notification_structure(notification_data: Dict[str, Any]): """Assert that notification data has the expected structure.""" # Required fields - required_fields = ["id", "owner", "org", "access_roles", "level", "resource_id", "resource_type", "message"] + required_fields = [ + "id", + "owner", + "org", + "access_roles", + "level", + "resource_id", + "resource_type", + "message", + ] for field in required_fields: - assert field in notification_data, f"Notification missing required field: {field}" - + assert ( + field in notification_data + ), f"Notification missing required field: {field}" + # Check valid levels valid_levels = ["DEBUG", "INFO", "WARN", "ERROR", "RECOVERED", "RESOLVED"] - assert notification_data["level"] in valid_levels, f"Invalid level: {notification_data['level']}" + assert ( + notification_data["level"] in valid_levels + ), f"Invalid level: {notification_data['level']}" def assert_probe_response_structure(probe_data: Dict[str, Any]): @@ -244,18 +297,26 @@ def assert_probe_response_structure(probe_data: Dict[str, Any]): required_fields = ["status", "message", "connection_type"] for field in required_fields: assert field in probe_data, f"Probe response missing required field: {field}" - - assert probe_data["status"] in ["ok", "success", "error"], f"Invalid probe status: {probe_data['status']}" + + assert probe_data["status"] in [ + "ok", + "success", + "error", + ], f"Invalid probe status: {probe_data['status']}" def assert_error_response_structure(error_data: Dict[str, Any]): """Assert that error response has the expected structure.""" # Should have either 'error' or 'message' field - assert "error" in error_data or "message" in error_data, "Error response missing error/message field" - + assert ( + "error" in error_data or "message" in error_data + ), "Error response missing error/message field" + # Should have some indication of the error type or status if "status_code" in error_data: - assert isinstance(error_data["status_code"], int), "status_code should be an integer" + assert isinstance( + error_data["status_code"], int + ), "status_code should be an integer" def assert_paginated_response_structure(response_data: Dict[str, Any]): @@ -265,21 +326,25 @@ def assert_paginated_response_structure(response_data: Dict[str, Any]): assert "currentPage" in meta, "Pagination meta missing currentPage" assert "totalCount" in meta, "Pagination meta missing totalCount" assert "pageCount" in meta, "Pagination meta missing pageCount" - + if "data" in response_data: - assert isinstance(response_data["data"], list), "Paginated data should be a list" + assert isinstance( + response_data["data"], list + ), "Paginated data should be a list" def assert_metrics_response_structure(metrics_data: Dict[str, Any]): """Assert that metrics response has the expected structure.""" assert "status" in metrics_data, "Metrics response missing status field" assert "metrics" in metrics_data, "Metrics response missing metrics field" - + if metrics_data["status"] == 200: metrics = metrics_data["metrics"] if isinstance(metrics, dict): # Single metrics object - assert "records" in metrics or "size" in metrics, "Metrics should have records or size" + assert ( + "records" in metrics or "size" in metrics + ), "Metrics should have records or size" elif isinstance(metrics, list): # List of metrics (e.g., daily metrics) for metric in metrics: @@ -290,7 +355,7 @@ def assert_flow_response_structure(flow_data: Dict[str, Any]): """Assert that flow response has the expected structure.""" assert "flows" in flow_data, "Flow response missing flows field" assert isinstance(flow_data["flows"], list), "flows should be a list" - + # Check optional elements optional_lists = ["data_sources", "data_sets", "data_sinks", "data_credentials"] for field in optional_lists: @@ -298,7 +363,9 @@ def assert_flow_response_structure(flow_data: Dict[str, Any]): assert isinstance(flow_data[field], list), f"{field} should be a list" -def assert_datetime_field_valid(data: Dict[str, Any], field_name: str, required: bool = False): +def assert_datetime_field_valid( + data: Dict[str, Any], field_name: str, required: bool = False +): """Assert that a datetime field is valid if present.""" if field_name in data: datetime_value = data[field_name] @@ -311,13 +378,17 @@ def assert_datetime_field_valid(data: Dict[str, Any], field_name: str, required: raise AssertionError(f"Required datetime field {field_name} is missing") -def assert_list_field_valid(data: Dict[str, Any], field_name: str, required: bool = False, min_length: int = 0): +def assert_list_field_valid( + data: Dict[str, Any], field_name: str, required: bool = False, min_length: int = 0 +): """Assert that a list field is valid if present.""" if field_name in data: list_value = data[field_name] if list_value is not None: assert isinstance(list_value, list), f"{field_name} should be a list" - assert len(list_value) >= min_length, f"{field_name} should have at least {min_length} items" + assert ( + len(list_value) >= min_length + ), f"{field_name} should have at least {min_length} items" elif required: raise AssertionError(f"Required list field {field_name} is missing") @@ -329,7 +400,7 @@ def assert_owner_response(actual, expected: Dict[str, Any]) -> None: assert actual.id == expected["id"] assert actual.full_name == expected["full_name"] assert actual.email == expected["email"] - + @staticmethod def assert_source_response(response: Source, expected_data: Dict[str, Any]): """Assert source response matches expected data.""" @@ -343,22 +414,24 @@ def assert_source_response(response: Source, expected_data: Dict[str, Any]): if "org" in expected_data: assert response.org.id == expected_data["org"]["id"] assert response.org.name == expected_data["org"]["name"] - + @staticmethod def assert_credential_response(actual, expected: Dict[str, Any]) -> None: """Assert credential response matches expected data.""" assert actual.id == expected["id"] assert actual.name == expected["name"] assert actual.credentials_type == expected["credentials_type"] - + if expected.get("owner"): NexlaAssertions.assert_owner_response(actual.owner, expected["owner"]) - + if expected.get("org"): NexlaAssertions.assert_organization_response(actual.org, expected["org"]) - + @staticmethod - def assert_destination_response(response: Destination, expected_data: Dict[str, Any]): + def assert_destination_response( + response: Destination, expected_data: Dict[str, Any] + ): """Assert destination response matches expected data.""" assert response.id == expected_data["id"] assert response.name == expected_data["name"] @@ -372,12 +445,12 @@ def assert_destination_response(response: Destination, expected_data: Dict[str, if "org" in expected_data: assert response.org.id == expected_data["org"]["id"] assert response.org.name == expected_data["org"]["name"] - + @staticmethod def assert_flow_node(actual: FlowNode, expected: Dict[str, Any]) -> None: """Assert flow node matches expected data.""" assert actual.id == expected["id"] - + # Check parent/source relationships if "parent_node_id" in expected: assert actual.parent_node_id == expected.get("parent_node_id") @@ -387,7 +460,7 @@ def assert_flow_node(actual: FlowNode, expected: Dict[str, Any]) -> None: assert actual.data_set_id == expected.get("data_set_id") if "data_sink_id" in expected: assert actual.data_sink_id == expected.get("data_sink_id") - + # Check optional fields if expected.get("status"): assert actual.status == expected["status"] @@ -395,11 +468,13 @@ def assert_flow_node(actual: FlowNode, expected: Dict[str, Any]) -> None: assert actual.name == expected["name"] if expected.get("description"): assert actual.description == expected["description"] - + # Recursively check children if present if expected.get("children") and actual.children: assert len(actual.children) == len(expected["children"]) - for actual_child, expected_child in zip(actual.children, expected["children"]): + for actual_child, expected_child in zip( + actual.children, expected["children"] + ): NexlaAssertions.assert_flow_node(actual_child, expected_child) @staticmethod @@ -409,25 +484,31 @@ def assert_flow_response(actual: FlowResponse, expected: Dict[str, Any]) -> None assert len(actual.flows) == len(expected["flows"]) for actual_flow, expected_flow in zip(actual.flows, expected["flows"]): NexlaAssertions.assert_flow_node(actual_flow, expected_flow) - + # Check optional expanded elements if expected.get("data_sources") and actual.data_sources: assert len(actual.data_sources) == len(expected["data_sources"]) - for actual_src, expected_src in zip(actual.data_sources, expected["data_sources"]): + for actual_src, expected_src in zip( + actual.data_sources, expected["data_sources"] + ): NexlaAssertions.assert_source_response(actual_src, expected_src) - + if expected.get("data_sets") and actual.data_sets: assert len(actual.data_sets) == len(expected["data_sets"]) # Note: Would need assert_nexset_response method if checking details - + if expected.get("data_sinks") and actual.data_sinks: assert len(actual.data_sinks) == len(expected["data_sinks"]) - for actual_sink, expected_sink in zip(actual.data_sinks, expected["data_sinks"]): + for actual_sink, expected_sink in zip( + actual.data_sinks, expected["data_sinks"] + ): NexlaAssertions.assert_destination_response(actual_sink, expected_sink) - + if expected.get("data_credentials") and actual.data_credentials: assert len(actual.data_credentials) == len(expected["data_credentials"]) - for actual_cred, expected_cred in zip(actual.data_credentials, expected["data_credentials"]): + for actual_cred, expected_cred in zip( + actual.data_credentials, expected["data_credentials"] + ): NexlaAssertions.assert_credential_response(actual_cred, expected_cred) @staticmethod @@ -458,7 +539,7 @@ def assert_lookup_response(response: Lookup, expected_data: Dict[str, Any]): if "org" in expected_data: assert response.org.id == expected_data["org"]["id"] assert response.org.name == expected_data["org"]["name"] - + @staticmethod def assert_lookup_entry(entry: Dict[str, Any], expected_data: Dict[str, Any]): """Assert lookup entry matches expected data.""" @@ -480,17 +561,21 @@ def assert_nexset_response(self, response: Nexset, expected_data: Dict[str, Any] if "org" in expected_data: assert response.org.id == expected_data["org"]["id"] assert response.org.name == expected_data["org"]["name"] - + def assert_nexset_sample(self, sample): """Assert nexset sample has expected structure.""" - assert hasattr(sample, 'raw_message'), "Sample should have raw_message" + assert hasattr(sample, "raw_message"), "Sample should have raw_message" assert isinstance(sample.raw_message, dict), "Sample raw_message should be dict" - - # If metadata exists, validate it - if hasattr(sample, 'nexla_metadata') and sample.nexla_metadata: - assert isinstance(sample.nexla_metadata, dict), "Sample nexla_metadata should be dict" - def assert_data_set_info_response(self, response: DataSetInfo, expected_data: Dict[str, Any]): + # If metadata exists, validate it + if hasattr(sample, "nexla_metadata") and sample.nexla_metadata: + assert isinstance( + sample.nexla_metadata, dict + ), "Sample nexla_metadata should be dict" + + def assert_data_set_info_response( + self, response: DataSetInfo, expected_data: Dict[str, Any] + ): """Assert data set info response matches expected data.""" assert response.id == expected_data["id"] assert response.name == expected_data["name"] @@ -499,7 +584,9 @@ def assert_data_set_info_response(self, response: DataSetInfo, expected_data: Di if "status" in expected_data: assert response.status == expected_data["status"] - def assert_data_map_info_response(self, response: DataMapInfo, expected_data: Dict[str, Any]): + def assert_data_map_info_response( + self, response: DataMapInfo, expected_data: Dict[str, Any] + ): """Assert data map info response matches expected data.""" assert response.id == expected_data["id"] assert response.name == expected_data["name"] @@ -530,7 +617,7 @@ def assert_project_response(response: Project, expected_data: Dict[str, Any]): assert response.access_roles == expected_data["access_roles"] if "tags" in expected_data: assert response.tags == expected_data["tags"] - + @staticmethod def assert_user_response(response, expected_data: Dict[str, Any]): """Assert user response matches expected data.""" @@ -551,8 +638,10 @@ def assert_user_response(response, expected_data: Dict[str, Any]): if "api_key" in expected_data: assert response.api_key == expected_data["api_key"] if "org_memberships" in expected_data: - assert len(response.org_memberships) == len(expected_data["org_memberships"]) - + assert len(response.org_memberships) == len( + expected_data["org_memberships"] + ) + @staticmethod def assert_team_response(response, expected_data: Dict[str, Any]): """Assert team response matches expected data.""" @@ -572,7 +661,7 @@ def assert_team_response(response, expected_data: Dict[str, Any]): assert response.access_roles == expected_data["access_roles"] if "members" in expected_data: assert len(response.members) == len(expected_data["members"]) - + @staticmethod def assert_team_member_response(response, expected_data: Dict[str, Any]): """Assert team member response matches expected data.""" @@ -580,9 +669,11 @@ def assert_team_member_response(response, expected_data: Dict[str, Any]): assert response.email == expected_data["email"] if "admin" in expected_data: assert response.admin == expected_data["admin"] - + @staticmethod - def assert_project_data_flow_response(response: ProjectDataFlow, expected_data: Dict[str, Any]): + def assert_project_data_flow_response( + response: ProjectDataFlow, expected_data: Dict[str, Any] + ): """Assert project data flow response matches expected data.""" assert response.id == expected_data["id"] assert response.project_id == expected_data["project_id"] @@ -595,14 +686,16 @@ def assert_project_data_flow_response(response: ProjectDataFlow, expected_data: if "name" in expected_data: assert response.name == expected_data["name"] if "description" in expected_data: - assert response.description == expected_data["description"] + assert response.description == expected_data["description"] @staticmethod - def assert_organization_response(response: Organization, expected_data: Dict[str, Any]): + def assert_organization_response( + response: Organization, expected_data: Dict[str, Any] + ): """Assert organization response matches expected data.""" assert isinstance(response, Organization) for key, expected_value in expected_data.items(): - if key == 'account_tier' and response.org_tier: + if key == "account_tier" and response.org_tier: # Special handling for nested model for k, v in expected_value.items(): assert getattr(response.org_tier, k) == v @@ -613,6 +706,6 @@ def assert_organization_response(response: Organization, expected_data: Dict[str def assert_org_member_response(response: OrgMember, expected_data: Dict[str, Any]): """Assert org member response matches expected data.""" assert isinstance(response, OrgMember) - assert response.id == expected_data.get('id') - assert response.email == expected_data.get('email') - assert response.is_admin == expected_data.get('is_admin?') \ No newline at end of file + assert response.id == expected_data.get("id") + assert response.email == expected_data.get("email") + assert response.is_admin == expected_data.get("is_admin?") diff --git a/tests/utils/fixtures.py b/tests/utils/fixtures.py index 6a2d2c2..1aa06f9 100644 --- a/tests/utils/fixtures.py +++ b/tests/utils/fixtures.py @@ -1,19 +1,22 @@ """Test fixtures and mock HTTP client.""" -from typing import Dict, Any, Optional, List, Callable, Union -from nexla_sdk.http_client import HttpClientInterface, HttpClientError +from typing import Any, Callable, Dict, List, Optional, Union + +from nexla_sdk.http_client import HttpClientError, HttpClientInterface class MockHTTPClient(HttpClientInterface): """Mock HTTP client for testing that records requests and returns configured responses.""" - + def __init__(self): self.requests = [] # Track all requests made self.responses = {} # Map of URL patterns to responses self.response_queue = [] # Queue of responses to return in order self.default_response = {"status": "ok"} - - def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> Dict[str, Any]: + + def request( + self, method: str, url: str, headers: Dict[str, str], **kwargs + ) -> Dict[str, Any]: """Record request and return mock response.""" request_data = { "method": method, @@ -21,10 +24,10 @@ def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> D "headers": headers, "params": kwargs.get("params", {}), "data": kwargs.get("data", {}), - "json": kwargs.get("json", {}) + "json": kwargs.get("json", {}), } self.requests.append(request_data) - + # Look for configured response for pattern, response in self.responses.items(): if pattern in url: @@ -35,67 +38,72 @@ def request(self, method: str, url: str, headers: Dict[str, str], **kwargs) -> D raise response else: return response - + # Return from queue if available if self.response_queue: response = self.response_queue.pop(0) if isinstance(response, HttpClientError): raise response return response - + # Return default response return self.default_response - - def add_response(self, url_pattern: str, response: Union[Dict[str, Any], HttpClientError, Callable]): + + def add_response( + self, + url_pattern: str, + response: Union[Dict[str, Any], HttpClientError, Callable], + ): """Add a response for a specific URL pattern.""" self.responses[url_pattern] = response - + def add_error(self, url_pattern: str, error: HttpClientError): """Add an error response for a specific URL pattern.""" self.responses[url_pattern] = error - + def queue_response(self, response: Union[Dict[str, Any], HttpClientError]): """Queue a response to be returned in order.""" self.response_queue.append(response) - + def clear_responses(self): """Clear all configured responses.""" self.responses.clear() self.response_queue.clear() - + def clear_requests(self): """Clear the recorded requests.""" self.requests.clear() - + def get_last_request(self) -> Optional[Dict[str, Any]]: """Get the last request made.""" return self.requests[-1] if self.requests else None - + def get_request(self) -> Optional[Dict[str, Any]]: """Get the last request made (alias for get_last_request).""" return self.get_last_request() - + def get_requests_by_method(self, method: str) -> List[Dict[str, Any]]: """Get all requests made with a specific method.""" return [req for req in self.requests if req["method"] == method] - + def get_requests_by_url_pattern(self, pattern: str) -> List[Dict[str, Any]]: """Get all requests made to URLs containing the pattern.""" return [req for req in self.requests if pattern in req["url"]] - + def assert_request_made(self, method: str, url_pattern: str, **kwargs): """Assert that a specific request was made.""" matching_requests = [ - req for req in self.requests + req + for req in self.requests if req["method"] == method and url_pattern in req["url"] ] - + if not matching_requests: raise AssertionError( f"No {method} request to '{url_pattern}' found. " f"Requests made: {[req['method'] + ' ' + req['url'] for req in self.requests]}" ) - + # Check additional parameters if provided if kwargs: for key, expected_value in kwargs.items(): @@ -111,46 +119,50 @@ def assert_request_made(self, method: str, url_pattern: str, **kwargs): raise AssertionError( f"Expected params {expected_value}, got {actual_value}" ) - - def assert_no_unexpected_requests(self, expected_patterns: Optional[List[str]] = None): + + def assert_no_unexpected_requests( + self, expected_patterns: Optional[List[str]] = None + ): """Assert that all recorded requests match expected URL patterns. - + Args: expected_patterns: List of URL patterns that are expected. If None, uses the keys from the responses dictionary. - + Raises: AssertionError: If any requests don't match the expected patterns. """ # Use response keys as default expected patterns if none provided if expected_patterns is None: expected_patterns = list(self.responses.keys()) - + # If no expected patterns and no responses configured, all requests are unexpected if not expected_patterns: if self.requests: - unexpected_requests = [f"{req['method']} {req['url']}" for req in self.requests] + unexpected_requests = [ + f"{req['method']} {req['url']}" for req in self.requests + ] raise AssertionError( f"Unexpected requests found (no expected patterns configured): {unexpected_requests}" ) return - + # Check each request against expected patterns unexpected_requests = [] for request in self.requests: url = request["url"] method = request["method"] - + # Check if this request matches any expected pattern matches_pattern = False for pattern in expected_patterns: if pattern in url: matches_pattern = True break - + if not matches_pattern: unexpected_requests.append(f"{method} {url}") - + # Raise error if any unexpected requests found if unexpected_requests: raise AssertionError( @@ -159,88 +171,98 @@ def assert_no_unexpected_requests(self, expected_patterns: Optional[List[str]] = ) -def create_mock_response(data: Dict[str, Any], status_code: int = 200) -> Dict[str, Any]: +def create_mock_response( + data: Dict[str, Any], status_code: int = 200 +) -> Dict[str, Any]: """Create a mock response with the given data.""" - response = { - "status_code": status_code, - "data": data - } + response = {"status_code": status_code, "data": data} response.update(data) return response -def create_http_error(status_code: int, message: str, details: Optional[Dict[str, Any]] = None) -> HttpClientError: +def create_http_error( + status_code: int, message: str, details: Optional[Dict[str, Any]] = None +) -> HttpClientError: """Create an HTTP error for testing.""" - error_data = { - "error": message, - "status_code": status_code, - "message": message - } + error_data = {"error": message, "status_code": status_code, "message": message} if details: error_data.update(details) - + return HttpClientError( message=message, status_code=status_code, - response=error_data # Fixed: was 'response_data', should be 'response' + response=error_data, # Fixed: was 'response_data', should be 'response' ) -def create_paginated_response(items: List[Dict[str, Any]], page: int = 1, per_page: int = 20, total: Optional[int] = None) -> Dict[str, Any]: +def create_paginated_response( + items: List[Dict[str, Any]], + page: int = 1, + per_page: int = 20, + total: Optional[int] = None, +) -> Dict[str, Any]: """Create a paginated response with the given items.""" if total is None: total = len(items) - + total_pages = (total + per_page - 1) // per_page - + # Calculate the items for this page start_index = (page - 1) * per_page end_index = start_index + per_page page_items = items[start_index:end_index] - + return { "data": page_items, "meta": { "currentPage": page, "totalCount": total, "pageCount": total_pages, - "perPage": per_page - } + "perPage": per_page, + }, } -def create_auth_token_response(access_token: str = "mock-token-12345", expires_in: int = 86400) -> Dict[str, Any]: +def create_auth_token_response( + access_token: str = "mock-token-12345", expires_in: int = 86400 +) -> Dict[str, Any]: """Create a mock authentication token response.""" return { "access_token": access_token, "token_type": "Bearer", "expires_in": expires_in, - "scope": "read write" + "scope": "read write", } -def create_webhook_response(webhook_id: Optional[int] = None, **overrides) -> Dict[str, Any]: +def create_webhook_response( + webhook_id: Optional[int] = None, **overrides +) -> Dict[str, Any]: """Create a mock webhook response.""" from faker import Faker + fake = Faker() - + base = { "id": webhook_id or fake.random_int(1, 10000), "url": fake.url(), "active": True, "events": ["source.created", "source.updated", "source.deleted"], "created_at": fake.date_time().isoformat(), - "updated_at": fake.date_time().isoformat() + "updated_at": fake.date_time().isoformat(), } base.update(overrides) return base -def create_api_key_response(api_key_id: Optional[int] = None, **overrides) -> Dict[str, Any]: +def create_api_key_response( + api_key_id: Optional[int] = None, **overrides +) -> Dict[str, Any]: """Create a mock API key response.""" from faker import Faker + fake = Faker() - + base = { "id": api_key_id or fake.random_int(1, 10000), "name": f"API Key {fake.random_int(1, 100)}", @@ -248,72 +270,75 @@ def create_api_key_response(api_key_id: Optional[int] = None, **overrides) -> Di "active": True, "permissions": ["read", "write"], "created_at": fake.date_time().isoformat(), - "updated_at": fake.date_time().isoformat() + "updated_at": fake.date_time().isoformat(), } base.update(overrides) return base -def create_rate_limit_response(rate_limit_info: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: +def create_rate_limit_response( + rate_limit_info: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: """Create a mock rate limit response.""" default_info = { "limit": 1000, "remaining": 999, "reset": 1640995200, # Unix timestamp - "window": 3600 # 1 hour in seconds + "window": 3600, # 1 hour in seconds } - + if rate_limit_info: default_info.update(rate_limit_info) - - return { - "rate_limit": default_info, - "message": "Rate limit information" - } + + return {"rate_limit": default_info, "message": "Rate limit information"} -def create_health_check_response(status: str = "healthy", **overrides) -> Dict[str, Any]: +def create_health_check_response( + status: str = "healthy", **overrides +) -> Dict[str, Any]: """Create a mock health check response.""" from faker import Faker + fake = Faker() - + base = { "status": status, "timestamp": fake.date_time().isoformat(), "version": "1.0.0", "uptime": fake.random_int(1, 1000000), - "services": { - "database": "healthy", - "cache": "healthy", - "storage": "healthy" - } + "services": {"database": "healthy", "cache": "healthy", "storage": "healthy"}, } base.update(overrides) return base -def create_validation_error_response(field_errors: Optional[Dict[str, List[str]]] = None) -> Dict[str, Any]: +def create_validation_error_response( + field_errors: Optional[Dict[str, List[str]]] = None, +) -> Dict[str, Any]: """Create a mock validation error response.""" default_errors = { "name": ["This field is required"], - "email": ["Invalid email format"] + "email": ["Invalid email format"], } - + errors = field_errors or default_errors - + return { "error": "Validation failed", "status_code": 400, "field_errors": errors, - "message": "The request data is invalid" + "message": "The request data is invalid", } -def create_batch_response(items: List[Dict[str, Any]], batch_id: Optional[str] = None) -> Dict[str, Any]: +def create_batch_response( + items: List[Dict[str, Any]], batch_id: Optional[str] = None +) -> Dict[str, Any]: """Create a mock batch operation response.""" from faker import Faker + fake = Faker() - + return { "batch_id": batch_id or fake.uuid4(), "status": "completed", @@ -322,52 +347,52 @@ def create_batch_response(items: List[Dict[str, Any]], batch_id: Optional[str] = "failed_items": 0, "results": items, "created_at": fake.date_time().isoformat(), - "completed_at": fake.date_time().isoformat() + "completed_at": fake.date_time().isoformat(), } def create_test_client(service_key: str = "test-service-key", access_token: str = None): """Create a test NexlaClient instance with mocked HTTP client.""" - from nexla_sdk import NexlaClient from unittest.mock import patch - + + from nexla_sdk import NexlaClient + # Create a mock HTTP client mock_http_client = MockHTTPClient() - + # Mock the auth token response - mock_http_client.add_response("/token", { - "access_token": "test-token", - "token_type": "Bearer", - "expires_in": 86400 - }) - + mock_http_client.add_response( + "/token", + {"access_token": "test-token", "token_type": "Bearer", "expires_in": 86400}, + ) + # Patch the HTTP client during client creation - with patch('nexla_sdk.client.RequestsHttpClient', return_value=mock_http_client): - with patch('nexla_sdk.auth.RequestsHttpClient', return_value=mock_http_client): + with patch("nexla_sdk.client.RequestsHttpClient", return_value=mock_http_client): + with patch("nexla_sdk.auth.RequestsHttpClient", return_value=mock_http_client): # Create client with either service key or access token if access_token: client = NexlaClient(access_token=access_token) else: client = NexlaClient(service_key=service_key) - + # Replace the HTTP client to ensure it's the mock one client.http_client = mock_http_client client.auth_handler.http_client = mock_http_client - + return client def get_test_credentials() -> Optional[Dict[str, Any]]: """Get test credentials from environment variables.""" import os - + service_key = os.getenv("NEXLA_SERVICE_KEY") access_token = os.getenv("NEXLA_ACCESS_TOKEN") api_url = os.getenv("NEXLA_API_URL", "https://api.nexla.io") - + if service_key: return {"service_key": service_key, "base_url": api_url} elif access_token: return {"access_token": access_token, "base_url": api_url} - - return None \ No newline at end of file + + return None diff --git a/tests/utils/mock_builders.py b/tests/utils/mock_builders.py index 4ec4edd..3c52211 100644 --- a/tests/utils/mock_builders.py +++ b/tests/utils/mock_builders.py @@ -1,20 +1,21 @@ """Mock response builders for creating realistic test data.""" -from datetime import timezone -from typing import Dict, Any, Optional, List -from faker import Faker - # Set a seed for deterministic test data generation # Can be overridden by environment variable for debugging import os +from datetime import timezone +from typing import Any, Dict, List, Optional + +from faker import Faker -faker_seed = int(os.getenv('FAKER_SEED', '12345')) +faker_seed = int(os.getenv("FAKER_SEED", "12345")) fake = Faker() Faker.seed(faker_seed) + class MockResponseBuilder: """Builder for creating realistic mock API responses.""" - + @staticmethod def credential(credential_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock credential response matching the API documentation.""" @@ -22,37 +23,46 @@ def credential(credential_id: Optional[int] = None, **overrides) -> Dict[str, An "id": credential_id or fake.random_int(1, 10000), "name": f"{fake.company()} Credentials", "description": fake.text(max_nb_chars=100) if fake.boolean() else None, - "credentials_type": fake.random_element(["s3", "postgres", "mysql", "ftp", "gcs"]), + "credentials_type": fake.random_element( + ["s3", "postgres", "mysql", "ftp", "gcs"] + ), "credentials_version": "1", - "verified_status": fake.random_element(["VERIFIED", "UNVERIFIED", "FAILED"]), + "verified_status": fake.random_element( + ["VERIFIED", "UNVERIFIED", "FAILED"] + ), "owner": { "id": fake.random_int(1, 1000), "full_name": fake.name(), - "email": fake.email() - }, - "org": { - "id": fake.random_int(1, 100), - "name": fake.company() + "email": fake.email(), }, + "org": {"id": fake.random_int(1, 100), "name": fake.company()}, "access_roles": ["owner"], "managed": fake.boolean(), "tags": [fake.word() for _ in range(fake.random_int(0, 3))], "created_at": fake.date_time(tzinfo=timezone.utc).isoformat(), - "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat() + "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat(), } base.update(overrides) return base - + @staticmethod - def source(source_id: Optional[int] = None, include_credentials: bool = False, - include_datasets: bool = False, **overrides) -> Dict[str, Any]: + def source( + source_id: Optional[int] = None, + include_credentials: bool = False, + include_datasets: bool = False, + **overrides, + ) -> Dict[str, Any]: """Build a mock source response matching the API documentation.""" base = { "id": source_id or fake.random_int(1, 10000), "name": f"{fake.company()} Data Source", "description": fake.text(max_nb_chars=200) if fake.boolean() else None, - "status": fake.random_element(["ACTIVE", "PAUSED", "DRAFT", "DELETED", "ERROR", "INIT"]), - "source_type": fake.random_element(["s3", "postgres", "mysql", "api_push", "ftp", "gcs", "bigquery"]), + "status": fake.random_element( + ["ACTIVE", "PAUSED", "DRAFT", "DELETED", "ERROR", "INIT"] + ), + "source_type": fake.random_element( + ["s3", "postgres", "mysql", "api_push", "ftp", "gcs", "bigquery"] + ), "ingest_method": fake.random_element(["POLL", "API", "STREAMING"]), "source_format": fake.random_element(["JSON", "CSV", "XML", "PARQUET"]), "managed": fake.boolean(), @@ -60,34 +70,31 @@ def source(source_id: Optional[int] = None, include_credentials: bool = False, "owner": { "id": fake.random_int(1, 1000), "full_name": fake.name(), - "email": fake.email() - }, - "org": { - "id": fake.random_int(1, 100), - "name": fake.company() + "email": fake.email(), }, + "org": {"id": fake.random_int(1, 100), "name": fake.company()}, "access_roles": ["owner"], "data_sets": [], "data_credentials": None, "tags": [fake.word() for _ in range(fake.random_int(0, 3))], "run_ids": [], "created_at": fake.date_time(tzinfo=timezone.utc).isoformat(), - "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat() + "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat(), } - + if include_credentials: base["data_credentials"] = MockResponseBuilder.credential() base["data_credentials_id"] = base["data_credentials"]["id"] - + if include_datasets: base["data_sets"] = [ - MockResponseBuilder.dataset_brief() + MockResponseBuilder.dataset_brief() for _ in range(fake.random_int(1, 3)) ] - + base.update(overrides) return base - + @staticmethod def dataset_brief(dataset_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock dataset brief response.""" @@ -99,11 +106,11 @@ def dataset_brief(dataset_id: Optional[int] = None, **overrides) -> Dict[str, An "description": f"DataSet #{fake.random_int(1, 100)} detected from {fake.company()}", "version": fake.random_int(1, 10), "created_at": fake.date_time(tzinfo=timezone.utc).isoformat(), - "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat() + "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat(), } base.update(overrides) return base - + @staticmethod def destination(destination_data: Dict[str, Any] = None) -> Dict[str, Any]: """Build a destination response.""" @@ -112,7 +119,7 @@ def destination(destination_data: Dict[str, Any] = None) -> Dict[str, Any]: if destination_data: base_destination.update(destination_data) return base_destination - + @staticmethod def data_set_info(data_set_data: Dict[str, Any] = None) -> Dict[str, Any]: """Build a data set info response.""" @@ -121,7 +128,7 @@ def data_set_info(data_set_data: Dict[str, Any] = None) -> Dict[str, Any]: if data_set_data: base_data_set.update(data_set_data) return base_data_set - + @staticmethod def data_map_info(data_map_data: Dict[str, Any] = None) -> Dict[str, Any]: """Build a data map info response.""" @@ -130,7 +137,7 @@ def data_map_info(data_map_data: Dict[str, Any] = None) -> Dict[str, Any]: if data_map_data: base_data_map.update(data_map_data) return base_data_map - + @staticmethod def nexset(nexset_data: Dict[str, Any] = None) -> Dict[str, Any]: """Build a nexset response.""" @@ -139,7 +146,7 @@ def nexset(nexset_data: Dict[str, Any] = None) -> Dict[str, Any]: if nexset_data: base_nexset.update(nexset_data) return base_nexset - + @staticmethod def nexset_sample(sample_data: Dict[str, Any] = None) -> Dict[str, Any]: """Build a nexset sample response.""" @@ -148,7 +155,7 @@ def nexset_sample(sample_data: Dict[str, Any] = None) -> Dict[str, Any]: if sample_data: base_sample.update(sample_data) return base_sample - + @staticmethod def lookup(lookup_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock lookup response.""" @@ -160,12 +167,12 @@ def lookup(lookup_id: Optional[int] = None, **overrides) -> Dict[str, Any]: "owner": { "id": fake.random_int(1, 1000), "full_name": fake.name(), - "email": fake.email() + "email": fake.email(), }, "org": { "id": fake.random_int(1, 100), "name": fake.company(), - "email_domain": fake.domain_name() + "email_domain": fake.domain_name(), }, "access_roles": ["owner"], "public": fake.boolean(), @@ -175,27 +182,21 @@ def lookup(lookup_id: Optional[int] = None, **overrides) -> Dict[str, Any]: "use_versioning": fake.boolean(), "data_format": fake.random_element([None, "json", "csv"]), "data_sink_id": fake.random_int(1, 1000) if fake.boolean() else None, - "data_defaults": { - "key": "default_key", - "value": "default_value" - }, + "data_defaults": {"key": "default_key", "value": "default_value"}, "data_set_id": fake.random_int(1, 1000) if fake.boolean() else None, "map_entry_count": fake.random_int(0, 1000), "map_entry_schema": { "type": "object", - "properties": { - "key": {"type": "string"}, - "value": {"type": "string"} - }, + "properties": {"key": {"type": "string"}, "value": {"type": "string"}}, "$schema": "http://json-schema.org/draft-04/schema#", - "$schema-id": fake.random_int(1000000, 9999999) + "$schema-id": fake.random_int(1000000, 9999999), }, "tags": [fake.word() for _ in range(fake.random_int(0, 3))], "created_at": fake.past_datetime().isoformat() + "Z", - "updated_at": fake.past_datetime().isoformat() + "Z" + "updated_at": fake.past_datetime().isoformat() + "Z", } return {**base, **overrides} - + @staticmethod def lookup_entry(**overrides) -> Dict[str, Any]: """Build a mock lookup entry response.""" @@ -205,12 +206,9 @@ def lookup_entry(**overrides) -> Dict[str, Any]: } # Add some additional fields for complex entries if fake.boolean(): - base.update({ - "description": fake.sentence(), - "category": fake.word() - }) + base.update({"description": fake.sentence(), "category": fake.word()}) return {**base, **overrides} - + @staticmethod def user(user_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock user response.""" @@ -220,23 +218,30 @@ def user(user_id: Optional[int] = None, **overrides) -> Dict[str, Any]: "full_name": fake.name(), "super_user": fake.boolean(), "impersonated": False, - "default_org": { - "id": fake.random_int(1, 100), - "name": fake.company() - }, + "default_org": {"id": fake.random_int(1, 100), "name": fake.company()}, "user_tier": fake.random_element(["FREE", "TRIAL", "PAID", "FREE_FOREVER"]), - "status": fake.random_element(["ACTIVE", "DEACTIVATED", "SOURCE_COUNT_CAPPED"]), + "status": fake.random_element( + ["ACTIVE", "DEACTIVATED", "SOURCE_COUNT_CAPPED"] + ), "account_locked": fake.boolean(), "org_memberships": [], "api_key": f"", - "email_verified_at": fake.date_time(tzinfo=timezone.utc).isoformat() if fake.boolean() else None, - "tos_signed_at": fake.date_time(tzinfo=timezone.utc).isoformat() if fake.boolean() else None, + "email_verified_at": ( + fake.date_time(tzinfo=timezone.utc).isoformat() + if fake.boolean() + else None + ), + "tos_signed_at": ( + fake.date_time(tzinfo=timezone.utc).isoformat() + if fake.boolean() + else None + ), "created_at": fake.date_time(tzinfo=timezone.utc).isoformat(), - "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat() + "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat(), } base.update(overrides) return base - + @staticmethod def organization(org_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock organization response.""" @@ -261,9 +266,9 @@ def account_summary(org_id: int, **overrides) -> Dict[str, Any]: "data_sources": {"total": 10, "active": 8, "paused": 2}, "data_sets": { "derived": {"total": 5, "active": 5}, - "detected": {"total": 5, "active": 5} + "detected": {"total": 5, "active": 5}, }, - "data_sinks": {"total": 10, "active": 10} + "data_sinks": {"total": 10, "active": 10}, } base.update(overrides) return base @@ -275,7 +280,7 @@ def audit_log_entry(**overrides) -> Dict[str, Any]: base = factory.create_mock_audit_log_entry() base.update(overrides) return base - + @staticmethod def team(team_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock team response.""" @@ -286,33 +291,30 @@ def team(team_id: Optional[int] = None, **overrides) -> Dict[str, Any]: "owner": { "id": fake.random_int(1, 1000), "full_name": fake.name(), - "email": fake.email() - }, - "org": { - "id": fake.random_int(1, 100), - "name": fake.company() + "email": fake.email(), }, + "org": {"id": fake.random_int(1, 100), "name": fake.company()}, "member": fake.boolean(), "members": [], "access_roles": ["owner"], "tags": [fake.word() for _ in range(fake.random_int(0, 3))], "created_at": fake.date_time(tzinfo=timezone.utc).isoformat(), - "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat() + "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat(), } base.update(overrides) return base - + @staticmethod def team_member(user_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock team member response.""" base = { "id": user_id or fake.random_int(1, 10000), "email": fake.email(), - "admin": fake.boolean() + "admin": fake.boolean(), } base.update(overrides) return base - + @staticmethod def org_membership(org_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock org membership response.""" @@ -321,11 +323,11 @@ def org_membership(org_id: Optional[int] = None, **overrides) -> Dict[str, Any]: "name": fake.company(), "is_admin": fake.boolean(), "org_membership_status": fake.random_element(["ACTIVE", "DEACTIVATED"]), - "api_key": f"" + "api_key": f"", } base.update(overrides) return base - + @staticmethod def project(project_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock project response.""" @@ -336,63 +338,67 @@ def project(project_id: Optional[int] = None, **overrides) -> Dict[str, Any]: "owner": { "id": fake.random_int(1, 1000), "full_name": fake.name(), - "email": fake.email() - }, - "org": { - "id": fake.random_int(1, 100), - "name": fake.company() + "email": fake.email(), }, + "org": {"id": fake.random_int(1, 100), "name": fake.company()}, "data_flows": [], "flows": [], "access_roles": ["owner"], "tags": [fake.word() for _ in range(fake.random_int(0, 3))], "created_at": fake.date_time(tzinfo=timezone.utc).isoformat(), - "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat() + "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat(), } base.update(overrides) return base - + @staticmethod - def notification(notification_id: Optional[int] = None, **overrides) -> Dict[str, Any]: + def notification( + notification_id: Optional[int] = None, **overrides + ) -> Dict[str, Any]: """Build a mock notification response.""" base = { "id": notification_id or fake.random_int(1, 10000), "owner": { "id": fake.random_int(1, 1000), "full_name": fake.name(), - "email": fake.email() - }, - "org": { - "id": fake.random_int(1, 100), - "name": fake.company() + "email": fake.email(), }, + "org": {"id": fake.random_int(1, 100), "name": fake.company()}, "access_roles": ["owner"], - "level": fake.random_element(["DEBUG", "INFO", "WARN", "ERROR", "RECOVERED"]), + "level": fake.random_element( + ["DEBUG", "INFO", "WARN", "ERROR", "RECOVERED"] + ), "resource_id": fake.random_int(1, 10000), "resource_type": fake.random_element(["SOURCE", "SINK", "DATASET"]), "message_id": fake.random_int(1, 1000), "message": fake.text(max_nb_chars=200), - "read_at": fake.date_time(tzinfo=timezone.utc).isoformat() if fake.boolean() else None, + "read_at": ( + fake.date_time(tzinfo=timezone.utc).isoformat() + if fake.boolean() + else None + ), "created_at": fake.date_time(tzinfo=timezone.utc).isoformat(), - "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat() + "updated_at": fake.date_time(tzinfo=timezone.utc).isoformat(), } base.update(overrides) return base - + @staticmethod def flow_response(**overrides) -> Dict[str, Any]: """Build a mock flow response.""" base = { - "flows": [MockResponseBuilder.flow_node() for _ in range(fake.random_int(1, 3))], + "flows": [ + MockResponseBuilder.flow_node() for _ in range(fake.random_int(1, 3)) + ], "data_sources": [], "data_sets": [], "data_sinks": [], "data_credentials": [], - "metrics": [] + "metrics": [], } base.update(overrides) return base - + @staticmethod def flow_node(node_id: Optional[int] = None, **overrides) -> Dict[str, Any]: """Build a mock flow node.""" @@ -408,11 +414,11 @@ def flow_node(node_id: Optional[int] = None, **overrides) -> Dict[str, Any]: "flow_type": fake.random_element(["batch", "streaming"]), "name": f"{fake.word()} Flow Node" if fake.boolean() else None, "description": fake.text(max_nb_chars=100) if fake.boolean() else None, - "children": [] + "children": [], } base.update(overrides) return base - + @staticmethod def probe_response(**overrides) -> Dict[str, Any]: """Build a probe response.""" @@ -420,11 +426,11 @@ def probe_response(**overrides) -> Dict[str, Any]: "status": "success", "message": "Probe completed successfully", "connection_verified": True, - "timestamp": fake.date_time(tzinfo=timezone.utc).isoformat() + "timestamp": fake.date_time(tzinfo=timezone.utc).isoformat(), } base.update(overrides) return base - + @staticmethod def probe_tree_response(connection_type: str = "s3", **overrides) -> Dict[str, Any]: """Build a probe tree response.""" @@ -443,18 +449,20 @@ def probe_tree_response(connection_type: str = "s3", **overrides) -> Dict[str, A "name": "file1.csv", "type": "file", "path": "/folder1/file1.csv", - "size": 1024 + "size": 1024, } - ] + ], } ] - } + }, } base.update(overrides) return base - + @staticmethod - def probe_sample_response(connection_type: str = "s3", **overrides) -> Dict[str, Any]: + def probe_sample_response( + connection_type: str = "s3", **overrides + ) -> Dict[str, Any]: """Build a probe sample response.""" base = { "status": "ok", @@ -463,26 +471,28 @@ def probe_sample_response(connection_type: str = "s3", **overrides) -> Dict[str, "output": { "sample_data": [ {"id": 1, "name": "Sample Row 1", "value": 100}, - {"id": 2, "name": "Sample Row 2", "value": 200} + {"id": 2, "name": "Sample Row 2", "value": 200}, ], "schema": { "fields": [ {"name": "id", "type": "integer"}, {"name": "name", "type": "string"}, - {"name": "value", "type": "integer"} + {"name": "value", "type": "integer"}, ] - } - } + }, + }, } base.update(overrides) return base @staticmethod - def webhook_send_response(dataset_id: Optional[int] = None, processed: int = 1, **overrides) -> Dict[str, Any]: + def webhook_send_response( + dataset_id: Optional[int] = None, processed: int = 1, **overrides + ) -> Dict[str, Any]: """Build a mock webhook send response.""" base = { "dataset_id": dataset_id or fake.random_int(1, 10000), - "processed": processed + "processed": processed, } base.update(overrides) return base @@ -495,9 +505,11 @@ def flow_log_entry(**overrides) -> Dict[str, Any]: "level": fake.random_element(["DEBUG", "INFO", "WARN", "ERROR"]), "message": fake.sentence(), "resource_id": fake.random_int(1, 10000), - "resource_type": fake.random_element(["data_sources", "data_sets", "data_sinks"]), + "resource_type": fake.random_element( + ["data_sources", "data_sets", "data_sinks"] + ), "run_id": fake.random_int(1, 10000), - "details": {"records": fake.random_int(0, 1000)} + "details": {"records": fake.random_int(0, 1000)}, } base.update(overrides) return base @@ -509,11 +521,7 @@ def flow_logs_response(log_count: int = 3, **overrides) -> Dict[str, Any]: "status": 200, "message": "Ok", "logs": [MockResponseBuilder.flow_log_entry() for _ in range(log_count)], - "meta": { - "currentPage": 1, - "pageCount": 1, - "totalCount": log_count - } + "meta": {"currentPage": 1, "pageCount": 1, "totalCount": log_count}, } base.update(overrides) return base @@ -531,15 +539,11 @@ def flow_metrics_api_response(**overrides) -> Dict[str, Any]: "records": fake.random_int(0, 10000), "size": fake.random_int(0, 100000), "errors": fake.random_int(0, 100), - "runId": fake.random_int(1, 10000) + "runId": fake.random_int(1, 10000), } }, - "meta": { - "currentPage": 1, - "pageCount": 1, - "totalCount": 1 - } - } + "meta": {"currentPage": 1, "pageCount": 1, "totalCount": 1}, + }, } base.update(overrides) return base @@ -547,29 +551,29 @@ def flow_metrics_api_response(**overrides) -> Dict[str, Any]: @staticmethod def docs_recommendation_response(**overrides) -> Dict[str, Any]: """Build a mock docs recommendation response.""" - base = { - "recommendation": fake.paragraph(), - "status": "success" - } + base = {"recommendation": fake.paragraph(), "status": "success"} base.update(overrides) return base class MockDataFactory: """Factory for generating mock data for testing.""" - + def __init__(self): self.fake = Faker() - + def create_mock_owner(self, **kwargs) -> Dict[str, Any]: """Create mock owner data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), "full_name": kwargs.get("full_name", self.fake.name()), "email": kwargs.get("email", self.fake.email()), - "email_verified_at": kwargs.get("email_verified_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "email_verified_at": kwargs.get( + "email_verified_at", + self.fake.date_time(tzinfo=timezone.utc).isoformat(), + ), } - + def create_mock_organization(self, **kwargs) -> Dict[str, Any]: """Create mock organization data.""" base_data = { @@ -579,11 +583,11 @@ def create_mock_organization(self, **kwargs) -> Dict[str, Any]: "access_roles": ["owner"], "account_tier": self.create_mock_org_tier(), "created_at": self.fake.date_time(tzinfo=timezone.utc).isoformat(), - "updated_at": self.fake.date_time(tzinfo=timezone.utc).isoformat() + "updated_at": self.fake.date_time(tzinfo=timezone.utc).isoformat(), } base_data.update(kwargs) - if 'id' not in base_data: - base_data['id'] = self.fake.random_int(min=1, max=1000) + if "id" not in base_data: + base_data["id"] = self.fake.random_int(min=1, max=1000) return base_data def create_mock_org_tier(self, **kwargs) -> Dict[str, Any]: @@ -594,9 +598,9 @@ def create_mock_org_tier(self, **kwargs) -> Dict[str, Any]: "display_name": kwargs.get("display_name", "Free"), "record_count_limit": kwargs.get("record_count_limit", 1000000), "record_count_limit_time": kwargs.get("record_count_limit_time", "DAILY"), - "data_source_count_limit": kwargs.get("data_source_count_limit", 3) + "data_source_count_limit": kwargs.get("data_source_count_limit", 3), } - + def create_mock_audit_log_entry(self, **kwargs) -> Dict[str, Any]: """Create a mock audit log entry.""" return { @@ -614,25 +618,29 @@ def create_mock_audit_log_entry(self, **kwargs) -> Dict[str, Any]: "request_user_agent": self.fake.user_agent(), "request_url": self.fake.uri(), "user": {"id": self.fake.random_int(1, 10000), "email": self.fake.email()}, - **kwargs + **kwargs, } - + def create_mock_connector(self, **kwargs) -> Dict[str, Any]: """Create mock connector data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=1000)), - "type": kwargs.get("type", self.fake.random_element(["s3", "postgres", "snowflake"])), + "type": kwargs.get( + "type", self.fake.random_element(["s3", "postgres", "snowflake"]) + ), "connection_type": kwargs.get("connection_type", "database"), "name": kwargs.get("name", self.fake.word().title() + " Connector"), "description": kwargs.get("description", self.fake.sentence()), - "nexset_api_compatible": kwargs.get("nexset_api_compatible", True) + "nexset_api_compatible": kwargs.get("nexset_api_compatible", True), } - + def create_mock_credential(self, **kwargs) -> Dict[str, Any]: """Create mock credential data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), - "name": kwargs.get("name", f"Test Credential {self.fake.random_int(min=1, max=100)}"), + "name": kwargs.get( + "name", f"Test Credential {self.fake.random_int(min=1, max=100)}" + ), "credentials_type": kwargs.get("credentials_type", "postgres"), "owner": kwargs.get("owner", self.create_mock_owner()), "org": kwargs.get("org", self.create_mock_organization()), @@ -640,18 +648,26 @@ def create_mock_credential(self, **kwargs) -> Dict[str, Any]: "verified_status": kwargs.get("verified_status", "VERIFIED"), "connector": kwargs.get("connector", self.create_mock_connector()), "description": kwargs.get("description", self.fake.sentence()), - "verified_at": kwargs.get("verified_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), + "verified_at": kwargs.get( + "verified_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), "tags": kwargs.get("tags", []), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "managed": kwargs.get("managed", False) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "managed": kwargs.get("managed", False), } - + def create_mock_source(self, **kwargs) -> Dict[str, Any]: """Create mock source data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), - "name": kwargs.get("name", f"Test Source {self.fake.random_int(min=1, max=100)}"), + "name": kwargs.get( + "name", f"Test Source {self.fake.random_int(min=1, max=100)}" + ), "status": kwargs.get("status", "ACTIVE"), "source_type": kwargs.get("source_type", "postgres"), "connector_type": kwargs.get("connector_type", "postgres"), @@ -667,7 +683,9 @@ def create_mock_source(self, **kwargs) -> Dict[str, Any]: "source_config": kwargs.get("source_config", {"table": "test_table"}), "poll_schedule": kwargs.get("poll_schedule"), "code_container_id": kwargs.get("code_container_id"), - "data_credentials_id": kwargs.get("data_credentials_id", self.fake.random_int(min=1, max=1000)), + "data_credentials_id": kwargs.get( + "data_credentials_id", self.fake.random_int(min=1, max=1000) + ), "data_credentials": kwargs.get("data_credentials"), "data_sets": kwargs.get("data_sets", []), "api_keys": kwargs.get("api_keys", []), @@ -678,15 +696,21 @@ def create_mock_source(self, **kwargs) -> Dict[str, Any]: "vendor_endpoint": kwargs.get("vendor_endpoint"), "vendor": kwargs.get("vendor"), "tags": kwargs.get("tags", []), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_destination(self, **kwargs) -> Dict[str, Any]: """Create mock destination data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), - "name": kwargs.get("name", f"Test Destination {self.fake.random_int(min=1, max=100)}"), + "name": kwargs.get( + "name", f"Test Destination {self.fake.random_int(min=1, max=100)}" + ), "status": kwargs.get("status", "ACTIVE"), "sink_type": kwargs.get("sink_type", "postgres"), "connector_type": kwargs.get("connector_type", "postgres"), @@ -696,7 +720,9 @@ def create_mock_destination(self, **kwargs) -> Dict[str, Any]: "managed": kwargs.get("managed", False), "connector": kwargs.get("connector", self.create_mock_connector()), "description": kwargs.get("description", self.fake.sentence()), - "data_set_id": kwargs.get("data_set_id", self.fake.random_int(min=1, max=1000)), + "data_set_id": kwargs.get( + "data_set_id", self.fake.random_int(min=1, max=1000) + ), "data_map_id": kwargs.get("data_map_id"), "data_source_id": kwargs.get("data_source_id"), "sink_format": kwargs.get("sink_format", "json"), @@ -705,7 +731,9 @@ def create_mock_destination(self, **kwargs) -> Dict[str, Any]: "in_memory": kwargs.get("in_memory", False), "data_set": kwargs.get("data_set"), "data_map": kwargs.get("data_map"), - "data_credentials_id": kwargs.get("data_credentials_id", self.fake.random_int(min=1, max=1000)), + "data_credentials_id": kwargs.get( + "data_credentials_id", self.fake.random_int(min=1, max=1000) + ), "data_credentials": kwargs.get("data_credentials"), "copied_from_id": kwargs.get("copied_from_id"), "flow_type": kwargs.get("flow_type", "batch"), @@ -713,48 +741,70 @@ def create_mock_destination(self, **kwargs) -> Dict[str, Any]: "vendor_endpoint": kwargs.get("vendor_endpoint"), "vendor": kwargs.get("vendor"), "tags": kwargs.get("tags", []), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_data_set_info(self, **kwargs) -> Dict[str, Any]: """Create mock data set info data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), - "name": kwargs.get("name", f"Test Dataset {self.fake.random_int(min=1, max=100)}"), + "name": kwargs.get( + "name", f"Test Dataset {self.fake.random_int(min=1, max=100)}" + ), "description": kwargs.get("description", self.fake.sentence()), "status": kwargs.get("status", "ACTIVE"), - "output_schema": kwargs.get("output_schema", {"type": "object", "properties": {}}), + "output_schema": kwargs.get( + "output_schema", {"type": "object", "properties": {}} + ), "version": kwargs.get("version", 1), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_data_map_info(self, **kwargs) -> Dict[str, Any]: """Create mock data map info data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), "owner_id": kwargs.get("owner_id", self.fake.random_int(min=1, max=1000)), "org_id": kwargs.get("org_id", self.fake.random_int(min=1, max=100)), - "name": kwargs.get("name", f"Test Data Map {self.fake.random_int(min=1, max=100)}"), + "name": kwargs.get( + "name", f"Test Data Map {self.fake.random_int(min=1, max=100)}" + ), "description": kwargs.get("description", self.fake.sentence()), "public": kwargs.get("public", False), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_nexset(self, **kwargs) -> Dict[str, Any]: """Create mock nexset data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), - "name": kwargs.get("name", f"Test Nexset {self.fake.random_int(min=1, max=100)}"), + "name": kwargs.get( + "name", f"Test Nexset {self.fake.random_int(min=1, max=100)}" + ), "description": kwargs.get("description", self.fake.sentence()), "status": kwargs.get("status", "ACTIVE"), "owner": kwargs.get("owner", self.create_mock_owner()), "org": kwargs.get("org", self.create_mock_organization()), "access_roles": kwargs.get("access_roles", ["owner"]), "flow_type": kwargs.get("flow_type", "batch"), - "data_source_id": kwargs.get("data_source_id", self.fake.random_int(min=1, max=1000)), + "data_source_id": kwargs.get( + "data_source_id", self.fake.random_int(min=1, max=1000) + ), "data_source": kwargs.get("data_source"), "parent_data_sets": kwargs.get("parent_data_sets", []), "data_sinks": kwargs.get("data_sinks", []), @@ -762,29 +812,41 @@ def create_mock_nexset(self, **kwargs) -> Dict[str, Any]: "output_schema": kwargs.get("output_schema", {"type": "object"}), "copied_from_id": kwargs.get("copied_from_id"), "tags": kwargs.get("tags", []), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_nexset_sample(self, **kwargs) -> Dict[str, Any]: """Create mock nexset sample data.""" return { - "raw_message": kwargs.get("raw_message", { - "id": self.fake.random_int(min=1, max=1000), - "name": self.fake.name(), - "value": self.fake.random_number(digits=3) - }), - "nexla_metadata": kwargs.get("nexla_metadata", { - "timestamp": self.fake.date_time(tzinfo=timezone.utc).isoformat(), - "source": "test" - }) + "raw_message": kwargs.get( + "raw_message", + { + "id": self.fake.random_int(min=1, max=1000), + "name": self.fake.name(), + "value": self.fake.random_number(digits=3), + }, + ), + "nexla_metadata": kwargs.get( + "nexla_metadata", + { + "timestamp": self.fake.date_time(tzinfo=timezone.utc).isoformat(), + "source": "test", + }, + ), } - + def create_mock_lookup(self, **kwargs) -> Dict[str, Any]: """Create mock lookup data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), - "name": kwargs.get("name", f"test_lookup_{self.fake.random_int(min=1, max=100)}"), + "name": kwargs.get( + "name", f"test_lookup_{self.fake.random_int(min=1, max=100)}" + ), "description": kwargs.get("description", self.fake.sentence()), "map_primary_key": kwargs.get("map_primary_key", "id"), "owner": kwargs.get("owner", self.create_mock_owner()), @@ -802,17 +864,21 @@ def create_mock_lookup(self, **kwargs) -> Dict[str, Any]: "map_entry_count": kwargs.get("map_entry_count", 0), "map_entry_schema": kwargs.get("map_entry_schema", {"type": "object"}), "tags": kwargs.get("tags", []), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_lookup_entry(self, **kwargs) -> Dict[str, Any]: """Create mock lookup entry data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=1000)), "name": kwargs.get("name", self.fake.name()), "value": kwargs.get("value", self.fake.word()), - "metadata": kwargs.get("metadata", {"source": "test"}) + "metadata": kwargs.get("metadata", {"source": "test"}), } def create_mock_org_member(self, **kwargs) -> Dict[str, Any]: @@ -824,7 +890,7 @@ def create_mock_org_member(self, **kwargs) -> Dict[str, Any]: "is_admin?": kwargs.get("is_admin", self.fake.boolean()), "access_role": kwargs.get("access_role", ["member"]), "org_membership_status": kwargs.get("org_membership_status", "ACTIVE"), - "user_status": kwargs.get("user_status", "ACTIVE") + "user_status": kwargs.get("user_status", "ACTIVE"), } def create_mock_project(self, **kwargs) -> Dict[str, Any]: @@ -833,34 +899,52 @@ def create_mock_project(self, **kwargs) -> Dict[str, Any]: "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), "owner": kwargs.get("owner", self.create_mock_owner()), "org": kwargs.get("org", self.create_mock_organization()), - "name": kwargs.get("name", f"Test Project {self.fake.random_int(min=1, max=100)}"), + "name": kwargs.get( + "name", f"Test Project {self.fake.random_int(min=1, max=100)}" + ), "description": kwargs.get("description", self.fake.sentence()), "client_identifier": kwargs.get("client_identifier"), "client_url": kwargs.get("client_url"), - "flows_count": kwargs.get("flows_count", self.fake.random_int(min=0, max=10)), + "flows_count": kwargs.get( + "flows_count", self.fake.random_int(min=0, max=10) + ), "data_flows": kwargs.get("data_flows", []), "flows": kwargs.get("flows", []), "access_roles": kwargs.get("access_roles", ["owner"]), "tags": kwargs.get("tags", []), "copied_from_id": kwargs.get("copied_from_id"), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_project_data_flow(self, **kwargs) -> Dict[str, Any]: """Create mock project data flow data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), - "project_id": kwargs.get("project_id", self.fake.random_int(min=1, max=1000)), - "data_source_id": kwargs.get("data_source_id", self.fake.random_int(min=1, max=1000)), + "project_id": kwargs.get( + "project_id", self.fake.random_int(min=1, max=1000) + ), + "data_source_id": kwargs.get( + "data_source_id", self.fake.random_int(min=1, max=1000) + ), "data_set_id": kwargs.get("data_set_id"), "data_sink_id": kwargs.get("data_sink_id"), - "name": kwargs.get("name", f"Test Flow {self.fake.random_int(min=1, max=100)}"), + "name": kwargs.get( + "name", f"Test Flow {self.fake.random_int(min=1, max=100)}" + ), "description": kwargs.get("description", self.fake.sentence()), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_user(self, **kwargs) -> Dict[str, Any]: """Create mock user data.""" return { @@ -869,21 +953,52 @@ def create_mock_user(self, **kwargs) -> Dict[str, Any]: "full_name": kwargs.get("full_name", self.fake.name()), "super_user": kwargs.get("super_user", self.fake.boolean()), "impersonated": kwargs.get("impersonated", False), - "default_org": kwargs.get("default_org", { - "id": self.fake.random_int(min=1, max=100), - "name": self.fake.company() - }), - "user_tier": kwargs.get("user_tier", self.fake.random_element(["FREE", "TRIAL", "PAID", "FREE_FOREVER"])), - "status": kwargs.get("status", self.fake.random_element(["ACTIVE", "DEACTIVATED", "SOURCE_COUNT_CAPPED"])), + "default_org": kwargs.get( + "default_org", + { + "id": self.fake.random_int(min=1, max=100), + "name": self.fake.company(), + }, + ), + "user_tier": kwargs.get( + "user_tier", + self.fake.random_element(["FREE", "TRIAL", "PAID", "FREE_FOREVER"]), + ), + "status": kwargs.get( + "status", + self.fake.random_element( + ["ACTIVE", "DEACTIVATED", "SOURCE_COUNT_CAPPED"] + ), + ), "account_locked": kwargs.get("account_locked", self.fake.boolean()), "org_memberships": kwargs.get("org_memberships", []), - "api_key": kwargs.get("api_key", f""), - "email_verified_at": kwargs.get("email_verified_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() if self.fake.boolean() else None), - "tos_signed_at": kwargs.get("tos_signed_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() if self.fake.boolean() else None), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "api_key": kwargs.get( + "api_key", f"" + ), + "email_verified_at": kwargs.get( + "email_verified_at", + ( + self.fake.date_time(tzinfo=timezone.utc).isoformat() + if self.fake.boolean() + else None + ), + ), + "tos_signed_at": kwargs.get( + "tos_signed_at", + ( + self.fake.date_time(tzinfo=timezone.utc).isoformat() + if self.fake.boolean() + else None + ), + ), + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_team(self, **kwargs) -> Dict[str, Any]: """Create mock team data.""" return { @@ -896,57 +1011,76 @@ def create_mock_team(self, **kwargs) -> Dict[str, Any]: "members": kwargs.get("members", []), "access_roles": kwargs.get("access_roles", ["owner"]), "tags": kwargs.get("tags", []), - "created_at": kwargs.get("created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "updated_at": kwargs.get("updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat()) + "created_at": kwargs.get( + "created_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "updated_at": kwargs.get( + "updated_at", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), } - + def create_mock_team_member(self, **kwargs) -> Dict[str, Any]: """Create mock team member data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=10000)), "email": kwargs.get("email", self.fake.email()), - "admin": kwargs.get("admin", self.fake.boolean()) + "admin": kwargs.get("admin", self.fake.boolean()), } - + def create_mock_org_membership(self, **kwargs) -> Dict[str, Any]: """Create mock org membership data.""" return { "id": kwargs.get("id", self.fake.random_int(min=1, max=100)), "name": kwargs.get("name", self.fake.company()), "is_admin": kwargs.get("is_admin", self.fake.boolean()), - "org_membership_status": kwargs.get("org_membership_status", self.fake.random_element(["ACTIVE", "DEACTIVATED"])), - "api_key": kwargs.get("api_key", f"") + "org_membership_status": kwargs.get( + "org_membership_status", + self.fake.random_element(["ACTIVE", "DEACTIVATED"]), + ), + "api_key": kwargs.get( + "api_key", f"" + ), } - + def create_mock_flow_response(self, **kwargs) -> Dict[str, Any]: """Create mock flow response data.""" include_elements = kwargs.get("include_elements", True) - + base = { "flows": [ { "id": self.fake.random_int(1, 10000), "origin_node_id": self.fake.random_int(1, 10000), - "parent_node_id": self.fake.random_int(1, 10000) if self.fake.boolean() else None, - "data_source_id": self.fake.random_int(1, 10000) if self.fake.boolean() else None, - "data_set_id": self.fake.random_int(1, 10000) if self.fake.boolean() else None, - "data_sink_id": self.fake.random_int(1, 10000) if self.fake.boolean() else None, + "parent_node_id": ( + self.fake.random_int(1, 10000) if self.fake.boolean() else None + ), + "data_source_id": ( + self.fake.random_int(1, 10000) if self.fake.boolean() else None + ), + "data_set_id": ( + self.fake.random_int(1, 10000) if self.fake.boolean() else None + ), + "data_sink_id": ( + self.fake.random_int(1, 10000) if self.fake.boolean() else None + ), "status": "ACTIVE", - "project_id": self.fake.random_int(1, 1000) if self.fake.boolean() else None, + "project_id": ( + self.fake.random_int(1, 1000) if self.fake.boolean() else None + ), "flow_type": "batch", "ingestion_mode": "POLL", "name": f"Flow {self.fake.random_int(1, 100)}", "description": "Mock flow for testing", - "children": [] + "children": [], } ] } - + if include_elements: base["data_sources"] = [self.create_mock_source()] base["data_sinks"] = [self.create_mock_destination()] base["nexsets"] = [self.create_mock_nexset()] - + # Remove include_elements from kwargs before updating flow_kwargs = {k: v for k, v in kwargs.items() if k != "include_elements"} base.update(flow_kwargs) @@ -955,23 +1089,41 @@ def create_mock_flow_response(self, **kwargs) -> Dict[str, Any]: def create_mock_flow_metrics(self, **kwargs) -> Dict[str, Any]: """Create mock flow metrics data.""" return { - "origin_node_id": kwargs.get("origin_node_id", self.fake.random_int(1, 10000)), + "origin_node_id": kwargs.get( + "origin_node_id", self.fake.random_int(1, 10000) + ), "records": kwargs.get("records", self.fake.random_int(0, 10000)), "size": kwargs.get("size", self.fake.random_int(0, 100000)), "errors": kwargs.get("errors", self.fake.random_int(0, 100)), - "reporting_date": kwargs.get("reporting_date", self.fake.date_time(tzinfo=timezone.utc).isoformat()), - "run_id": kwargs.get("run_id", self.fake.random_int(1, 10000)) + "reporting_date": kwargs.get( + "reporting_date", self.fake.date_time(tzinfo=timezone.utc).isoformat() + ), + "run_id": kwargs.get("run_id", self.fake.random_int(1, 10000)), } - def create_mock_flow_node(self, max_depth: int = 2, current_depth: int = 0, parent_node_id: int = None, **kwargs) -> Dict[str, Any]: + def create_mock_flow_node( + self, + max_depth: int = 2, + current_depth: int = 0, + parent_node_id: int = None, + **kwargs, + ) -> Dict[str, Any]: """Create mock flow node with optional nested children.""" node_id = kwargs.get("id", self.fake.random_int(1, 10000)) node = { "id": node_id, - "origin_node_id": kwargs.get("origin_node_id", self.fake.random_int(1, 10000)), + "origin_node_id": kwargs.get( + "origin_node_id", self.fake.random_int(1, 10000) + ), "parent_node_id": parent_node_id, - "data_source_id": kwargs.get("data_source_id", self.fake.random_int(1, 10000) if current_depth == 0 else None), - "data_set_id": kwargs.get("data_set_id", self.fake.random_int(1, 10000) if current_depth > 0 else None), + "data_source_id": kwargs.get( + "data_source_id", + self.fake.random_int(1, 10000) if current_depth == 0 else None, + ), + "data_set_id": kwargs.get( + "data_set_id", + self.fake.random_int(1, 10000) if current_depth > 0 else None, + ), "data_sink_id": kwargs.get("data_sink_id"), "status": kwargs.get("status", "ACTIVE"), "project_id": kwargs.get("project_id"), @@ -979,7 +1131,7 @@ def create_mock_flow_node(self, max_depth: int = 2, current_depth: int = 0, pare "ingestion_mode": kwargs.get("ingestion_mode", "POLL"), "name": kwargs.get("name", f"Flow Node {node_id}"), "description": kwargs.get("description", "Mock flow node"), - "children": [] + "children": [], } # Add children if not at max depth @@ -989,7 +1141,7 @@ def create_mock_flow_node(self, max_depth: int = 2, current_depth: int = 0, pare child = self.create_mock_flow_node( max_depth=max_depth, current_depth=current_depth + 1, - parent_node_id=node_id + parent_node_id=node_id, ) node["children"].append(child) @@ -1029,4 +1181,4 @@ def team_list(count: int = 3) -> List[Dict[str, Any]]: def project_list(count: int = 3) -> List[Dict[str, Any]]: """Generate a list of mock projects.""" - return [MockResponseBuilder.project() for _ in range(count)] + return [MockResponseBuilder.project() for _ in range(count)]