class ArkeselErr(Exception):
    """
    API Key Cannot Be Found
    """


class MissingAPIKey(ArkeselErr):
    """
    API Key Not Found
    """
arkesel/errors.py
aglili/arkesel
import requests


class OTP:
    """
    A class to interact with the Arkesel OTP API for generating and verifying OTPs.

    Attributes:
        api_key (str): The API key for authentication.
    """

    def __init__(self, api_key: str) -> None:
        """
        Initializes an instance of the OTP class.

        Args:
            api_key (str): The API key for authentication.
        """
        self.api_key = api_key
        self.headers = {"api-key": api_key}

    def sms_otp(self, expiry_minutes: int, recipient: str, sender_id: str)->dict:
        """
        Generates an OTP and sends it via SMS.

        Args:
            expiry_minutes (int): The expiry time for the OTP in minutes.
            recipient (str): The recipient's phone number.
            sender_id (str): The sender ID to display in the SMS.

        Returns:
            response(dict): Returns Json Response
        """
        url = "https://sms.arkesel.com/api/otp/generate"
        data = {
            "expiry": expiry_minutes,
            "length": 6,
            "medium": "sms",
            "message": 'This is OTP for verification, %otp_code% and expires in %expiry%',
            "number": recipient,
            "sender_id": sender_id,
            "type": "numeric"
        }
        response = requests.post(url=url, headers=self.headers, data=data).json()
        return response


    def voice_otp(self, expiry_minutes: int, recipient: str, sender_id: str)->dict:
        """
        Generates an OTP and sends it via voice call.

        Args:
            expiry_minutes (int): The expiry time for the OTP in minutes.
            recipient (str): The recipient's phone number.
            sender_id (str): The sender ID for the voice call.

        Returns:
            response(dict): Returns Json Response
        """
        url = "https://sms.arkesel.com/api/otp/generate"
        data = {
            "expiry": expiry_minutes,
            "length": 6,
            "medium": "voice",
            "message": 'This is OTP for verification, %otp_code% and expires in %expiry%',
            "number": recipient,
            "sender_id": sender_id,
            "type": "numeric"
        }
        response = requests.post(url=url, headers=self.headers, data=data).json()
        return response


    def verify_otp(self, code: str, number: str)->dict:
        """
        Verifies an OTP with the provided code and phone number.

        Args:
            code (str): The OTP code to verify.
            number (str): The phone number associated with the OTP.

        Returns:
            response(dict): Returns Json Response
        """
        url = "https://sms.arkesel.com/api/otp/verify"
        data = {
            "api-key": self.api_key,
            "code": code,
            "number": number
        }
        response = requests.post(url=url, data=data, headers=self.headers).json()
        return response
arkesel/otp.py
aglili/arkesel
import requests
from typing import Optional
from .errors import MissingAPIKey

class SMSV1:
    """
    A class to interact with the Arkesel SMS API for sending SMS messages and retrieving account information.

    Attributes:
        api_key (str): The API key for authentication.
    """
    def __init__(self, api_key: str) -> None:
        """
        Initializes an instance of the SMSV1 class.

        Args:
            api_key (str): The API key for authentication.
        """
        if not api_key:
            raise MissingAPIKey("Your API Key is missing")
        self.api_key = api_key

    def send_sms(self, sender: str, message: str, recipient: str)->dict:
        """
        Sends an SMS message to the specified recipient.

        Args:
            sender (str): The sender's phone number or name.
            message (str): The message content.
            recipient (str): The recipient's phone number.

        Returns:
            response(dict): Returns Json Response
        """
        url = "https://sms.arkesel.com/sms/api?action=send-sms"
        params = {
            "api_key": self.api_key,
            "to": recipient,
            "from": sender,
            "sms": message
        }
        response = requests.get(url=url, params=params).json()
        return response


    def schedule_sms(self, recipient: str, sender: str, message: str, time: str)->dict:
        """
        Schedule an SMS message to be sent at a specific time.

        Args:
            recipient (str): The recipient's phone number.
            sender (str): The sender's phone number or name.
            message (str): The message content.
            time (str): The time to schedule the SMS (format: "YYYY-MM-DD HH:MM:SS AM/PM").

        Returns:
            response(dict): Returns Json Response
        """
        url = f"https://sms.arkesel.com/sms/api?action=send-sms&api_key={self.api_key}&from={sender}&sms={message}&schedule={time}"

        response = requests.get(url=url).json()
        return response

    def check_balance(self)->dict:
        """
        Retrieves the account balance.

        Returns:
            dict: The API response in JSON format containing balance information.
        """
        url = "https://sms.arkesel.com/sms/api?action=check-balance"
        params = {
            "api_key": self.api_key,
            "response": "json"
        }
        response = requests.get(url=url, params=params).json()
        return response


    def save_contact(self, phonebook_name: str, phone_number: str, first_name: Optional[str] = None,
                     last_name: Optional[str] = None, email: Optional[str] = None, company: Optional[str] = None)->dict:
        """
        Saves a contact to the specified phone book.

        Args:
            phonebook_name (str): The name of the phone book.
            phone_number (str): The phone number of the contact.
            first_name (str, optional): The first name of the contact. Defaults to None.
            last_name (str, optional): The last name of the contact. Defaults to None.
            email (str, optional): The email address of the contact. Defaults to None.
            company (str, optional): The company name of the contact. Defaults to None.

        Returns:
            response(dict): Returns Json Response
        """
        url = "https://sms.arkesel.com/contacts/api"
        params = {
            "action": "subscribe-us",
            "api_key": self.api_key,
            "phone_book": phonebook_name,
            "phone_number": phone_number,
        }

        if first_name:
            params["first_name"] = first_name
        if last_name:
            params["last_name"] = last_name
        if email:
            params["email"] = email
        if company:
            params["company"] = company

        response = requests.get(url=url, params=params).json()
        return response

