From d255277bc579b3916abe9b77f2f5f4a1ad2a4ccb Mon Sep 17 00:00:00 2001 From: Amirul Islam Date: Fri, 6 Dec 2024 01:41:44 +0100 Subject: [PATCH 1/6] Updated documentation, add setup.py --- docs/webhook_guide.md | 9 +++++++ setup.py | 56 ++++++++++++++++++++++++++++++++++++++++++ src/schemas/webhook.py | 1 + 3 files changed, 66 insertions(+) create mode 100644 setup.py diff --git a/docs/webhook_guide.md b/docs/webhook_guide.md index 4ddcaf6..4e3d604 100644 --- a/docs/webhook_guide.md +++ b/docs/webhook_guide.md @@ -137,6 +137,15 @@ async def handle_webhook( ## Testing the Webhook +Test the webhook by sending a simulated payload to it: + +```bash +curl -X POST http://localhost:3010/webhooks \ + -H "Authorization: Bearer " \ + -d '{"id": "msg123", "status": "delivered", "deliveredAt": "2024-11-30T12:00:00Z"}' +``` + + ### Unit Tests Run unit tests to verify webhook functionality: diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..acfc525 --- /dev/null +++ b/setup.py @@ -0,0 +1,56 @@ +from setuptools import setup, find_packages + +setup( + name="messaging-sdk", + version="2.1.0", + description="A Python SDK for managing messages and contacts, with webhook integration.", + author="Amirul Islam", + author_email="amirulislamalmamun@gmail.com", + url="https://github.com/your-repo/messaging-sdk", + packages=find_packages(where="src"), + package_dir={"": "src"}, + install_requires=[ + "requests", + "python-dotenv", + "pytest", + "pytest-mock", + "flake8", + "black", + "mypy", + "pydantic", + "pytest-cov", + "fastapi", + "uvicorn", + "pytest-asyncio", + "httpx", + "pydantic-settings", + ], + extras_require={ + "dev": [ + "flake8", + "black", + "mypy", + "pytest", + "pytest-cov", + "pytest-asyncio", + "pytest-mock", + ], + }, + entry_points={ + "console_scripts": [ + # Example entry point (if applicable) + # "messaging-sdk-cli = messaging_sdk.cli:main", + ] + }, + classifiers=[ + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + python_requires=">=3.8", +) diff --git a/src/schemas/webhook.py b/src/schemas/webhook.py index 3dbdc35..3d141ae 100644 --- a/src/schemas/webhook.py +++ b/src/schemas/webhook.py @@ -6,6 +6,7 @@ class WebhookPayload(BaseModel): """ Schema for webhook payloads received from the API server. + Represents the MessageDeliveryEvent schema from the OpenAPI specification. Attributes: id (str): Unique identifier for the message. From d8d4f3974400d0a8130472ae36c3b90e67ffb17e Mon Sep 17 00:00:00 2001 From: Amirul Islam Date: Fri, 6 Dec 2024 05:02:41 +0100 Subject: [PATCH 2/6] Refactoring schemas, add setup.py, fix tests to align with schema --- docker-compose.yml | 1 - setup.py | 12 +-- src/core/config.py | 4 +- src/schemas/contacts.py | 2 + src/schemas/messages.py | 99 +++++++++++++++---- src/sdk/client.py | 5 +- src/sdk/features/contacts.py | 3 +- src/sdk/features/messages.py | 28 ++++-- tests/e2e/test_contacts_e2e.py | 2 +- tests/e2e/test_messages_e2e.py | 36 +++++-- .../sdk/test_end_to_end_workflows.py | 25 +++-- tests/unit/sdk/test_messages.py | 27 ++++- 12 files changed, 184 insertions(+), 60 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e6f7708..4f5c459 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,6 @@ services: ports: - "3000:3000" environment: - WEBHOOK_URL: ${WEBHOOK_URL} WEBHOOK_SECRET: ${WEBHOOK_SECRET} swagger-ui: diff --git a/setup.py b/setup.py index acfc525..4e2b5ec 100644 --- a/setup.py +++ b/setup.py @@ -6,9 +6,10 @@ description="A Python SDK for managing messages and contacts, with webhook integration.", author="Amirul Islam", author_email="amirulislamalmamun@gmail.com", - url="https://github.com/your-repo/messaging-sdk", - packages=find_packages(where="src"), - package_dir={"": "src"}, + url="https://github.com/shiningflash/messaging-sdk", + packages=find_packages(where="src"), # Discover packages under "src" + package_dir={"": "src"}, # Specify that all packages live under "src" + include_package_data=True, # Include non-Python files specified in MANIFEST.in install_requires=[ "requests", "python-dotenv", @@ -37,10 +38,7 @@ ], }, entry_points={ - "console_scripts": [ - # Example entry point (if applicable) - # "messaging-sdk-cli = messaging_sdk.cli:main", - ] + "console_scripts": [], }, classifiers=[ "Programming Language :: Python :: 3.8", diff --git a/src/core/config.py b/src/core/config.py index 2f854d0..3742934 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -1,3 +1,5 @@ +import os + from pydantic import Field, field_validator, ConfigDict from pydantic_settings import BaseSettings from src.core.logger import logger @@ -23,7 +25,7 @@ def validate_non_empty(cls, value, info): logger.info(f"Validated {field_name}: {value}") return value - model_config = ConfigDict(env_file=".env", env_file_encoding="utf-8") + model_config = ConfigDict(env_file=os.path.join(os.path.dirname(__file__), "../../.env"), env_file_encoding="utf-8") settings = Settings() diff --git a/src/schemas/contacts.py b/src/schemas/contacts.py index 4dcec5d..a00bc05 100644 --- a/src/schemas/contacts.py +++ b/src/schemas/contacts.py @@ -33,6 +33,8 @@ def validate_phone(cls, value): raise ValueError("Phone numbers must start with a '+' prefix.") if not value[1:].isdigit(): raise ValueError("Phone numbers must contain only digits after the '+' prefix.") + if len(value) < 10 or len(value) > 15: + raise ValueError("Phone numbers must be between 10 and 15 characters.") return value diff --git a/src/schemas/messages.py b/src/schemas/messages.py index 8cb8f19..163fef7 100644 --- a/src/schemas/messages.py +++ b/src/schemas/messages.py @@ -1,36 +1,65 @@ from pydantic import BaseModel, Field, field_validator, ConfigDict -from typing import List, Literal +from typing import List, Literal, Union, Optional from datetime import datetime +class MessageContact(BaseModel): + """ + Schema for a message contact, representing either a contact ID or full contact details. + + Attributes: + id (str): The unique identifier of the contact. + """ + id: str = Field( + ..., + description="The unique identifier of the contact.", + json_schema_extra={"example": "contact123"} + ) + + +class ContactDetails(BaseModel): + """ + Schema for detailed contact information in a message. + + Attributes: + name (str): The name of the contact. + phone (str): The phone number of the contact. + id (str): The unique identifier for the contact. + """ + name: Optional[str] = Field(None, description="The name of the contact.", json_schema_extra={"example": "John Doe"}) + phone: Optional[str] = Field(None, description="The phone number of the contact.", json_schema_extra={"example": "+123456789"}) + id: str = Field(..., description="The unique ID of the contact.", json_schema_extra={"example": "contact-id-123"}) + + class CreateMessageRequest(BaseModel): """ Schema for creating a new message. Attributes: - to (str): Recipient's phone number in E.164 format. + to (Union[str, MessageContact]): Contact ID or full contact details of the recipient. content (str): Message content, with a maximum length of 160 characters. - sender (str): Sender's phone number in E.164 format. + from_sender (str): Sender's phone number in E.164 format. """ - to: str = Field( - ..., - description="Recipient's phone number in E.164 format.", - json_schema_extra={"example": "+1234567890"} + to: MessageContact = Field( + ..., + description="Recipient's contact ID.", + json_schema_extra={"example": {"id": "contact123"}} ) content: str = Field( - ..., - min_length=1, - max_length=160, + ..., + min_length=1, + max_length=160, description="Message content, limited to 160 characters.", json_schema_extra={"example": "Hello, World!"} ) - sender: str = Field( - ..., + from_sender: str = Field( + ..., + alias="from", description="Sender's phone number in E.164 format.", json_schema_extra={"example": "+0987654321"} ) - @field_validator("to", "sender") + @field_validator("from_sender") def validate_phone_number(cls, value): if not value.startswith("+"): raise ValueError("Phone numbers must include the '+' prefix.") @@ -45,33 +74,65 @@ class Message(BaseModel): Attributes: id (str): Unique identifier for the message. - from_ (str): Sender's phone number. - to (str): Recipient's phone number. + from_sender (str): Sender's phone number. + to (Union[ContactDetails, str]): Recipient details or just contact ID. content (str): Message content. status (str): Message status, one of 'queued', 'delivered', or 'failed'. created_at (datetime): Timestamp when the message was created. + delivered_at (Optional[datetime]): Timestamp when the message was delivered. """ id: str = Field(..., description="Unique identifier for the message.", json_schema_extra={"example": "msg123"}) - from_: str = Field( + from_sender: str = Field( ..., alias="from", description="Sender's phone number.", json_schema_extra={"example": "+0987654321"} ) - to: str = Field(..., description="Recipient's phone number.", json_schema_extra={"example": "+1234567890"}) + to: Union[ContactDetails, str] = Field( + ..., + description="Recipient details or just contact ID." + ) content: str = Field(..., description="Message content.", json_schema_extra={"example": "Hello, World!"}) status: Literal["queued", "delivered", "failed"] = Field( ..., description="Message status.", - json_schema_extra={"example": "delivered"} + json_schema_extra={"example": "queued"} ) created_at: datetime = Field( ..., alias="createdAt", description="Timestamp when the message was created.", - json_schema_extra={"example": "2024-12-01T12:00:00Z"} + json_schema_extra={"example": "2024-12-06T03:01:37.416Z"} + ) + delivered_at: Optional[datetime] = Field( + None, + alias="deliveredAt", + description="Timestamp when the message was delivered.", + json_schema_extra={"example": "2024-12-06T03:01:37.416Z"} ) + @classmethod + def validate_to_field(cls, value): + """ + Custom validator for the 'to' field to normalize its structure. + """ + if isinstance(value, str): + # Treat the string as a contact ID and wrap it in a ContactDetails object + return ContactDetails(id=value) + elif isinstance(value, dict): + # Parse it as a ContactDetails object + return ContactDetails(**value) + raise ValueError("'to' must be a valid contact ID (str) or ContactDetails object.") + + @classmethod + def model_validate(cls, data): + """ + Custom validation for the entire model to preprocess 'to'. + """ + if "to" in data: + data["to"] = cls.validate_to_field(data["to"]) + return super().model_validate(data) + model_config = ConfigDict( populate_by_name=True, arbitrary_types_allowed=True diff --git a/src/sdk/client.py b/src/sdk/client.py index a09f5fc..1096ad4 100644 --- a/src/sdk/client.py +++ b/src/sdk/client.py @@ -35,7 +35,10 @@ def _handle_api_errors(self, response: requests.Response) -> None: ServerError: For other 500+ server errors. ApiError: Generic API error for unexpected status codes. """ - if response.status_code == 401: + if response.status_code == 204: # No Content + logger.info("No content returned. Operation successful.") + return + elif response.status_code == 401: logger.error(f"Unauthorized: {response.text}") raise UnauthorizedError("Unauthorized. Check your API key.") elif response.status_code == 404: diff --git a/src/sdk/features/contacts.py b/src/sdk/features/contacts.py index ac9867c..fb329e2 100644 --- a/src/sdk/features/contacts.py +++ b/src/sdk/features/contacts.py @@ -109,8 +109,7 @@ def delete_contact(self, contact_id: str) -> None: """ logger.info(f"Deleting contact with ID: {contact_id}") try: - response = self.client.request("DELETE", f"/contacts/{contact_id}") + self.client.request("DELETE", f"/contacts/{contact_id}") logger.info(f"Successfully deleted contact with ID: {contact_id}") - return response except HTTPStatusError as e: handle_404_error(e, contact_id, "Contact") diff --git a/src/sdk/features/messages.py b/src/sdk/features/messages.py index c38286a..6e766b9 100644 --- a/src/sdk/features/messages.py +++ b/src/sdk/features/messages.py @@ -33,12 +33,19 @@ def send_message(self, payload: Dict) -> Message: Send a new message to a contact. Args: - payload (dict): A dictionary containing 'to', 'content', and 'sender'. + payload (dict): A dictionary containing 'to', 'content', and 'from_sender'. Returns: Message: The details of the sent message. """ - logger.info(f"Sending message with payload: {payload}") + logger.info("Preparing to send a message.") + # Ensure the payload aligns with the API's expected format + if "from_sender" in payload: + payload["from"] = payload.pop("from_sender") + logger.debug(f"Transformed payload: {payload}") + + # Make the API call to send the message + logger.info("Sending message request to the API.") return self.client.request("POST", "/messages", json=payload) @validate_response(ListMessagesResponse) @@ -55,7 +62,7 @@ def list_messages(self, page: int = 1, limit: int = 10) -> ListMessagesResponse: ListMessagesResponse: A paginated list of sent messages. """ params = {"page": page, "limit": limit} - logger.info(f"Listing messages with params: {params}") + logger.info(f"Requesting a list of messages with params: {params}") return self.client.request("GET", "/messages", params=params) @validate_response(Message) @@ -70,13 +77,13 @@ def get_message(self, message_id: str) -> Message: Returns: Message: The retrieved message details. """ - logger.info(f"Fetching message with ID: {message_id}") + logger.info(f"Fetching message details for ID: {message_id}") try: return self.client.request("GET", f"/messages/{message_id}") except HTTPStatusError as e: + logger.error(f"Message with ID {message_id} not found.") handle_404_error(e, message_id, "Message") - - + def validate_webhook_signature(self, raw_body: bytes, signature: str, secret: str): """ Validate the webhook signature using the SDK. @@ -89,5 +96,10 @@ def validate_webhook_signature(self, raw_body: bytes, signature: str, secret: st Raises: ValueError: If the signature validation fails. """ - logger.info("Validating webhook signature through SDK.") - verify_signature(raw_body, signature, secret) + logger.info("Validating webhook signature via the SDK.") + try: + verify_signature(raw_body, signature, secret) + logger.info("Webhook signature successfully validated.") + except ValueError as e: + logger.error(f"Invalid webhook signature: {e}") + raise diff --git a/tests/e2e/test_contacts_e2e.py b/tests/e2e/test_contacts_e2e.py index eeb2f0a..8ed9a52 100644 --- a/tests/e2e/test_contacts_e2e.py +++ b/tests/e2e/test_contacts_e2e.py @@ -82,7 +82,7 @@ def test_delete_contact_and_handle_absence(mock_api_client, contacts): # Step 1: Delete the contact contact_id = "contact123" delete_response = contacts.delete_contact(contact_id=contact_id) - assert delete_response == {"success": True}, "Failed to delete contact." + assert delete_response == None # Mock the API response for attempting to retrieve the deleted contact mock_api_client.request.side_effect = RuntimeError("Contact not found") diff --git a/tests/e2e/test_messages_e2e.py b/tests/e2e/test_messages_e2e.py index 06781ad..2c813c5 100644 --- a/tests/e2e/test_messages_e2e.py +++ b/tests/e2e/test_messages_e2e.py @@ -9,7 +9,11 @@ def test_send_message_and_verify(mock_api_client, messages): # Step 1: Mock the API response for sending a message send_response = { "id": "msg123", - "to": "+123456789", + "to": { + "id": "contact123", + "name": "John Doe", + "phone": "+123456789" + }, "from": "+987654321", "content": "Hello, World!", "status": "delivered", @@ -18,14 +22,22 @@ def test_send_message_and_verify(mock_api_client, messages): mock_api_client.request.return_value = send_response # Step 2: Send the message - send_payload = {"to": "+123456789", "content": "Hello, World!", "sender": "+987654321"} + send_payload = { + "to": {"id": "contact123"}, # Match the expected schema + "content": "Hello, World!", + "from": "+987654321" # Use the correct field name + } sent_message = messages.send_message(payload=send_payload) assert sent_message == send_response, "Failed to send message." # Step 3: Mock the API response for retrieving the message retrieve_response = { "id": "msg123", - "to": "+123456789", + "to": { + "id": "contact123", + "name": "John Doe", + "phone": "+123456789" + }, "from": "+987654321", "content": "Hello, World!", "status": "delivered", @@ -90,7 +102,11 @@ def test_resend_failed_message(mock_api_client, messages): # Step 1: Mock the API response for retrieving a failed message retrieve_response = { "id": "msg123", - "to": "+123456789", + "to": { + "id": "contact123", + "name": "John Doe", + "phone": "+123456789" + }, "from": "+987654321", "content": "Hello, World!", "status": "failed", @@ -103,10 +119,18 @@ def test_resend_failed_message(mock_api_client, messages): assert failed_message == retrieve_response, "Failed to retrieve the failed message." # Step 3: Mock the API response for resending the message - resend_payload = {"to": "+123456789", "content": "Hello, World!", "sender": "+987654321"} + resend_payload = { + "to": {"id": "contact123"}, # Match the expected schema + "content": "Hello, World!", + "from": "+987654321" # Use the correct field + } resend_response = { "id": "msg123", - "to": "+123456789", + "to": { + "id": "contact123", + "name": "John Doe", + "phone": "+123456789" + }, "from": "+987654321", "content": "Hello, World!", "status": "delivered", diff --git a/tests/integration/sdk/test_end_to_end_workflows.py b/tests/integration/sdk/test_end_to_end_workflows.py index a37cb76..89a2231 100644 --- a/tests/integration/sdk/test_end_to_end_workflows.py +++ b/tests/integration/sdk/test_end_to_end_workflows.py @@ -6,18 +6,26 @@ def test_send_and_check_message_workflow(messages, mock_api_client): Test the end-to-end workflow of sending a message and verifying its status. """ # Step 1: Send a message - send_payload = {"to": "+123456789", "content": "Hello, World!", "sender": "+987654321"} # Corrected sender format + send_payload = { + "to": {"id": "contact123"}, # Align with ContactID schema + "content": "Hello, World!", + "from": "+987654321" # Use the correct field + } sent_message_response = { "id": "msg123", - "to": "+123456789", + "to": { + "id": "contact123", + "name": "John Doe", + "phone": "+123456789" + }, "from": "+987654321", "content": "Hello, World!", - "sender": "+987654321", "status": "delivered", "createdAt": "2024-12-01T00:00:00Z", } mock_api_client.request.return_value = sent_message_response + # Send the message sent_message = messages.send_message(payload=send_payload) assert sent_message == sent_message_response, "Message sending failed." @@ -88,13 +96,12 @@ def test_delete_contact_and_verify(contacts, mock_api_client): """ Test the workflow of deleting a contact and verifying the deletion. """ - # Step 1: Mock the response for deleting a contact + # Step 1: Mock the API response for deleting a contact contact_id = "contact123" - mock_api_client.request.return_value = {"success": True} + mock_api_client.request.return_value = None # Simulate DELETE response (typically no content) - # Call the delete_contact method - delete_response = contacts.delete_contact(contact_id=contact_id) - assert delete_response == {"success": True}, "Contact deletion failed." + # Step 2: Call the delete_contact method + contacts.delete_contact(contact_id=contact_id) - # Assert the API call was made with correct parameters + # Step 3: Verify the API call was made with the correct parameters mock_api_client.request.assert_called_once_with("DELETE", f"/contacts/{contact_id}") diff --git a/tests/unit/sdk/test_messages.py b/tests/unit/sdk/test_messages.py index 607c674..c87c6de 100644 --- a/tests/unit/sdk/test_messages.py +++ b/tests/unit/sdk/test_messages.py @@ -4,18 +4,24 @@ def test_send_message_success(messages, mock_api_client): """Test successfully sending a message.""" - # Mock response + # Mock API response mock_api_client.request.return_value = { "id": "msg123", "from": "+123456789", - "to": "+987654321", + "to": {"id": "contact123", "name": "John Doe", "phone": "+987654321"}, "content": "Hello, World!", "status": "queued", "createdAt": "2024-11-28T10:00:00Z" } - # Call method - payload = {"to": "+987654321", "content": "Hello, World!", "sender": "+123456789"} + # Corrected payload + payload = { + "to": {"id": "contact123"}, # The `to` field must be a dictionary with a valid contact ID + "content": "Hello, World!", + "from": "+123456789" # Use `from` field (aliased to `from_sender` in the schema) + } + + # Call the send_message method response = messages.send_message(payload=payload) # Assertions @@ -23,6 +29,7 @@ def test_send_message_success(messages, mock_api_client): assert response["id"] == "msg123" assert response["status"] == "queued" assert response["content"] == "Hello, World!" + assert response["to"] == {"id": "contact123", "name": "John Doe", "phone": "+987654321"} def test_send_message_validation_error(messages): @@ -37,10 +44,20 @@ def test_send_message_api_error(messages, mock_api_client): # Mock API error mock_api_client.request.side_effect = ApiError("Unhandled API Error") - payload = {"to": "+987654321", "content": "Hello, World!", "sender": "+123456789"} + # Corrected payload + payload = { + "to": {"id": "contact123"}, # Use correct dictionary structure for `to` + "content": "Hello, World!", + "from": "+123456789" # Use `from` field (aliased to `from_sender`) + } + + # Ensure the error is raised as expected with pytest.raises(ApiError, match="Unhandled API Error"): messages.send_message(payload=payload) + # Validate that the request was called with the correct parameters + mock_api_client.request.assert_called_once_with("POST", "/messages", json=payload) + def test_list_messages_success(messages, mock_api_client): """Test successfully listing messages.""" From 9bc4906faaa8720f60ec6bab2ac2d586da7c7de1 Mon Sep 17 00:00:00 2001 From: Amirul Islam Date: Fri, 6 Dec 2024 05:10:48 +0100 Subject: [PATCH 3/6] Refactor readme.md with sdk usage update --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index eb83843..4c2d09f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A Python SDK designed to simplify interaction with the messaging API and webhook functionalities. It ensures seamless message management, automatic signature validation, and provides a robust foundation for developing scalable messaging applications. +[![SDK Usage Documentation](https://img.shields.io/badge/Docs-SDK%20Usage%20Guide-blue?style=for-the-badge)](docs/sdk_usage.md) [![Webhook Documentation](https://img.shields.io/badge/Docs-Webhook%20Guide-blue?style=for-the-badge)](docs/webhook_guide.md) + --- ## Table of Contents @@ -80,33 +82,66 @@ The **Messaging SDK** is a Python library that allows developers to interact wit ### SDK Usage -1. Initialize the SDK: +1. **Initialize the SDK:** + ```python - from src.sdk.client import ApiClient - from src.sdk.features.messages import Messages + from sdk.client import ApiClient + from sdk.features.messages import Messages + # Initialize the API client and Messages module api_client = ApiClient() messages = Messages(api_client) # Send a message response = messages.send_message({ - "to": "+123456789", + "to": {"id": "contact123"}, # Use the contact ID format for the recipient "content": "Hello, World!", - "sender": "+987654321" + "from": "+987654321" # Sender's phone number }) print(response) ``` -2. List messages: +2. **List Messages:** + ```python + # List sent messages with pagination response = messages.list_messages(page=1, limit=10) print(response) ``` - + + **Example Response:** + ```json + { + "messages": [ + { + "id": "msg123", + "to": { + "id": "contact123", + "name": "John Doe", + "phone": "+1234567890" + }, + "from": "+987654321", + "content": "Hello, World!", + "status": "delivered", + "createdAt": "2024-12-01T00:00:00Z" + } + ], + "page": 1, + "quantityPerPage": 10 + } + ``` + +#### Additional Features + +- **Contact Management:** Add, update, delete, and list contacts. +- **Webhook Integration:** Validate and handle webhook payloads with ease. + #### Comprehensive User Guide for SDK Usage [![SDK Usage Documentation](https://img.shields.io/badge/Docs-SDK%20Usage%20Guide-blue?style=for-the-badge)](docs/sdk_usage.md) +--- + ### Webhook Setup 1. Run the webhook server: @@ -151,6 +186,10 @@ A detailed overview of the project structure, including descriptions of key file ├── .github/ # GitHub workflows for CI/CD │ └── workflows/ │ └── ci.yml # Continuous Integration pipeline configuration +├── docs/ # Additional documentation +│ ├── openapi.yaml # OpenAPI docs +│ ├── sdk_usage.md # Comprehensive SDK usage documentation +│ └── webhook_guide.md # Webhook-specific documentation ├── src/ # Source code directory │ ├── core/ # Core modules for shared logic │ │ ├── __init__.py # Core module initialization @@ -211,10 +250,7 @@ A detailed overview of the project structure, including descriptions of key file ├── requirements.in # Base Python dependencies ├── requirements.txt # Locked Python dependencies ├── README.md # Project documentation and usage guide -├── docs/ # Additional documentation -│ ├── openapi.yaml # OpenAPI docs -│ ├── sdk_usage.md # Comprehensive SDK usage documentation -│ └── webhook_guide.md # Webhook-specific documentation +├── setup.py # Setup file to enable 'pip install .' ``` @@ -335,7 +371,7 @@ Here is the comprehensive example for lifecycle of Messaging SDK and Webhook for | Example Code: | | sdk.messages.send_message( | | to="+123456789", | -| sender="+987654321", | +| from_sender="+987654321", | | content="Hello, World!" | | ) | +----------------------------------------------------------+ @@ -357,7 +393,7 @@ Here is the comprehensive example for lifecycle of Messaging SDK and Webhook for | { | | "to": "+123456789", | | "content": "Hello, World!", | -| "sender": "+987654321" | +| "from_sender": "+987654321" | | } | +----------------------------------------------------------+ | From c794ffbf7bd841bb7ff5af468a94ff0608602b95 Mon Sep 17 00:00:00 2001 From: Amirul Islam Date: Fri, 6 Dec 2024 05:25:42 +0100 Subject: [PATCH 4/6] Refactor sdk usage documentation --- docs/sdk_usage.md | 128 ++++++++++++++++++++++++++++++---------------- 1 file changed, 85 insertions(+), 43 deletions(-) diff --git a/docs/sdk_usage.md b/docs/sdk_usage.md index fc60783..5620f2f 100644 --- a/docs/sdk_usage.md +++ b/docs/sdk_usage.md @@ -1,6 +1,6 @@ # SDK Usage Guide -Welcome to the SDK Usage Guide for the **messaging-sdk**. This guide will walk you through the installation, configuration, and practical usage of the SDK, providing detailed examples and instructions for integrating the SDK into your applications. +Welcome to the **messaging-sdk** SDK Usage Guide. This document provides detailed instructions for installing, configuring, and utilizing the SDK, including advanced features and best practices. --- @@ -13,42 +13,64 @@ Welcome to the SDK Usage Guide for the **messaging-sdk**. This guide will walk y - [Sending Messages](#sending-messages) - [Managing Contacts](#managing-contacts) 5. [Advanced Usage](#advanced-usage) + - [Pagination](#pagination) + - [Retry Mechanism](#retry-mechanism) 6. [Error Handling](#error-handling) 7. [Testing](#testing) 8. [Logging](#logging) +9. [Complete Functionalities](#complete-functionalities) --- ## Introduction -The `messaging-sdk` is a Python library designed to simplify interactions with the messaging and contacts API. The SDK provides: +The `messaging-sdk` is a Python library designed for seamless integration with messaging and contacts APIs. It provides: -- Simplified API interactions without requiring manual configuration of authentication headers. -- IDE-friendly auto-completion for seamless development. -- Robust retry mechanisms for handling transient errors. -- Built-in validation to ensure API requests meet expected formats. +- Simplified API interactions with minimal setup. +- Robust error handling and retry mechanisms. +- Logging for debugging and monitoring. +- Easy-to-use methods for sending messages and managing contacts. --- ## Installation -To install the SDK, use `pip`: +To install the SDK locally: -```bash -pip install -r requirements.txt -``` +1. Clone the repository: + + ```bash + git clone https://github.com/shiningflash/messaging-sdk.git + cd messaging-sdk + ``` + +2. Install additional dependencies if required: + + ```bash + pip install -r requirements.txt + ``` --- ## Configuration -1. Copy the `.env.example` file to `.env` in the root directory: +1. Copy the `.env.example` file and rename it to `.env`: ```bash cp .env.example .env ``` -2. Open `.env` and update the variables accordingly. +2. Open `.env` and configure the following variables: + + - `BASE_URL`: Base URL for the API (e.g., `http://localhost:3000`). + - `API_KEY`: Your API key for authentication. + - `WEBHOOK_SECRET`: Secret key for validating webhooks. + +Install the SDK using pip in editable mode: + +```bash +pip install -e . +``` --- @@ -56,47 +78,49 @@ pip install -r requirements.txt ### Sending Messages -The SDK provides a straightforward way to send messages: +The SDK allows you to send messages easily: ```python -from src.sdk.features.messages import Messages -from src.sdk.client import ApiClient +from sdk.client import ApiClient +from sdk.features.messages import Messages client = ApiClient() messages = Messages(client) # Prepare the payload payload = { - "to": "+123456789", + "to": {"id": "contact-id"}, # Contact ID "content": "Hello, world!", - "sender": "+987654321" + "from": "+9876543210" # Sender's phone number } # Send the message response = messages.send_message(payload=payload) +print(response) ``` ### Managing Contacts -You can create, list, and delete contacts using the SDK: +You can create, list, and delete contacts: ```python -from src.sdk.features.contacts import Contacts +from sdk.features.contacts import Contacts contacts = Contacts(client) # Create a new contact new_contact = { "name": "John Doe", - "phone": "+123456789" + "phone": "+1234567890" } response = contacts.create_contact(new_contact) # List all contacts -contacts_list = contacts.list_contacts() +contacts_list = contacts.list_contacts(page=1, max=5) +print(contacts_list) # Delete a contact -contacts.delete_contact(contact_id="contact123") +contacts.delete_contact(contact_id="contact-id") ``` --- @@ -105,11 +129,11 @@ contacts.delete_contact(contact_id="contact123") ### Pagination -Retrieve paginated lists for messages or contacts: +The SDK supports pagination for listing messages and contacts: ```python # Retrieve paginated messages -messages_list = messages.list_messages(page=1, limit=5) +messages_list = messages.list_messages(page=1, limit=10) print(messages_list) # Retrieve paginated contacts @@ -117,43 +141,45 @@ contacts_list = contacts.list_contacts(page=1, max=5) print(contacts_list) ``` -### Retry Mechanisms +### Retry Mechanism -The SDK automatically retries failed requests for transient errors (e.g., `503 Service Unavailable`). You can customize retry logic in the `src/sdk/utils/retry.py` module. +The SDK automatically retries requests for transient errors (e.g., HTTP 503). The retry logic is located in `src/core/retry.py` and can be customized. --- ## Error Handling -The SDK raises specific exceptions for various error scenarios: +The SDK provides built-in exceptions for various scenarios: -- `UnauthorizedError`: Raised for `401 Unauthorized` responses. -- `NotFoundError`: Raised for `404 Not Found` responses. -- `ServerError`: Raised for `500 Internal Server Error` responses. -- `ApiError`: Raised for other unexpected API errors. +- `UnauthorizedError`: Raised for authentication errors (`401 Unauthorized`). +- `NotFoundError`: Raised when a resource is not found (`404 Not Found`). +- `ServerError`: Raised for server-side errors (`500 Internal Server Error`). +- `ContactNotFoundError`: Raised for missing contacts. +- `MessageNotFoundError`: Raised for missing messages. +- `ApiError`: Raised for other API-related issues. Example: ```python try: - messages.list_messages() -except UnauthorizedError as e: - print(f"Authentication failed: {e}") + messages.get_message("invalid-id") +except MessageNotFoundError as e: + print(f"Message not found: {e}") except ApiError as e: - print(f"Unexpected error: {e}") + print(f"API Error: {e}") ``` --- ## Testing -The SDK includes unit, integration, and end-to-end tests. To run all tests: +Run tests using `pytest`: ```bash pytest ``` -To generate a coverage report: +To check code coverage: ```bash pytest --cov=src --cov-report=term-missing @@ -163,15 +189,31 @@ pytest --cov=src --cov-report=term-missing ## Logging -The SDK includes comprehensive logging for debugging and auditing. Logs are categorized as follows: +Logs provide detailed insights into SDK operations: -- **Console Logs**: Informational and error logs for immediate feedback. -- **File Logs**: Warnings and errors logged to `logs/app.log`. +- **Console Logs**: Informational logs for debugging. +- **File Logs**: Errors and warnings logged to `logs/app.log`. -Example of enabling logger in your application: +Example: ```python -from src.core.logger import logger +from sdk.core.logger import logger -logger.info("Application started.") +logger.info("Starting application...") ``` + +--- + +## Complete Functionalities + +### Messages + +- **Send Message**: `send_message(payload)` +- **List Messages**: `list_messages(page, limit)` +- **Get Message by ID**: `get_message(message_id)` + +### Contacts + +- **Create Contact**: `create_contact(contact_payload)` +- **List Contacts**: `list_contacts(page, max)` +- **Delete Contact**: `delete_contact(contact_id)` From fed676a513472d9c02d35ab27764a6f0f4f54511 Mon Sep 17 00:00:00 2001 From: Amirul Islam Date: Fri, 6 Dec 2024 11:55:43 +0100 Subject: [PATCH 5/6] Fix delete api operation issue --- src/sdk/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sdk/client.py b/src/sdk/client.py index 1096ad4..e35ba83 100644 --- a/src/sdk/client.py +++ b/src/sdk/client.py @@ -80,6 +80,11 @@ def request(self, method: str, endpoint: str, **kwargs) -> Any: logger.info(f"Sending {method} request to {url} with headers {headers} and payload {kwargs}") response = requests.request(method, url, headers=headers, **kwargs) logger.info(f"Received response with status {response.status_code}") + + # Handle deletion api + if response.status_code == 204: + logger.info(f"Item successfully deleted.") + return None # Handle API errors self._handle_api_errors(response) From 9d2adbcc5691f48eda1552deda70f3f59ca455c3 Mon Sep 17 00:00:00 2001 From: Amirul Islam Date: Fri, 6 Dec 2024 12:21:51 +0100 Subject: [PATCH 6/6] Update documentation of webhook guide' --- docs/webhook_guide.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/webhook_guide.md b/docs/webhook_guide.md index 4e3d604..8f24a84 100644 --- a/docs/webhook_guide.md +++ b/docs/webhook_guide.md @@ -145,6 +145,24 @@ curl -X POST http://localhost:3010/webhooks \ -d '{"id": "msg123", "status": "delivered", "deliveredAt": "2024-11-30T12:00:00Z"}' ``` +For example, + +```bash +secret="mySecret" +payload='{"id": "msg123", "status": "delivered", "deliveredAt": "2024-11-30T12:00:00Z"}' +signature=$(echo -n $payload | openssl dgst -sha256 -hmac $secret | awk '{print $2}') + +curl -X POST http://localhost:3010/webhooks \ + -H "Authorization: Bearer $signature" \ + -H "Content-Type: application/json" \ + -d "$payload" +``` + +The response should be look like this, + +``` +{"message":"Webhook processed successfully."} +``` ### Unit Tests