Skip to content

Commit

Permalink
98 setup library for and add to pypi (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jordan-Prescott authored Aug 20, 2024
1 parent 5d61685 commit d78bb40
Show file tree
Hide file tree
Showing 22 changed files with 215 additions and 86 deletions.
77 changes: 65 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,78 @@
# 👋 Welcome!!
# ⚔️ Odin's Spear

Working on Broadwork's for 5 years was painful, with its stiff 90’s user interface and all its limitations so when I first saw Odin by [Rev.io](https://www.rev.io/blog/solutions/rev-io-odin-api). I nearly fell off my seat. Those beautiful people brought the solution into the 21st century with a modern user interface, automation, and most of all its API. This made my life so much easier, I was able to build 500 users in one go rather than one by one, I could give users access to manage their own systems, and I could duplicate groups with everything I needed when building new groups for customers. But I still couldn’t easily locate where the alias 0 was assigned and here is where the story of Odin’s Spear begins.
![Odin's Spear Logo](./assets/images/logo.svg)

Odin’s web user interface is great but it’s not perfect and it's limited, however, its API is a gift sent from the developer gods over at Rev.io (thank you!). Using the documentation and a few lines of code you can achieve everything you can achieve in the web interface but you are no longer limited to how fast you can operate your keyboard with the power of programming languages like Python. I can create a loop to create 10, 100, 1000, or 10, 000 users, hunt groups, and call centers in seconds or minutes.
## Overview

Managing them all becomes a breeze also, let's say a customer has a new user they want to add to all 50 of their hunt groups (if you’ve read this far I’m assuming you know what this is). If you’re using Broadworks then block the day out in your calendar, if you're using Odin’s web portal block the morning out, if you're using Python with the Odin API block 30 minutes out (that’s being generous).
Odin's Spear is a Python library designed to streamline and enhance your experience with Odin's API by [Rev.io](https://www.rev.io/blog/solutions/rev-io-odin-api). If you've worked with BroadWorks for years and struggled with its outdated interface and limitations, Odin's API feels like a breath of fresh air—offering a modern user interface, automation, and comprehensive API access.

[Odin API Documenation](https://doc.odinapi.net/)
With Odin's Spear, managing users, hunt groups, call centers, and other telecom operations becomes significantly easier. This library encapsulates Odin's API functionality, making it accessible, efficient, and user-friendly.

Alas, using the API is not perfect, each time I would write a script to help me achieve these things I was hit with some recurring issues, I would have to find the API call I needed, format the data, design the request, handle any errors that could occur and the list goes on. Each time I was starting fresh and this was frustrating, if only there was a solution that handled my authentication, I could just select the method and pass it the data rather than designing the request, if an error occurs tell me what the issue is, resolve or even handle it for me…
## Features

## Introducing...
- **Bulk User Management:** Create and manage thousands of users, hunt groups, and call centers in minutes.
- **Error Handling:** Automatically manage authentication, request design, and error handling.
- **Advanced Tools:** Features like call flow visualization, group audit reports, and bulk management of telecom entities.
- **Alias Assignment Locator:** The first feature release addresses a long-standing issue by allowing you to easily locate where an alias is assigned within BroadWorks—saving you time and frustration.

![Odin's Spear Logo](./assets/images/logo.svg)
## Why Odin's Spear?

Working with BroadWorks for over five years was a challenge, with its 90s-style UI and rigid functionality. When Rev.io introduced Odin, with its modern interface and API, it revolutionized how telecom management could be done. However, even with these advancements, some tasks remained cumbersome, like locating alias assignments.

Odin's Spear is the solution. It simplifies your workflow by automating repetitive tasks, handling errors, and making API interactions as smooth as possible. Whether you're managing 10 users or 10,000, Odin's Spear has you covered.

## 🚀 Getting Started

### Prerequisites

- Python 3.10+
- An Odin account

### Installation

Install Odin's Spear using pip:

```bash
pip install odins-spear
```

### Basic Usage

**Odin’s Spear** is a Python library that aims to do exactly that. It will encapsulate the entire functionality of Odin’s API making the use easy, efficient, and accessible. The project is stakeholder lead and all features have come from engineers that have been using Broadworks and now Odin for decades. It will also introduce other features requested such as a graph showing a call flow to a number, a group audit report for all things billable, and bulk management of call centers, hunt groups, and auto attendants. However, it was clear what the first feature had to be. For years now locating where an alias has been assigned on the Broadworks system has brought hosted telephony engineers to their knees. No more. With Odin’s Spears’ very first release (beta) I have addressed this issue making locating an alias a breeze. In 3 lines of code and 1 minute of your time, you can now achieve this once painstaking task, and more features are coming soon!
Here's a simple example to get you started:

![Odin's Spear Slogan](./assets/images/slogan.svg)
```python
from odins_spear.api import API

# Initialize the API with your credentials
my_api = api.Api(base_url="https://base_url/api/vx", username="john.smith", password="ODIN-INSTANCE-1")
my_api.authenticate()

# Locate an alias assignment
alias_info = my_api.scripter.find_alias('ServiceProviderID', 'GroupID', alias=0)
print(alias_info)
```

For more detailed usage and examples, check out our [Documentation](#-documentation).

## 📖 Documentation

We have extensive docs on how to get started, and all the features we have built. Check them out below:
We provide extensive documentation to help you get started quickly and take full advantage of Odin's Spear's capabilities:

- [Odin's Spear Documentation](https://docs.jordan-prescott.com/odins_spear)

## Contributing

We welcome contributions! If you'd like to contribute, please fork the project, make your changes then submit a pull request.
For issues to work on please see our [project](https://github.com/users/Jordan-Prescott/projects/2).

## License

This project is licensed under the MIT License—see the [LICENSE.md](LICENSE) file for details.

## Support

If you encounter any issues or have questions, feel free to open an issue on GitHub.

## Acknowledgements

[Odin's Spear Documenation](https://docs.jordan-prescott.com/odins_spear)
Special thanks to the developers at Rev.io for creating the Odin API and to the engineers who provided invaluable feedback and feature suggestions.
33 changes: 32 additions & 1 deletion odins_spear/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ def __init__(self, base_url: str, username: str, password: str,


def authenticate(self):
"""Authenticates session with username and password supplied by user.
Raises:
OSApiAuthenticationFail: Raised if authenticaion fails.
Returns:
Bool: Returns True to indicate authentication was successful.
"""

try:
response = self.post.session(self.username, self.password)
Expand All @@ -59,6 +67,14 @@ def authenticate(self):


def refresh_authorisation(self):
"""Re-authenticates the session with the API. Used if API key is to expire.
Raises:
OSSessionRefreshFail: Raised if authentication fails.
Returns:
Bool: Returns True to indicate authentication was successful.
"""

try:
response = self.put.session()
Expand All @@ -69,6 +85,15 @@ def refresh_authorisation(self):


def get_auth_details(self):
"""Gets current session details.
Raises:
OSFailedToLocateSession: Raised when session details can't be found.
Most likely because session has expired.
Returns:
Dict: Current session details.
"""

try:
return self.get.session()
Expand All @@ -77,12 +102,18 @@ def get_auth_details(self):


def _update_requester(self, session_response):
"""When authenticating or re-auth update requester with token so it can make
api calls
Args:
session_response (obj): Requests mod response.
"""
self.token = session_response["token"]
self._requester.headers["Authorization"] = f"Bearer {self.token}"
self.authorised = True


def __str__(self) -> str:
return f"API - url: {self.base_url}, username: {self.username}, password: {self.password}." \
return f"API - url: {self.base_url}, username: {self.username}" \
f"Authenticated: {self.authorised}"

11 changes: 10 additions & 1 deletion odins_spear/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
""" Library exceptions.
"""


class OSError(Exception):
""" Odin Api Exceptions
"""
Expand Down Expand Up @@ -33,6 +32,7 @@ class OSObjectParseError(OSError):

def __str__(self) -> str:
return f"Parsing Broadwork Entity failed."


class OSUnsupportedFilter(OSError):
""" Raised when user requests to filter on unsupported filter
Expand All @@ -41,13 +41,15 @@ class OSUnsupportedFilter(OSError):
def __str__(self) -> str:
return f"Unsupported filter. Supported: macAddress, lastName," \
f"firstName, dn, emailAddress, userId, extension"


class OSAliasNotFound(OSError):
""" Raised when alias is not found in Broadowks Group.
"""

def __str__(self) -> str:
return f"Alias not found, it either does not exist or check alias."


class OSSessionRefreshFail(OSError):
""" Raised when refreshing session fails.
Expand All @@ -56,27 +58,31 @@ class OSSessionRefreshFail(OSError):
def __str__(self) -> str:
return f"Refreshing sesion failed. Check credentials are valid and " \
f"token has not yet expired. If expired request another."


class OSLogoutFailed(OSError):
""" Raised when logout attempt failed.
"""

def __str__(self) -> str:
return f"Failed to logout, session still valid. Please try again."


class OSFailedToLocateSession(OSError):
""" Raised when user attempts to get session details but session cant be found.
"""

def __str__(self) -> str:
return f"Session details not found. Check token is valid and not exppired."


class OSInvalidCode(OSError):
""" Raised when code is less than 4 and higher than 6.
"""

def __str__(self) -> str:
return f"Code needs to be between 4 and 6 digits."


class OSInvalidWeighting(OSError):
""" Raised when invalid weighted call distribution set.
Expand All @@ -93,6 +99,7 @@ class OSInvalidData(OSError):
def __str__(self) -> str:
return f"Data invalid or incomplete, please check data passed to method is correct."


class OSInvalidBroadworkService(OSError):
""" Raised when service given by user is not a valid license of Broadworks.
"""
Expand All @@ -116,13 +123,15 @@ class OSServiceNotAssigned(OSError):
def __str__(self) -> str:
return f"Service not assigend to target Broadworks entity. Please check services assigned."


class OSFileNotFound(OSError):
""" Raised when a file can not be found.
"""

def __str__(self) -> str:
return f"File can not be found, please check path and file name."


class OSLicenseNonExistent(OSError):
""" Raised when the Specified Entity doesn't exist due to licensing.
"""
Expand Down
42 changes: 38 additions & 4 deletions odins_spear/requester.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,17 @@
from ratelimit import limits, sleep_and_retry
class Requester():


def __init__(self, base_url: str, rate_limit: bool, logger: object = None):
"""Requester is the object that handles all request to and from the API.
Each method object will manage the setting up of the request however it is Requester
that will send and receive the data.
Args:
base_url (str): Base URL of API
rate_limit (bool): Rate limit flag which will limit to 5 calls per 1 second
logger (object, optional): Logger object for logging requests. Defaults to None.
"""

self.base_url = base_url
self.headers = {
'Authorization': "",
Expand All @@ -15,6 +24,7 @@ def __init__(self, base_url: str, rate_limit: bool, logger: object = None):
self.logger = logger


# get, post, put, delete methods takes in data and params and returns method type to _request
def get(self, endpoint, data=None, params=None):
return self._request(requests.get, endpoint, data, params)

Expand All @@ -32,8 +42,19 @@ def delete(self, endpoint, data=None):


def _request(self, method, endpoint, data=None, params=None):
"""Handles an API request with or without rate limiting.
Args:
method (request obj): Request module method type either GET, POST, PUT, DELETE
endpoint (str): Specific API endpoint for functionality
data (dict, optional): Python dict used in payload data if needed. Defaults to None.
params (dict, optional): Parameters used in endpoint if needed. Defaults to None.
Returns:
API data: Data returned from API on completion of API call.
"""


# if rate limiting is enabled uses _rate_limited_request where limiting is in place.
if self.rate_limit:
return self._rate_limited_request(method, endpoint, data, params)
else:
Expand All @@ -43,21 +64,34 @@ def _request(self, method, endpoint, data=None, params=None):
data=json.dumps(data if data is not None else {}),
params=(params if params is not None else {})
)
self.logger._log_request(endpoint=endpoint, response_code=response.status_code)

# if logger used log request
if self.logger:
self.logger._log_request(endpoint=endpoint, response_code=response.status_code)

# flags errors if any returned from the API
response.raise_for_status()
return response.json()


@sleep_and_retry
@limits(calls=5, period=1)
def _rate_limited_request(self, method, endpoint, data=None, params=None):
"""Handles an API request with rate limiting.
"""

response = method(
url=self.base_url + endpoint,
headers=self.headers,
data=json.dumps(data if data is not None else {}),
params=(params if params is not None else {})
)
self.logger._log_request(endpoint=endpoint, response_code=response.status_code)

# if logger used log request
if self.logger:
self.logger._log_request(endpoint=endpoint, response_code=response.status_code)

# flags errors if any returned from the API
response.raise_for_status()
return response.json()

File renamed without changes.
File renamed without changes.
3 changes: 1 addition & 2 deletions odins_spear/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
__all__ = ["broadwork_services", "generators", "formatting", "parsing"]
__all__ = ["generators", "formatting", "parsing"]

from .broadworks_services import *
from .generators import *
from .formatting import *
from .parsing import *
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
23 changes: 13 additions & 10 deletions odins_spear/utils/formatting.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from ..exceptions import *

def format_filter_value(type, value):
"""_summary_
"""Takes in a filter type and the value to filter for. Depenining on the type
the value is formatted with the correct wild card e.g. 'contains' will add a
wildcard to the start and end of value *value*
Args:
type (_type_): _description_
value (_type_): _description_
type (str): Either 'equal to', 'contains', 'starts with'
value (str): value to filter for
Raises:
OAUnsupportedFilter: _description_
OAUnsupportedFilter: Raised when unsupported filter is requested such as 'ends with'
Returns:
_type_: _description_
str: Formatted filter with the value and correct filter wildcards.
"""
if type.lower() == "equal to":
return f"{value}"
Expand All @@ -24,16 +26,17 @@ def format_filter_value(type, value):


def format_int_list_of_numbers(counrty_code: int, numbers: list):
"""_summary_
"""Takes a list of integer numbers with no country code and the country code needed.
This will then return a list of strings with the country code inserted infront of all
numbers in orginal list.
Args:
counrty_code (int): _description_
numbers (list): _description_
counrty_code (int): Country code to be added infront of all numbers in list
numbers (list): List of integer numbers with no country code
Returns:
_type_: _description_
list: List of numbers in string format with the country code added.
"""
numbers.sort()

formatted_numbers = [f'+{str(counrty_code)}-{str(num)}' for num in numbers]
return formatted_numbers
Loading

0 comments on commit d78bb40

Please sign in to comment.