Skip to content

Commit

Permalink
Merge pull request #13 from exonet/refactor
Browse files Browse the repository at this point in the history
Breaking refactor
  • Loading branch information
trizz authored Aug 14, 2019
2 parents 49297c4 + 9cecf3b commit e3184f2
Show file tree
Hide file tree
Showing 31 changed files with 705 additions and 468 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ All notable changes to `exonet-api-python` will be documented in this file.
Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles.

## [Unreleased]
[Compare 0.0.5 - Unreleased](https://github.com/exonet/exonet-api-python/compare/0.0.5...master)
[Compare 1.0.0 - Unreleased](https://github.com/exonet/exonet-api-python/compare/1.0.0...master)

## [1.0.0](https://github.com/exonet/exonet-api-python/releases/tag/1.0.0) - 2019-08-14
[Compare 0.0.5 - 1.0.0](https://github.com/exonet/exonet-api-python/compare/0.0.5...1.0.0)
## Breaking
- The Client has been refactored to keep consistency between packages in different programming languages. See the updated documentation and examples.

## [0.0.5](https://github.com/exonet/exonet-api-python/releases/tag/0.0.5) - 2019-04-29
[Compare 0.0.4 - 0.0.5](https://github.com/exonet/exonet-api-python/compare/0.0.4...0.0.5)
Expand Down
4 changes: 2 additions & 2 deletions examples/dns_zone_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from exonetapi import Client

# Create a new Client.
client = Client('https://api.exonet.nl')
client = Client()

# Authorize with a personal access token.
client.authenticator.set_token(sys.argv[1])
Expand All @@ -28,7 +28,7 @@
))

# Get the records for this zone.
records = client.resource('dns_zones/{id}/records'.format(id=zone.id())).get()
records = zone.related('records').get()

# Show records.
for record in records:
Expand Down
4 changes: 2 additions & 2 deletions examples/dns_zones.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from exonetapi import Client

# Create a new Client.
client = Client('https://api.exonet.nl')
client = Client()

# Authorize with a personal access token.
client.authenticator.set_token(sys.argv[1])
Expand All @@ -17,7 +17,7 @@
# Print zone name and record count.
print('{zone_name} - {record_count} records'.format(
zone_name=zone.attribute('name'),
record_count=len(zone.relationship('records')['data'])
record_count=len(zone.relationship('records'))
))

print('\n')
12 changes: 7 additions & 5 deletions examples/ticket_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
from exonetapi import Client

# Create a new Client.
client = Client('https://api.exonet.nl')
client = Client()

# Authorize with a personal access token.
client.authenticator.set_token(sys.argv[1])

# Get a single ticket resource. Because depending on who is authorized, the ticket IDs change, all tickets are
# retrieved with a limit of 1. From this result, the first ticket is used. In a real world scenario you would
# call something like `ticket = client.resource('tickets').get('VX09kwR3KxNo')` to get a single ticket by it's ID.
'''
Get a single ticket resource. Because depending on who is authorized, the ticket IDs change, all tickets are
retrieved with a limit of 1. From this result, the first ticket is used. In a real world scenario you would call
something like `ticket = client.resource('tickets').get('VX09kwR3KxNo')` to get a single ticket by it's ID.
'''
tickets = client.resource('tickets').size(1).get()

# Show this message when there are no tickets available.
Expand All @@ -33,7 +35,7 @@
)

# Get the emails in the ticket.
emails = client.resource('tickets/{id}/emails'.format(id=ticket.id())).get()
emails = ticket.related('emails').get()

print('This ticket has {mailCount} emails'.format(
mailCount=len(emails)
Expand Down
2 changes: 1 addition & 1 deletion examples/tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from exonetapi import Client

# Create a new Client.
client = Client('https://api.exonet.nl')
client = Client()

# Authorize with a personal access token.
client.authenticator.set_token(sys.argv[1])
Expand Down
29 changes: 20 additions & 9 deletions exonetapi/Client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,31 @@
from urllib.parse import urlparse


class Client:
class Singleton(type):
_instances = {}

def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]


class Client(metaclass=Singleton):
"""The client to interact with the API.
Manages connection details.
"""
# The API hostname.
__host = None

# The URL to use for authentication.
authentication_endpoint = '/oauth/token'

# An instance of the Authenticator that keeps track of the token.
authenticator = None
def __init__(self, host=None):
self.__host = 'https://api.exonet.nl'

def __init__(self, host):
self.set_host(host)
self.authenticator = Authenticator(self.__host, self.authentication_endpoint)
if host:
self.set_host(host)

self.authenticator = Authenticator(self.get_host(), self.authentication_endpoint)

def set_host(self, host):
"""
Expand All @@ -40,10 +48,13 @@ def set_host(self, host):

self.__host = parsed_host.geturl()

def get_host(self):
return self.__host

def resource(self, resource):
"""Prepare a new request to a resource endpoint.
:param resource: The type of resource.
:return: A RequestBuilder to make API calls.
"""
return RequestBuilder(self.__host, self.authenticator).set_resource(resource)
return RequestBuilder(resource, self)
100 changes: 29 additions & 71 deletions exonetapi/RequestBuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,39 @@
Build requests to send to the API.
"""
import requests
from urllib.parse import urlencode

from .result import Parser
from exonetapi.exceptions.ValidationException import ValidationException


class RequestBuilder:
class RequestBuilder(object):
"""Create and make requests to the API.
Takes care of Authentication, accessing resources and related data.
"""
# The API host.
__host = None
# An Authenticator instance to use when making requests to the API.
__authenticator = None

def __init__(self, host, authenticator):
self.__host = host
self.__authenticator = authenticator

# The resource name to access.
self.__resource_name = None
# Optional resource ID.
self.__id = None
# Optional related resources name.
self.__related = None
# The query params that will be used in the GET requests. Can contain filters and page options.
self.__query_params = {}

def set_resource(self, resource_name):
"""Prepare this RequestBuilder to query a specific resource.
def __init__(self, resource, client=None):
if not resource.startswith('/'):
resource = '/' + resource

:param resource_name: The resource type name.
:return: self
self.__resource = resource
"""
self.__resource_name = resource_name
return self

def id(self, identifier):
"""Prepare this RequestBuilder to query an individual resource on the API.
:param identifier: The ID of the resource to access.
:return: self
The query params that will be used in the GET requests.
Can contain filters and page options.
"""
self.__id = identifier
return self
self.__query_params = {}

if client:
self.__client = client
elif not hasattr(self, '__client'):
from exonetapi import Client
self.__client = Client()

def filter(self, filter_name, filter_value):
"""Prepare this RequestBuilder to apply a filter on the next get request.
:param filter_name: The name of the filter to apply.
:param filter_value: The value of the applied filter.
:return: self
"""
self.__query_params['filter['+filter_name+']'] = filter_value
self.__query_params['filter[' + filter_name + ']'] = filter_value
return self

def page(self, page_number):
Expand Down Expand Up @@ -89,45 +68,30 @@ def sort(self, sort_field, sort_order='asc'):
)
return self

def sortAsc(self, sort_field):
def sort_asc(self, sort_field):
"""Prepare this RequestBuilder to sort by a field in ascending order.
:param sort_field: The field name to sort on.
:return: self
"""
return self.sort(sort_field, 'asc')

def sortDesc(self, sort_field):
def sort_desc(self, sort_field):
"""Prepare this RequestBuilder to sort by a field in descending order.
:param sort_field: The field name to sort on.
:return: self
"""
return self.sort(sort_field, 'desc')

def related(self, related):
"""Prepare this RequestBuilder to query related resources on the API.
:param related: The name of the relationship to get resources for.
:return: self
"""
self.__related = related
return self

def get(self, identifier = None):
def get(self, identifier=None):
"""Make a call to the API using the previously set options.
:param: identifier The optional identifier to get.
:return: A Resource or a Collection of Resources.
"""
if not self.__resource_name:
raise ValueError('Setting a resource is required before making a call.')

# Set the resource ID if an identifier was provided.
if identifier:
self.id(identifier)

response = requests.get(
self.__build_url(),
self.__build_url(identifier),
headers=self.__get_headers(),
params=self.__query_params if not self.__id else None
params=self.__query_params if not identifier else None
)

# Raise exception on failed request.
Expand All @@ -136,14 +100,11 @@ def get(self, identifier = None):
return Parser(response.content).parse()

def store(self, resource):
"""Make a POST request to the API with the provided Resource as data.
"""Make a POST request to the API with the provided resource as data.
:param resource: The Resource to use as POST data.
:return: A Resource or a Collection of Resources.
:param resource: The resource to use as POST data.
:return: A resource or a collection of resources.
"""
if not self.__resource_name:
raise ValueError('Setting a resource is required before making a call.')

response = requests.post(
self.__build_url(),
headers=self.__get_headers(),
Expand All @@ -159,18 +120,15 @@ def store(self, resource):

return Parser(response.content).parse()

def __build_url(self):
def __build_url(self, identifier=None):
"""Get the URL to call, based on all previously called setter methods.
:return: A URL.
"""
url = self.__host + '/' + self.__resource_name
url = self.__client.get_host() + self.__resource

if self.__id:
url += '/' + self.__id

if self.__related:
url += '/' + self.__related
if identifier:
url += '/' + identifier

return url

Expand All @@ -182,5 +140,5 @@ def __get_headers(self):
return {
'Accept': 'application/vnd.Exonet.v1+json',
'Content-Type': 'application/json',
'Authorization': 'Bearer %s' % (self.__authenticator.get_token())
'Authorization': 'Bearer %s' % (self.__client.authenticator.get_token())
}
1 change: 1 addition & 0 deletions exonetapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .Client import Client
from .RequestBuilder import RequestBuilder
from .result import Parser
from .create_resource import create_resource
11 changes: 2 additions & 9 deletions exonetapi/auth/Authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,19 @@ class Authenticator:
"""
Manage the authentication and keep track of (valid) tokens.
"""
# The host to connect to when authenticating.
__host = None

# The endpoint on the Host to use when authenticating.
__authentication_endpoint = None

# The obtained authentication details.
__auth_details = None

def __init__(self, host, authentication_endpoint):
self.__host = host
self.__authentication_endpoint = authentication_endpoint
self.__auth_details = None

def get_token(self):
"""Get the obtained authentication token.
:return: The token if available.
"""
if self.__auth_details:
return self.__auth_details['access_token']
return self.__auth_details['access_token'].strip()

def password_auth(self, username, password, client_id, client_secret):
"""Authorize using the password grant.
Expand Down
25 changes: 12 additions & 13 deletions exonetapi/create_resource.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from inflection import camelize
from .result.Resource import Resource

class create_resource:
def create_resource(resource_type, attributes=(), id=None, relationships=None):
"""Create a dynamic Resource based on the type that is provided in the data.
:param resource_type: The resource type.
:param attributes: The json data to construct a Resource.
:param id: The Resource identifier.
:param relationships: The initial relationships for the resource.
:return: A Resource instance.
"""
return type(camelize(resource_type), (Resource,), {})(attributes=attributes, id=id, relationships=relationships)
from .structures.Resource import Resource


def create_resource(resource):
"""Create a dynamic Resource based on the type that is provided in the data.
:param resource: The resource.
:return: A Resource instance.
"""
resource_type = resource['type']

return type(camelize(resource_type), (Resource,), {})(resource)
Loading

0 comments on commit e3184f2

Please sign in to comment.