arkesel/smsV1.py
aglili/arkesel
import requests
from .errors import MissingAPIKey

class SMSV2:

    """
    A class to send SMS messages using the Arkesel SMS API.

    This Uses Version 2 of the API

    Attributes:
        api_key (str): The API key for authentication.
    """
    def __init__(self,api_key:str) -> None:

        """
        Initializes an instance of the SMSV2 class.

        Args:
            api_key (str): The API key for authentication.
        """
        if not api_key:
            raise MissingAPIKey("Your API Key is missing")
        self.headers = {"api-key":api_key}



    def send_sms(self,sender:str,message:str,recipients:list)->dict:
        """
        Sends an SMS message to the specified recipients.

        Args:
            sender (str): The sender's phone number or name.
            message (str): The message content.
            recipients (List[str]): List of recipient phone numbers.

        Returns:
            response(dict): Returns Json Response
        """
        url = "https://sms.arkesel.com/api/v2/sms/send"
        data = {
        "sender": sender,
        "message": message,
        "recipients": recipients 
        }
        response = requests.post(url=url,data=data,headers=self.headers)
        return response



    def schedule_sms(self,sender:str,recipients:list,message:str,schedule_date:str)->dict:

        """
        Schedule an SMS message to be sent at a specific date and time.

        Args:
            sender (str): The sender's phone number or name.
            recipients (list): List of recipient phone numbers.
            message (str): The message content.
            schedule_date (str): The date and time to schedule the SMS (format: "YYYY-MM-DD HH:MM:SS").

        Returns:
            response(dict): Returns Json Response
        """
        url = "https://sms.arkesel.com/api/v2/sms/send"


        data = {
        "sender": sender,
        "message": message,
        "recipients": recipients,
        "scheduled_date": schedule_date
        }


        response = requests.post(url=url,data=data,headers=self.headers)

        return response
    

    def check_balance(self)->dict:
        """
        Retrieves the account balance and other related details.

        Returns:
            response(dict): Returns Json Response
        """
        url = "https://sms.arkesel.com/api/v2/clients/balance-details"
        response = requests.get(url=url,headers=self.headers).json()
        return response
    
    def sms_details(self,sms_id:str)->dict:
        """
        Retrieves details of a specific SMS using its ID.

        Args:
            sms_id (str): The ID of the SMS.

        Returns:
            response(dict): Returns Json Response
        """
        url = f"https://sms.arkesel.com/api/v2/sms{sms_id}"
        response = requests.get(url=url,headers=self.headers).json()
        return response
    

    def group_message(self,send_id:str,message:str,group_name:str)->dict:
        """
        Sends a message to a created group.

        Args:
            send_id (str): Id of the sender.
            message (str): Message to be sent to that particular group
            group_name (str): The Name of the group to which the message is being sent

        Returns:
            response(dict): Returns Json Response
        """

        url = "https://sms.arkesel.com/api/v2/sms/send/contact-group"

        data = {
            "sender": send_id,
            "group_name": group_name,
            "message": message
        }

        response = requests.post(url=url,data=data,headers=self.headers).json()

        return response



