diff --git a/README.md b/README.md index 956b6f6c..075430ab 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This is the Python server SDK to help you use Vonage APIs in your Python applica - [Application API](#application-api) - [HTTP Client](#http-client) - [JWT Client](#jwt-client) +- [Identity Insights](#identity-insights) - [Messages API](#messages-api) - [Network Number Verification API](#network-number-verification-api) - [Network Sim Swap API](#network-sim-swap-api) @@ -403,6 +404,34 @@ from vonage_jwt import verify_signature verify_signature(TOKEN, SIGNATURE_SECRET) # Returns a boolean ``` +## Identity Insights + +### Get Insights + +```python +from vonage_identity_insights import ( + IdentityInsightsRequest, + InsightsRequest, + EmptyInsight, + SimSwapInsight, +) + +options = HttpClientOptions(api_host='api-eu.vonage.com', timeout=30) + +client = Vonage(auth=auth, http_client_options=options) + +request = IdentityInsightsRequest( + phone_number='1234567890', + purpose='FraudPreventionAndDetection', + insights=InsightsRequest( + format=EmptyInsight(), + sim_swap=SimSwapInsight(period=240) + ) +) + +response = client.identity_insights.get_insights(request) +``` + ## Messages API ### How to Construct a Message @@ -1436,6 +1465,7 @@ The following is a list of Vonage APIs and whether the Python SDK provides suppo | External Accounts API | Beta | ❌ | | Media API | Beta | ❌ | | Messages API | General Availability | ✅ | +| Identity Insights API | General Availability | ✅ | | Number Insight API | General Availability | ✅ | | Number Management API | General Availability | ✅ | | Pricing API | General Availability | ✅ | diff --git a/account/tests/test_account.py b/account/tests/test_account.py index 318d72ff..f3043d52 100644 --- a/account/tests/test_account.py +++ b/account/tests/test_account.py @@ -2,6 +2,7 @@ import responses from pytest import raises +from testutils import build_response, get_mock_api_key_auth from vonage_account.account import Account from vonage_account.errors import InvalidSecretError from vonage_account.requests import ( @@ -12,8 +13,6 @@ from vonage_http_client.errors import ForbiddenError from vonage_http_client.http_client import HttpClient -from testutils import build_response, get_mock_api_key_auth - path = abspath(__file__) account = Account(HttpClient(get_mock_api_key_auth())) diff --git a/application/tests/test_application.py b/application/tests/test_application.py index 6c14a4c6..675f61f8 100644 --- a/application/tests/test_application.py +++ b/application/tests/test_application.py @@ -2,6 +2,7 @@ import responses from pytest import raises +from testutils import build_response, get_mock_api_key_auth from vonage_application.application import Application from vonage_application.common import ( ApplicationUrl, @@ -24,8 +25,6 @@ from vonage_application.requests import ApplicationConfig, ListApplicationsFilter from vonage_http_client.http_client import HttpClient -from testutils import build_response, get_mock_api_key_auth - path = abspath(__file__) application = Application(HttpClient(get_mock_api_key_auth())) diff --git a/http_client/tests/test_http_client.py b/http_client/tests/test_http_client.py index 29d7842b..687a9a1f 100644 --- a/http_client/tests/test_http_client.py +++ b/http_client/tests/test_http_client.py @@ -8,6 +8,7 @@ from requests import PreparedRequest, Response, Session from requests.exceptions import ConnectionError from responses import matchers +from testutils import build_response, get_mock_jwt_auth from vonage_http_client.auth import Auth from vonage_http_client.errors import ( AuthenticationError, @@ -20,8 +21,6 @@ ) from vonage_http_client.http_client import HttpClient, HttpClientOptions -from testutils import build_response, get_mock_jwt_auth - path = abspath(__file__) diff --git a/identity_insights/BUILD b/identity_insights/BUILD new file mode 100644 index 00000000..2bd77f26 --- /dev/null +++ b/identity_insights/BUILD @@ -0,0 +1,16 @@ +resource(name='pyproject', source='pyproject.toml') +file(name='readme', source='README.md') + +files(sources=['tests/data/*']) + +python_distribution( + name='vonage-identity-insights', + dependencies=[ + ':pyproject', + ':readme', + 'identity_insights/src/vonage_identity_insights', + ], + provides=python_artifact(), + generate_setup=False, + repositories=['@pypi'], +) diff --git a/identity_insights/CHANGES.md b/identity_insights/CHANGES.md new file mode 100644 index 00000000..be516a55 --- /dev/null +++ b/identity_insights/CHANGES.md @@ -0,0 +1,2 @@ +# 1.0.0 +- Initial upload diff --git a/identity_insights/README.md b/identity_insights/README.md new file mode 100644 index 00000000..00db2d08 --- /dev/null +++ b/identity_insights/README.md @@ -0,0 +1,35 @@ +# Vonage Identity Insights Package + +This package contains the code to use the [Vonage Identity Insights API](https://developer.vonage.com/en/identity-insights/overview) in Python. The API provides real-time access to a broad range of attributes related to the carrier, subscriber, or device associated with a phone number. To use it you will need a Vonage account. Sign up [for free at vonage.com][signup]. + +## Usage + +It is recommended to use this as part of the main `vonage` package. The examples below assume you've created an instance of the `vonage.Vonage` class called `vonage_client`. + +### Make a Standard Identity Insights Request + +```python +from vonage import Vonage, Auth, HttpClientOptions +from vonage_identity_insights import ( + IdentityInsightsRequest, + InsightsRequest, + EmptyInsight, + SimSwapInsight, +) + +options = HttpClientOptions(api_host="api-eu.vonage.com", timeout=30) + +client = Vonage(auth=auth, http_client_options=options) + +request = IdentityInsightsRequest( + phone_number="1234567890", + purpose="FraudPreventionAndDetection", + insights=InsightsRequest( + format=EmptyInsight(), sim_swap=SimSwapInsight(period=240) + ), +) + +response = client.identity_insights.get_insights(request) + +``` + diff --git a/identity_insights/pyproject.toml b/identity_insights/pyproject.toml new file mode 100644 index 00000000..b4cd6a6b --- /dev/null +++ b/identity_insights/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = 'vonage-identity-insights' +dynamic = ["version"] +description = 'Vonage Identity Insights package' +readme = "README.md" +authors = [{ name = "Vonage", email = "devrel@vonage.com" }] +requires-python = ">=3.9" +dependencies = [ + "vonage-http-client>=1.5.0", + "vonage-utils>=1.1.4", + "pydantic>=2.9.2", +] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "License :: OSI Approved :: Apache Software License", +] + +[project.urls] +homepage = "https://github.com/Vonage/vonage-python-sdk" + +[tool.setuptools.dynamic] +version = { attr = "vonage_identity_insights._version.__version__" } + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/identity_insights/src/vonage_identity_insights/BUILD b/identity_insights/src/vonage_identity_insights/BUILD new file mode 100644 index 00000000..db46e8d6 --- /dev/null +++ b/identity_insights/src/vonage_identity_insights/BUILD @@ -0,0 +1 @@ +python_sources() diff --git a/identity_insights/src/vonage_identity_insights/__init__.py b/identity_insights/src/vonage_identity_insights/__init__.py new file mode 100644 index 00000000..80e76e7c --- /dev/null +++ b/identity_insights/src/vonage_identity_insights/__init__.py @@ -0,0 +1,30 @@ +from . import errors +from .identity_insights import IdentityInsights +from .requests import ( + EmptyInsight, + IdentityInsightsRequest, + InsightsRequest, + Location, + LocationCenter, + LocationVerificationInsight, + SimSwapInsight, + SubscriberMatchInsight, +) +from .responses import IdentityInsightsResponse, InsightStatus + +__all__ = [ + "IdentityInsights", + # Requests + "IdentityInsightsRequest", + "InsightsRequest", + "EmptyInsight", + "SimSwapInsight", + "SubscriberMatchInsight", + "LocationVerificationInsight", + "Location", + "LocationCenter", + # Responses + "IdentityInsightsResponse", + "InsightStatus", + "errors", +] diff --git a/identity_insights/src/vonage_identity_insights/_version.py b/identity_insights/src/vonage_identity_insights/_version.py new file mode 100644 index 00000000..5becc17c --- /dev/null +++ b/identity_insights/src/vonage_identity_insights/_version.py @@ -0,0 +1 @@ +__version__ = "1.0.0" diff --git a/identity_insights/src/vonage_identity_insights/errors.py b/identity_insights/src/vonage_identity_insights/errors.py new file mode 100644 index 00000000..3006b318 --- /dev/null +++ b/identity_insights/src/vonage_identity_insights/errors.py @@ -0,0 +1,9 @@ +from vonage_utils.errors import VonageError + + +class IdentityInsightsError(VonageError): + """Indicates an error when using the Vonage Identity Insights API.""" + + +class EmptyInsightsRequestException(VonageError): + """At least one insight must be provided.""" diff --git a/identity_insights/src/vonage_identity_insights/identity_insights.py b/identity_insights/src/vonage_identity_insights/identity_insights.py new file mode 100644 index 00000000..4bc0754b --- /dev/null +++ b/identity_insights/src/vonage_identity_insights/identity_insights.py @@ -0,0 +1,88 @@ +from logging import getLogger + +from pydantic import validate_call +from vonage_http_client.http_client import HttpClient + +from .errors import EmptyInsightsRequestException, IdentityInsightsError +from .requests import IdentityInsightsRequest +from .responses import IdentityInsightsResponse + +logger = getLogger("vonage_identity_insights") + + +class IdentityInsights: + """Calls Vonage's Identity Insights API.""" + + def __init__(self, http_client: HttpClient) -> None: + """Initialize the IdentityInsights client. + + Args: + http_client (HttpClient): Configured HTTP client used to make + authenticated requests to the Vonage API. + """ + self._http_client = http_client + self._auth_type = "jwt" + + @property + def http_client(self) -> HttpClient: + """The HTTP client used to make requests to the Vonage Indentity Insights API. + + Returns: + HttpClient: The HTTP client used to make requests to the Identity Insights API. + """ + return self._http_client + + @validate_call + def get_insights( + self, insights_request: IdentityInsightsRequest + ) -> IdentityInsightsResponse: + """Retrieve identity insights for a phone number. + + Sends an aggregated request to the Identity Insights API and returns + the results for each requested insight. + + Args: + insights_request (IdentityInsightsRequest): The request object + containing the phone number and the set of identity insights + to retrieve. + + Returns: + IdentityInsightsResponse: The response object containing the results + and status of each requested insight. + + Raises: + IdentityInsightsError: If the API returns an error response in + `application/problem+json` format. + """ + payload = insights_request.model_dump(exclude_none=True) + + insights = payload.get("insights") + if not insights or not isinstance(insights, dict): + raise EmptyInsightsRequestException() + + response = self._http_client.post( + self._http_client.api_host, + "/v0.1/identity-insights", + payload, + auth_type=self._auth_type, + ) + self._check_for_error(response) + + return IdentityInsightsResponse(**response) + + def _check_for_error(self, response: dict) -> None: + """Check whether the API response represents an error. + + The Identity Insights API returns errors using the + `application/problem+json` format. If such an error is detected, this + method raises an IdentityInsightsError. + + Args: + response (dict): Raw response returned by the HTTP client. + + Raises: + IdentityInsightsError: If the response contains an error payload. + """ + if "title" in response and "detail" in response: + error_message = f"Error with the following details: {response}" + raise IdentityInsightsError(error_message) diff --git a/identity_insights/src/vonage_identity_insights/requests.py b/identity_insights/src/vonage_identity_insights/requests.py new file mode 100644 index 00000000..5642f1c4 --- /dev/null +++ b/identity_insights/src/vonage_identity_insights/requests.py @@ -0,0 +1,173 @@ +from datetime import date +from typing import Literal, Optional + +from pydantic import BaseModel, Field +from vonage_utils.types import PhoneNumber + + +class EmptyInsight(BaseModel): + """Model for an insight request without parameters. + + This model represents insights that must be included as an empty JSON object (`{}`) to + indicate that the insight is requested. + """ + + +class SimSwapInsight(BaseModel): + """Model for a SIM swap insight request. + + This insight checks whether a SIM swap has occurred within a specified + period of time. + + Args: + period (int, Optional): Period in hours to be checked for SIM swap. + Must be between 1 and 2400. Defaults to 240. + """ + + period: Optional[int] = Field( + default=240, + ge=1, + le=2400, + description="Period in hours to be checked for SIM swap", + ) + + +class SubscriberMatchInsight(BaseModel): + """Model for a subscriber match insight request. + + This insight compares customer-provided identity attributes with those + stored and verified by the mobile network operator. + + At least one attribute must be provided. + + Args: + id_document (str, Optional): Identifier from the customer's official + identity document. + given_name (str, Optional): First or given name of the customer. + family_name (str, Optional): Last name or family name of the customer. + street_name (str, Optional): Name of the street in the customer's address. + street_number (str, Optional): Street number of the customer's address. + postal_code (str, Optional): Postal or ZIP code of the customer's address. + locality (str, Optional): City or locality of the customer's address. + region (str, Optional): Region or prefecture of the customer's address. + country (str, Optional): Country code of the customer's address + (ISO 3166-1 alpha-2). + house_number_extension (str, Optional): Additional house identifier + (e.g. apartment or suite number). + birthdate (date, Optional): Birthdate of the customer in ISO 8601 format. + """ + + id_document: Optional[str] + given_name: Optional[str] + family_name: Optional[str] + street_name: Optional[str] + street_number: Optional[str] + postal_code: Optional[str] + locality: Optional[str] + region: Optional[str] + country: Optional[str] + house_number_extension: Optional[str] + birthdate: Optional[date] + + +class LocationCenter(BaseModel): + """Model for the center point of a geographic area. + + Args: + latitude (float): Latitude of the center point, in decimal degrees. + Must be between -90 and 90. + longitude (float): Longitude of the center point, in decimal degrees. + Must be between -180 and 180. + """ + + latitude: float = Field(..., ge=-90, le=90) + longitude: float = Field(..., ge=-180, le=180) + + +class Location(BaseModel): + """Model for a geographic area definition. + + Currently, only circular areas are supported. + + Args: + type (Literal["CIRCLE"]): Type of the geographic area. + radius (int): Radius of the area in meters. Must be between 2000 and + 200000. + center (LocationCenter): Center point of the area. + """ + + type: Literal["CIRCLE"] + radius: int = Field(..., ge=2000, le=200000) + center: LocationCenter + + +class LocationVerificationInsight(BaseModel): + """Model for a location verification insight request. + + This insight verifies whether the device associated with the phone number + is located within the specified geographic area. + + Args: + location (Location): Geographic area used for verification. + """ + + location: Location + + +class InsightsRequest(BaseModel): + """Model for a collection of identity insight requests. + + Each field represents an individual insight. Only the insights included + in this object will be processed and returned in the response. + + Args: + format (EmptyInsight, Optional): Request phone number format validation. + sim_swap (SimSwapInsight, Optional): Request SIM swap information. + original_carrier (EmptyInsight, Optional): Request original carrier + information. + current_carrier (EmptyInsight, Optional): Request current carrier + information. + subscriber_match (SubscriberMatchInsight, Optional): Request subscriber + identity matching. + roaming (EmptyInsight, Optional): Request roaming status information. + reachability (EmptyInsight, Optional): Request device reachability + information. + location_verification (LocationVerificationInsight, Optional): Request + location verification. + """ + + format: Optional[EmptyInsight] = None + sim_swap: Optional[SimSwapInsight] = None + original_carrier: Optional[EmptyInsight] = None + current_carrier: Optional[EmptyInsight] = None + subscriber_match: Optional[SubscriberMatchInsight] = None + roaming: Optional[EmptyInsight] = None + reachability: Optional[EmptyInsight] = None + location_verification: Optional[LocationVerificationInsight] = None + + def at_least_one_insight(cls, values): + """Validate that at least one insight is provided.""" + if not any(v is not None for v in values.values()): + raise ValueError("At least one insight must be provided") + return values + + +class IdentityInsightsRequest(BaseModel): + """Model for an Identity Insights API request. + + This model represents a single aggregated request for one or more identity + insights related to a phone number. + + Args: + phone_number (PhoneNumber): The phone number to retrieve identity + insights for. + purpose (str, Optional): Purpose of the request. Required for insights + that rely on the Network Registry. + insights (InsightsRequest): Collection of requested insights. + """ + + phone_number: PhoneNumber + purpose: Optional[str] = Field( + None, description="Purpose of the request (required for some insights)" + ) + insights: InsightsRequest diff --git a/identity_insights/src/vonage_identity_insights/responses.py b/identity_insights/src/vonage_identity_insights/responses.py new file mode 100644 index 00000000..b62095a3 --- /dev/null +++ b/identity_insights/src/vonage_identity_insights/responses.py @@ -0,0 +1,251 @@ +from datetime import datetime +from typing import List, Optional + +from pydantic import BaseModel + + +class InsightStatus(BaseModel): + """Model for the status of an individual insight response. + + Args: + code (str): Status code of the insight processing. + message (str, Optional): Human-readable description of the status. + """ + + code: str + message: Optional[str] + + +class FormatInsightResponse(BaseModel): + """Model for the response of the `format` insight. + + This insight validates the phone number format and provides information + derived from its numbering plan. + + Args: + country_code (str, Optional): Country code in ISO 3166-1 alpha-2 format. + country_name (str, Optional): Full country name. + country_prefix (str, Optional): Numeric country calling code. + offline_location (str, Optional): Location derived from the number prefix. + time_zones (List[str], Optional): Time zones associated with the number. + number_international (str, Optional): Phone number in E.164 format. + number_national (str, Optional): Phone number in national format. + is_format_valid (bool, Optional): Indicates whether the number format is valid. + status (InsightStatus): Processing status of the insight. + """ + + country_code: Optional[str] + country_name: Optional[str] + country_prefix: Optional[str] + offline_location: Optional[str] + time_zones: Optional[List[str]] + number_international: Optional[str] + number_national: Optional[str] + is_format_valid: Optional[bool] + status: InsightStatus + + +class SimSwapInsightResponse(BaseModel): + """Model for the response of the `sim_swap` insight. + + This insight indicates whether a SIM swap has occurred recently. + + Args: + latest_sim_swap_at (datetime, Optional): Timestamp of the most recent + SIM swap, in UTC. + is_swapped (bool, Optional): Indicates whether a SIM swap occurred + within the requested period. + status (InsightStatus): Processing status of the insight. + """ + + latest_sim_swap_at: Optional[datetime] = None + is_swapped: Optional[bool] = None + status: InsightStatus + + +class OriginalCarrierInsightResponse(BaseModel): + """Model for the response of the `original_carrier` insight. + + Provides information about the network to which the phone number was + originally assigned. + + Args: + name (str, Optional): Full name of the original carrier. + network_type (str, Optional): Type of the network (e.g. MOBILE, LANDLINE). + country_code (str, Optional): Country code in ISO 3166-1 alpha-2 format. + network_code (str, Optional): MCC + MNC network identifier. + status (InsightStatus): Processing status of the insight. + """ + + name: Optional[str] + network_type: Optional[str] + country_code: Optional[str] + network_code: Optional[str] + status: InsightStatus + + +class CurrentCarrierInsightResponse(BaseModel): + """Model for the response of the `current_carrier` insight. + + Provides information about the network the phone number is currently + assigned to. + + Args: + name (str, Optional): Full name of the current carrier. + network_type (str, Optional): Type of the network (e.g. MOBILE). + country_code (str, Optional): Country code in ISO 3166-1 alpha-2 format. + network_code (str, Optional): MCC + MNC network identifier. + status (InsightStatus): Processing status of the insight. + """ + + name: Optional[str] + network_type: Optional[str] + country_code: Optional[str] + network_code: Optional[str] + status: InsightStatus + + +class LocationVerificationInsightResponse(BaseModel): + """Model for the response of the `location_verification` insight. + + Indicates whether the device associated with the phone number is located + within the requested geographic area. + + Args: + is_verified (str, Optional): Verification result (TRUE, FALSE, PARTIAL, UNKNOWN). + latest_location_at (datetime, Optional): Timestamp of the latest + location update, in UTC. + match_rate (int, Optional): Percentage indicating the degree of overlap + between requested and detected locations. + status (InsightStatus): Processing status of the insight. + """ + + is_verified: Optional[str] + latest_location_at: Optional[datetime] + match_rate: Optional[int] + status: InsightStatus + + +class SubscriberMatchInsightResponse(BaseModel): + """Model for the response of the `subscriber_match` insight. + + Provides matching results between customer-provided identity attributes + and the operator's verified records. + + Args: + id_document_match (str, Optional): Match result for the ID document. + given_name_match (str, Optional): Match result for the given name. + family_name_match (str, Optional): Match result for the family name. + address_match (str, Optional): Match result for the full address. + street_name_match (str, Optional): Match result for the street name. + street_number_match (str, Optional): Match result for the street number. + postal_code_match (str, Optional): Match result for the postal code. + locality_match (str, Optional): Match result for the locality. + region_match (str, Optional): Match result for the region. + country_match (str, Optional): Match result for the country. + house_number_extension_match (str, Optional): Match result for the + house number extension. + birthdate_match (str, Optional): Match result for the birthdate. + status (InsightStatus): Processing status of the insight. + """ + + id_document_match: Optional[str] + given_name_match: Optional[str] + family_name_match: Optional[str] + address_match: Optional[str] + street_name_match: Optional[str] + street_number_match: Optional[str] + postal_code_match: Optional[str] + locality_match: Optional[str] + region_match: Optional[str] + country_match: Optional[str] + house_number_extension_match: Optional[str] + birthdate_match: Optional[str] + status: InsightStatus + + +class RoamingInsightResponse(BaseModel): + """Model for the response of the `roaming` insight. + + Indicates whether the device is currently roaming and the associated + roaming countries. + + Args: + latest_status_at (datetime, Optional): Timestamp of the latest roaming + status update, in UTC. + is_roaming (bool, Optional): Indicates whether the device is roaming. + country_codes (List[str], Optional): Country codes where the device + is roaming. + status (InsightStatus): Processing status of the insight. + """ + + latest_status_at: Optional[datetime] + is_roaming: Optional[bool] + country_codes: Optional[List[str]] + status: InsightStatus + + +class ReachabilityInsightResponse(BaseModel): + """Model for the response of the `reachability` insight. + + Indicates whether the device is reachable on the mobile network. + + Args: + latest_status_at (datetime, Optional): Timestamp of the latest + reachability update, in UTC. + is_reachable (bool, Optional): Indicates whether the device is reachable. + connectivity (List[str], Optional): Connectivity types available + (e.g. DATA, SMS). + status (InsightStatus): Processing status of the insight. + """ + + latest_status_at: Optional[datetime] + is_reachable: Optional[bool] + connectivity: Optional[List[str]] + status: InsightStatus + + +class InsightsResponse(BaseModel): + """Model for the collection of identity insight responses. + + Each field corresponds to an insight requested in the original request. + Only insights that were requested will be present in the response. + + Args: + format (FormatInsightResponse, Optional): Format validation response. + sim_swap (SimSwapInsightResponse, Optional): SIM swap response. + original_carrier (OriginalCarrierInsightResponse, Optional): Original + carrier response. + current_carrier (CurrentCarrierInsightResponse, Optional): Current + carrier response. + location_verification (LocationVerificationInsightResponse, Optional): + Location verification response. + subscriber_match (SubscriberMatchInsightResponse, Optional): Subscriber + match response. + roaming (RoamingInsightResponse, Optional): Roaming status response. + reachability (ReachabilityInsightResponse, Optional): Reachability response. + """ + + format: Optional[FormatInsightResponse] = None + sim_swap: Optional[SimSwapInsightResponse] = None + original_carrier: Optional[OriginalCarrierInsightResponse] = None + current_carrier: Optional[CurrentCarrierInsightResponse] = None + location_verification: Optional[LocationVerificationInsightResponse] = None + subscriber_match: Optional[SubscriberMatchInsightResponse] = None + roaming: Optional[RoamingInsightResponse] = None + reachability: Optional[ReachabilityInsightResponse] = None + + +class IdentityInsightsResponse(BaseModel): + """Model for an Identity Insights API response. + + Represents the aggregated response containing the results of all requested + identity insights. + + Args: + request_id (str, Optional): Unique identifier for the request. + insights (InsightsResponse): Collection of insight responses. + """ + + request_id: Optional[str] = None + insights: InsightsResponse diff --git a/identity_insights/tests/BUILD b/identity_insights/tests/BUILD new file mode 100644 index 00000000..40c70fd4 --- /dev/null +++ b/identity_insights/tests/BUILD @@ -0,0 +1 @@ +python_tests(dependencies=['identity_insights', 'testutils']) diff --git a/identity_insights/tests/data/format.json b/identity_insights/tests/data/format.json new file mode 100644 index 00000000..969a17e9 --- /dev/null +++ b/identity_insights/tests/data/format.json @@ -0,0 +1,29 @@ +{ + "request_id": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab", + "insights": { + "format": { + "country_code": "US", + "country_name": "United States", + "country_prefix": "1", + "offline_location": "Georgia", + "time_zones": [ + "America/New_York" + ], + "number_international": "+14040000000", + "number_national": "(404) 000-0000", + "is_format_valid": true, + "status": { + "code": "OK", + "message": "Success" + } + }, + "sim_swap": { + "latest_sim_swap_at": "2024-07-08T09:30:27.504Z", + "is_swapped": true, + "status": { + "code": "OK", + "message": "Success" + } + } + } +} diff --git a/identity_insights/tests/data/insight_error.json b/identity_insights/tests/data/insight_error.json new file mode 100644 index 00000000..1993541c --- /dev/null +++ b/identity_insights/tests/data/insight_error.json @@ -0,0 +1,7 @@ +{ + "title": "Malformed JSON", + "type": "https://developer.vonage.com/api-errors#invalid-json", + "instance": "868cf2e9-bd6d-4bac-96ba-2b08120d8cf9", + "detail": "Malformed JSON payload" +} + diff --git a/identity_insights/tests/test_identity_insights.py b/identity_insights/tests/test_identity_insights.py new file mode 100644 index 00000000..038c92cb --- /dev/null +++ b/identity_insights/tests/test_identity_insights.py @@ -0,0 +1,76 @@ +from os.path import abspath + +import responses +from pytest import raises +from vonage_http_client.http_client import HttpClient, HttpClientOptions +from vonage_identity_insights.errors import ( + EmptyInsightsRequestException, + IdentityInsightsError, +) +from vonage_identity_insights.identity_insights import IdentityInsights +from vonage_identity_insights.requests import ( + EmptyInsight, + IdentityInsightsRequest, + InsightsRequest, +) + +from testutils import build_response, get_mock_jwt_auth + +path = abspath(__file__) + + +options = HttpClientOptions(api_host="api-eu.vonage.com", timeout=30) +identity_insights = IdentityInsights(HttpClient(get_mock_jwt_auth(), options)) + + +def test_http_client_property(): + http_client = identity_insights.http_client + assert isinstance(http_client, HttpClient) + + +@responses.activate +def test_format_insight(): + build_response( + path, + 'POST', + 'https://api-eu.vonage.com/v0.1/identity-insights', + 'format.json', + ) + + options = IdentityInsightsRequest( + phone_number="1234567890", insights=InsightsRequest(format=EmptyInsight()) + ) + + response = identity_insights.get_insights(options) + + assert response.insights.format.status.code == "OK" + assert response.insights.format.status.message == "Success" + + +@responses.activate +def test_basic_insight_error(): + build_response( + path, + 'POST', + 'https://api-eu.vonage.com/v0.1/identity-insights', + 'insight_error.json', + ) + + options = IdentityInsightsRequest( + phone_number="1234567890", insights=InsightsRequest(format=EmptyInsight()) + ) + + with raises(IdentityInsightsError) as e: + identity_insights.get_insights(options) + + assert "Malformed JSON" in str(e.value) + + +@responses.activate +def test_empty_insights_request_raises_exception(): + options = IdentityInsightsRequest( + phone_number="1234567890", insights=InsightsRequest() + ) + + with raises(EmptyInsightsRequestException): + identity_insights.get_insights(options) diff --git a/requirements.txt b/requirements.txt index 8c149e7d..3cf935e8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,6 +11,7 @@ urllib3 -e http_client -e account -e application +-e identity_insights -e messages -e network_auth -e network_number_verification diff --git a/vonage/src/vonage/vonage.py b/vonage/src/vonage/vonage.py index 317613f5..cfd2b567 100644 --- a/vonage/src/vonage/vonage.py +++ b/vonage/src/vonage/vonage.py @@ -3,6 +3,7 @@ from vonage_account.account import Account from vonage_application.application import Application from vonage_http_client import Auth, HttpClient, HttpClientOptions +from vonage_identity_insights import IdentityInsights from vonage_messages import Messages from vonage_network_number_verification import NetworkNumberVerification from vonage_network_sim_swap import NetworkSimSwap @@ -51,6 +52,7 @@ def __init__( self.verify_legacy = VerifyLegacy(self._http_client) self.video = Video(self._http_client) self.voice = Voice(self._http_client) + self.identity_insights = IdentityInsights(self._http_client) @property def http_client(self):