arkesel/smsV2.py
aglili/arkesel
import requests
from typing import List,Optional

class VOICE:
    """
    A class to interact with the Arkesel Voice API for sending voice messages.

    Attributes:
        api_key (str): The API key for authentication.
    """

    def __init__(self, api_key: str) -> None:
        """
        Initializes an instance of the VOICE class.

        Args:
            api_key (str): The API key for authentication.
        """
        self.headers = {"api-key": api_key}

    def send_voice_message(self, recipients: List[str], voice_file: str,voice_id:Optional[str]=None)->dict:
        """
        Sends a voice message to the specified recipients.

        Args:
            recipients (List[str]): List of recipient phone numbers.
            voice_file (str): Path or URL to the voice file.
            voice_id (str): A Voice ID or DID number that can be used for the Voice SMS.Voice ID is optional and can be registered through Arkesel Support channel.

       Returns:
            response(dict): Returns Json Response
        """
        url = "https://sms.arkesel.com/api/v2/sms/voice/send"
        data = {
            "voice_file": voice_file,
            "recipients": recipients
        }
        if voice_id:
            data["voice_id"] = voice_id
        response = requests.post(url=url, headers=self.headers, data=data).json()
        return response

arkesel/voice.py
aglili/arkesel
from setuptools import setup,find_packages



setup(
    name="arkesel",
    version="0.1.7",
    packages=find_packages(),
    install_requires=[
        'certifi==2023.7.22',
        'charset-normalizer==3.2.0',
        'docutils==0.20.1',
        'idna==3.4',
        'importlib-metadata==6.8.0',
        'jaraco.classes==3.3.0',
        'keyring==24.2.0',
        'markdown-it-py==3.0.0',
        'mdurl==0.1.2',
        'more-itertools==10.1.0',
        'nh3==0.2.14',
        'pkginfo==1.9.6',
        'Pygments==2.16.1',
        'pywin32-ctypes==0.2.2',
        'readme-renderer==42.0',
        'requests==2.31.0',
        'requests-toolbelt==1.0.0',
        'rfc3986==2.0.0',
        'rich==13.5.3',
        'twine==4.0.2',
        'urllib3==2.0.6',
        'zipp==3.16.2'
    ],
    classifiers=[
        'Programming Language :: Python :: 3',
        'License :: OSI Approved :: MIT License',
        'Operating System :: OS Independent',
    ],

    # meta data
    author="Aglili Selorm Cecil",
    author_email="cecilselorm34@gmail.com",
    description= "Un-Official API-Wrapper for the Arkesel API",
    url="https://github.com/aglili/arkesel",
    license="MIT"
)
setup.py
aglili/arkesel
import os
import pytest
from arkesel.otp import OTP

ARKESEL_API_KEY = os.getenv("ARKESEL_API_KEY")

@pytest.fixture
def mock_requests_post(mocker):
    return mocker.patch("requests.post")

@pytest.fixture
def otp_instance():
    return OTP(api_key=ARKESEL_API_KEY)

def test_sms_otp(mocker, mock_requests_post, otp_instance):
    expected_response = {
        'code': '1000',
        'ussd_code': '*928*01#',
        'message': 'Successful, OTP is being processed for delivery'
    }

    mock_requests_post.return_value.status_code = 200
    mock_requests_post.return_value.json.return_value = expected_response

    # Call the sms_otp method
    response = otp_instance.sms_otp(5, '0557362859', 'Aglili')

    # Check if the API was called with the correct parameters
    mock_requests_post.assert_called_once_with(
        url="https://sms.arkesel.com/api/otp/generate",
        headers={'api-key': ARKESEL_API_KEY},
        data={
            'expiry': 5,
            'length': 6,
            'medium': 'sms',
            'message': 'This is OTP for verification, %otp_code% and expires in %expiry%',
            'number': '0557362859',
            'sender_id': 'Aglili',
            'type': 'numeric'
        }
    )

    # Check the API response
    assert response == expected_response

def test_voice_otp(mocker, mock_requests_post, otp_instance):
    expected_response = {
        'code': '1000',
        'ussd_code': '*928*01#',
        'message': 'Successful, OTP is being processed for delivery'
    }

    mock_requests_post.return_value.status_code = 200
    mock_requests_post.return_value.json.return_value = expected_response

    # Call the voice_otp method
    response = otp_instance.voice_otp(5, '0557362859', 'Aglili')

    # Check if the API was called with the correct parameters
    mock_requests_post.assert_called_once_with(
        url="https://sms.arkesel.com/api/otp/generate",
        headers={'api-key': ARKESEL_API_KEY},
        data={
            'expiry': 5,
            'length': 6,
            'medium': 'voice',
            'message': 'This is OTP for verification, %otp_code% and expires in %expiry%',
            'number': '0557362859',
            'sender_id': 'Aglili',
            'type': 'numeric'
        }
    )

    # Check the API response
    assert response == expected_response

def test_verify_otp(mocker, mock_requests_post, otp_instance):
    expected_response = {
        "code": "1100",
        "message": "Successful"
    }

    mock_requests_post.return_value.status_code = 200
    mock_requests_post.return_value.json.return_value = expected_response

    # Call the verify_otp method
    response = otp_instance.verify_otp('123456', '0557362859')

    # Check if the API was called with the correct parameters
    mock_requests_post.assert_called_once_with(
        url="https://sms.arkesel.com/api/otp/verify",
        headers={'api-key': ARKESEL_API_KEY},
        data={
            'api-key': ARKESEL_API_KEY,
            'code': '123456',
            'number': '0557362859'
        }
    )

    # Check the API response
    assert response == expected_response


tests/test_otp.py
aglili/arkesel
import os
import sys
import pytest
from arkesel.smsV1 import SMSV1
from dotenv import load_dotenv

load_dotenv()

ARKESEL_API_KEY = os.getenv("ARKESEL_API_KEY")

project_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, project_directory)

@pytest.fixture
def sms_instance():
    return SMSV1(api_key=ARKESEL_API_KEY)

def test_send_sms(sms_instance, mocker):
    sender = "SenderID"
    message = "This Is A Message"
    recipient = "054xxxxxxx"

    expected_response = {
        "code": "ok",
        "message": "Successfully Sent",
        "balance": 1185,
        "main_balance": 2.974,
        "user": "Arkesel Dev"
    }

    mocker.patch("requests.get", return_value=MockResponse(200, expected_response))

    response = sms_instance.send_sms(sender, message, recipient)

    assert response == expected_response

@pytest.mark.parametrize("scheduled_time", ["2023-09-30 14:30:00 PM"])
def test_schedule_sms(sms_instance, scheduled_time, mocker):
    sender = "SenderID"
    message = "This Is A Scheduled Message"
    recipient = "054xxxxxxx"

    expected_response = {
        "code": "ok",
        "message": "Scheduled SMS created successfully",
        "scheduled_time": scheduled_time
    }

    mocker.patch("requests.get", return_value=MockResponse(200, expected_response))

    response = sms_instance.schedule_sms(recipient, sender, message, scheduled_time)

    assert response == expected_response

@pytest.mark.parametrize("phonebook_name, phone_number, first_name, last_name, email, company", [
    ("MyPhoneBook", "1234567890", "John", "Doe", "john.doe@example.com", "Example Inc.")
])
def test_save_contact(sms_instance, phonebook_name, phone_number, first_name, last_name, email, company, mocker):
    expected_response = {
        "code": "ok",
        "message": "Contact saved successfully"
    }

    mocker.patch("requests.get", return_value=MockResponse(200, expected_response))

    response = sms_instance.save_contact(phonebook_name, phone_number, first_name, last_name, email, company)

    assert response == expected_response

def test_check_balance(sms_instance, mocker):
    expected_response = {
        "balance": 1185,
        "user": "Arkesel Dev",
        "country": "Ghana"
    }

    mocker.patch("requests.get", return_value=MockResponse(200, expected_response))

    response = sms_instance.check_balance()

    assert response == expected_response

class MockResponse:
    def __init__(self, status_code, json_data):
        self.status_code = status_code
        self.json_data = json_data

    def json(self):
        return self.json_data

tests/test_smsv1.py
aglili/arkesel
import os
import pytest
from dotenv import load_dotenv
from arkesel.smsV2 import SMSV2

load_dotenv()

ARKESEL_API_KEY = os.getenv("ARKESEL_API_KEY")

@pytest.fixture
def smsv2_instance(mocker):
    return SMSV2(api_key=ARKESEL_API_KEY)

def test_send_sms(smsv2_instance, mocker):
    sender = "SenderID"
    message = "This Is A Message"
    recipients = ['1234567890', '9876543210', '22354674948']

    expected_response = {
        "status": "success",
        "data": [
            {
                "recipient": "1234567890",
                "id": "9b752841-7ee7-4d40-b4fe-768bfb1da4f0"
            },
            {
                "recipient": "9876543210",
                "id": "7ea01acd-485c-4df3-b646-e9e24430e145"
            },
            {
                "invalid numbers": [
                    "22354674948"
                ]
            }
        ]
    }

    # Mock the requests.post
    mocker.patch("requests.post", return_value=mocker.Mock(json=lambda: expected_response))

    # Call the send_sms method 
    response = smsv2_instance.send_sms(sender, message, recipients)
    assert response == expected_response

def test_schedule_sms(smsv2_instance, mocker):
    sender = "SenderID"
    message = "This Is A Scheduled Message"
    recipients = ['1234567890', '9876543210', '22354674948']
    scheduled_date = "2023-09-30 14:30:00 PM"

    expected_response = {
        "status": "success",
        "data": [
            {
                "recipient": "1234567890",
                "id": "9b752841-7ee7-4d40-b4fe-768bfb1da4f0",
                "scheduled_date": scheduled_date
            },
            {
                "recipient": "9876543210",
                "id": "7ea01acd-485c-4df3-b646-e9e24430e145",
                "scheduled_date": scheduled_date
            }
        ],
        "invalid numbers": ["22354674948"]
    }

    # Mock the requests.post 
    mock_post = mocker.patch("requests.post")
    mock_post.return_value.json.return_value = expected_response

    # Call the schedule_sms method 
    response = smsv2_instance.schedule_sms(sender, recipients, message, scheduled_date)
    assert response == expected_response

def test_check_balance(smsv2_instance, mocker):
    # Define the expected API response
    expected_response ={
        "status": "success",
        "data": {
            "sms_balance": "2003",
            "main_balance": "GHS 20.99"
        }
    }

    # Mock the requests.get call using mocker.patch
    mock_get = mocker.patch("requests.get")
    mock_get.return_value.json.return_value = expected_response

    # Call the check_balance method and assert the response
    response = smsv2_instance.check_balance()
    assert response == expected_response

def test_sms_details(smsv2_instance, mocker):
    sms_id = "SMS123"

    expected_response = {
        "status": "success",
        "data": {
            "ID": sms_id,
            "status": "DELIVERED",
            "sender": "Arkesel",
            "recipient": "233544919953",
            "message": "Welcome to version 2 of our API!",
            "message_count": 1,
            "sent_at_time": "2021-04-09 18:44:05"
        }
    }

    # Mock the requests.get call using mocker.patch
    mock_get = mocker.patch("requests.get")
    mock_get.return_value.json.return_value = expected_response

    # Call the sms_details method and assert the response
    response = smsv2_instance.sms_details(sms_id)
    assert response == expected_response

tests/test_smsv2.py
aglili/arkesel
import os
import pytest
from requests import Response
from arkesel.voice import VOICE

ARKESEL_API_KEY = os.getenv("ARKESEL_API_KEY")

@pytest.fixture
def voice_instance():
    return VOICE(api_key=ARKESEL_API_KEY)

def test_send_voice_message(voice_instance, mocker):
    recipients = ['1234567890', '9876543210']
    voice_file = 'https://example.com/voice_file.mp3'
    voice_id = 'VOICE_ID123'

    expected_response = {
        'status': 'success',
        'message': 'Voice message sent successfully!'
    }

    def mock_requests_post(url, headers, json=None, data=None):
        assert url == "https://sms.arkesel.com/api/v2/sms/voice/send"
        assert headers['api-key'] == ARKESEL_API_KEY
        assert json is None  
        assert data == {
            'voice_file': voice_file,
            'recipients': recipients,
            'voice_id': voice_id
        }

        # Create Mock response object
        mock_response = Response()
        mock_response.status_code = 200
        mock_response.json = lambda: expected_response

        return mock_response

    mocker.patch("requests.post", side_effect=mock_requests_post)

    # Call the send_voice_message method
    response = voice_instance.send_voice_message(recipients, voice_file, voice_id)

    # Check if the API was called with the correct parameters
    assert response == expected_response

tests/test_voice.py
aglili/arkesel