From 0dcd5b651946de88e94654dc6a54831ae6613850 Mon Sep 17 00:00:00 2001 From: Styxit Date: Fri, 21 Jun 2019 17:13:06 +0200 Subject: [PATCH 01/16] Renew structure --- examples/ticket_details.py | 2 +- exonetapi/Client.py | 25 +++-- exonetapi/RequestBuilder.py | 75 ++++++-------- exonetapi/__init__.py | 1 + exonetapi/auth/Authenticator.py | 12 ++- exonetapi/create_resource.py | 4 +- exonetapi/result/__init__.py | 1 - exonetapi/structures/Relation.py | 74 ++++++++++++++ exonetapi/structures/Resource.py | 99 +++++++++++++++++++ .../ResourceIdentifier.py} | 58 +++-------- exonetapi/structures/__init__.py | 2 + 11 files changed, 249 insertions(+), 104 deletions(-) create mode 100644 exonetapi/structures/Relation.py create mode 100755 exonetapi/structures/Resource.py rename exonetapi/{result/Resource.py => structures/ResourceIdentifier.py} (68%) create mode 100755 exonetapi/structures/__init__.py diff --git a/examples/ticket_details.py b/examples/ticket_details.py index a2393e1..3b60473 100644 --- a/examples/ticket_details.py +++ b/examples/ticket_details.py @@ -33,7 +33,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) diff --git a/exonetapi/Client.py b/exonetapi/Client.py index 16b8b6e..24189df 100755 --- a/exonetapi/Client.py +++ b/exonetapi/Client.py @@ -5,14 +5,21 @@ from .RequestBuilder import RequestBuilder from urllib.parse import urlparse +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: + +class Client(metaclass=Singleton): """The client to interact with the API. Manages connection details. """ - # The API hostname. - __host = None + # The API host. + __host = 'https://api.exonet.nl' # The URL to use for authentication. authentication_endpoint = '/oauth/token' @@ -20,8 +27,11 @@ class Client: # An instance of the Authenticator that keeps track of the token. authenticator = None - def __init__(self, host): - self.set_host(host) + def __init__(self, host=None): + + if host: + self.set_host(host) + self.authenticator = Authenticator(self.__host, self.authentication_endpoint) def set_host(self, host): @@ -40,10 +50,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) diff --git a/exonetapi/RequestBuilder.py b/exonetapi/RequestBuilder.py index 8b86b89..91c613d 100755 --- a/exonetapi/RequestBuilder.py +++ b/exonetapi/RequestBuilder.py @@ -4,41 +4,34 @@ import requests from urllib.parse import urlencode +from exonetapi.auth import Authenticator from .result import Parser from exonetapi.exceptions.ValidationException import ValidationException - -class RequestBuilder: +class RequestBuilder(): +# class RequestBuilder(): """Create and make requests to the API. Takes care of Authentication, accessing resources and related data. """ - # The API host. - __host = None + + # The url to access the resource. + __resource = None # An Authenticator instance to use when making requests to the API. - __authenticator = None + __client = None - def __init__(self, host, authenticator): - self.__host = host - self.__authenticator = authenticator + # The query params that will be used in the GET requests. Can contain filters and page options. + __query_params = {} - # 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 __init__(self, resource, client=None): + self.__resource = resource - def set_resource(self, resource_name): - """Prepare this RequestBuilder to query a specific resource. + if client: + self.__client = client + else: + from exonetapi import Client + self.__client = Client() - :param resource_name: The resource type name. - :return: self - """ - self.__resource_name = resource_name - return self def id(self, identifier): """Prepare this RequestBuilder to query an individual resource on the API. @@ -46,7 +39,10 @@ def id(self, identifier): :param identifier: The ID of the resource to access. :return: self """ - self.__id = identifier + + + # Make ResourceIdentifier + return self def filter(self, filter_name, filter_value): @@ -103,31 +99,19 @@ def sortDesc(self, sort_field): """ 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): """Make a call to the API using the previously set options. :return: A Resource or a Collection of Resources. """ - if not self.__resource_name: + if not self.__resource: 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(), 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. @@ -141,7 +125,7 @@ def store(self, resource): :param resource: The Resource to use as POST data. :return: A Resource or a Collection of Resources. """ - if not self.__resource_name: + if not self.__resource: raise ValueError('Setting a resource is required before making a call.') response = requests.post( @@ -159,18 +143,15 @@ def store(self, resource): return Parser(response.content).parse() - def __build_url(self): + def __build_url(self, id=None): """Get the URL to call, based on all previously called setter methods. :return: A URL. """ - url = self.__host + '/' + self.__resource_name - - if self.__id: - url += '/' + self.__id + url = self.__client.get_host() + '/' + self.__resource - if self.__related: - url += '/' + self.__related + if id: + url += '/' + id return url @@ -182,5 +163,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()) } diff --git a/exonetapi/__init__.py b/exonetapi/__init__.py index 0461a50..5a9a4d0 100755 --- a/exonetapi/__init__.py +++ b/exonetapi/__init__.py @@ -1,3 +1,4 @@ from .Client import Client +from .RequestBuilder import RequestBuilder from .result import Parser from .create_resource import create_resource diff --git a/exonetapi/auth/Authenticator.py b/exonetapi/auth/Authenticator.py index ef68614..43d1148 100755 --- a/exonetapi/auth/Authenticator.py +++ b/exonetapi/auth/Authenticator.py @@ -3,8 +3,16 @@ """ import requests +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 Authenticator: + + +class Authenticator(metaclass=Singleton): """ Manage the authentication and keep track of (valid) tokens. """ @@ -27,7 +35,7 @@ def get_token(self): :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. diff --git a/exonetapi/create_resource.py b/exonetapi/create_resource.py index ce3e26f..9e55440 100755 --- a/exonetapi/create_resource.py +++ b/exonetapi/create_resource.py @@ -1,5 +1,5 @@ from inflection import camelize -from .result.Resource import Resource +from .structures.Resource import Resource class create_resource: def create_resource(resource_type, attributes=(), id=None, relationships=None): @@ -11,4 +11,4 @@ def create_resource(resource_type, attributes=(), id=None, relationships=None): :param relationships: The initial relationships for the resource. :return: A Resource instance. """ - return type(camelize(resource_type), (Resource,), {})(attributes=attributes, id=id, relationships=relationships) + return type(camelize(resource_type), (Resource,), {})(attributes=attributes, id=id) diff --git a/exonetapi/result/__init__.py b/exonetapi/result/__init__.py index 45ffae1..a9bbb26 100755 --- a/exonetapi/result/__init__.py +++ b/exonetapi/result/__init__.py @@ -1,2 +1 @@ from .Parser import Parser -from .Resource import Resource diff --git a/exonetapi/structures/Relation.py b/exonetapi/structures/Relation.py new file mode 100644 index 0000000..6b7dd59 --- /dev/null +++ b/exonetapi/structures/Relation.py @@ -0,0 +1,74 @@ + + + +class Relation(): + + # string Pattern to create the relation url. + __urlPattern = '/%s/%s/%s' + + # string The url for the relation data. + __url = None + + # string The name of the relation. + __name = None + + # Request The prepared request to get the relation data. + __request = None + + # ApiResourceSet|ApiResourceIdentifier The related resource identifier or a ApiResourceSet. + __resourceIdentifiers = None; + + + def __init__(self, relation_name, origin_type, origin_id): + """Relation constructor. + + :param: string $relationName The name of the relation. + :param: string $originType The resource type of the origin resource. + :param: string $originId The resource ID of the origin resource. + """ + self.__name = relation_name + + self.__url = self.__urlPattern % (origin_type, origin_id, relation_name) + + from exonetapi import RequestBuilder + self.__request = RequestBuilder(self.__url) + + + def __getattr__(self, name): + def method(*args): + return getattr(self.__request,name)() + + return method + + # /** + # * Pass unknown calls to the Request instance. + # * + # * @param string $methodName The method to call. + # * @param array $arguments The method arguments. + # * + # * @return Request|ApiResource|ApiResourceSet The request instance or retrieved resource (set). + # */ + # public function __call($methodName, $arguments) + # { + # return call_user_func_array([$this->request, $methodName], $arguments); + # } + + def get_resource_identifiers(self): + """ + Get the resource identifiers for this relation. + + return ApiResourceSet|ApiResourceIdentifier The resource identifier or a resource set. + """ + return self.__resourceIdentifiers + + + def set_resource_identifiers(self, newRelationship): + """ + Replace the related resource identifiers with new data. + + :param ApiResourceSet|ApiResourceIdentifier $newRelationship A new resource identifier or a new resource set. + :return self + """ + self.__resourceIdentifiersresourceIdentifiers = newRelationship + + return self diff --git a/exonetapi/structures/Resource.py b/exonetapi/structures/Resource.py new file mode 100755 index 0000000..01c34f9 --- /dev/null +++ b/exonetapi/structures/Resource.py @@ -0,0 +1,99 @@ +""" +Work with API resources. +""" +from inflection import underscore + +from exonetapi.structures.ResourceIdentifier import ResourceIdentifier + + +class Resource(ResourceIdentifier): + """Basic Resource with attributes. + """ + + def __init__(self, attributes, id=None): + + type = underscore(self.__class__.__name__) + # Call parent init method. + super().__init__(type, id) + + # The Resource attributes. + self.__attributes = attributes` + + + def attribute(self, item): + """Get Resource attributes if available. + + :param item: The name of the Resource attribute. + :return: The attribute or None when attribute does not exist. + """ + return self.__attributes.get(item) + + def attributes(self): + """Get all resource attributes. + :return: All defined attributes in a dict. + """ + return self.__attributes + + def relationship(self, name, *data): + """Define a new relationship for this resource, replace an existing one or get an existing one. + When data is provided the relationship is set, without data the relationship is returned. + + :param name: The name of the relation to set. + :param data: The value of the relation, can be a Resource or a dict of Resources. + :return: self when setting a relationship, or the actual relationship when getting it + """ + if len(data) is 1: + return self.set_relationship(name, data[0]) + + return self.get_relationship(name) + + + def to_json(self): + """Convert a Resource to a dict according to the JSON-API format. + + :return: The dict with attributes according to JSON-API spec. + """ + json = { + 'type': self.type(), + 'attributes': self.attributes(), + + } + + if self.__id: + json['id'] = self.__id + + if self.__relationships: + json['relationships'] = self.get_json_relationships() + + return json + + def to_json_resource_identifier(self): + """Convert a Resource to JSON, including only the type and ID. + + :return: A dict with the Resources type and ID. + """ + return { + 'type': self.type(), + 'id': self.__id, + } + + def get_json_relationships(self): + """Get a dict representing the relations for the resource in JSON-API format. + + :return: A dict with the relationships. + """ + relationships = {} + + for relation_name, relation in self.__relationships.items(): + relationships[relation_name] = {} + if type(relation) is list: + relation_list = [] + for relation_resource in relation: + relation_list.append(relation_resource.to_json_resource_identifier()) + relationships[relation_name]['data'] = relation_list + elif type(relation) is dict: + relationships[relation_name]['data'] = relation['data'] + else: + relationships[relation_name]['data'] = relation.to_json_resource_identifier() + + return relationships diff --git a/exonetapi/result/Resource.py b/exonetapi/structures/ResourceIdentifier.py similarity index 68% rename from exonetapi/result/Resource.py rename to exonetapi/structures/ResourceIdentifier.py index 577e98b..2ded435 100755 --- a/exonetapi/result/Resource.py +++ b/exonetapi/structures/ResourceIdentifier.py @@ -3,39 +3,21 @@ """ from inflection import underscore +from exonetapi.structures.Relation import Relation -class Resource: + +class ResourceIdentifier(): """Basic Resource with attributes. """ - def __init__(self, attributes, id=None, relationships=None): + __relationships = {} + + def __init__(self, type, id=None): # Keep track of the resource type. - self.__type = underscore(self.__class__.__name__) - # The Resource attributes. - self.__attributes = attributes + self.__type = type # Keep track of the resource id. self.__id = id - # The relationships for this resource. - if relationships: - self.__relationships = relationships - else : - self.__relationships = {} - - def attribute(self, item): - """Get Resource attributes if available. - - :param item: The name of the Resource attribute. - :return: The attribute or None when attribute does not exist. - """ - return self.__attributes.get(item) - - def attributes(self): - """Get all resource attributes. - :return: All defined attributes in a dict. - """ - return self.__attributes - def type(self): """Get the resource type of this Resource instance. @@ -50,6 +32,11 @@ def id(self): """ return self.__id + + def related(self, name): + return Relation(name, self.type(), self.id()) + + def relationship(self, name, *data): """Define a new relationship for this resource, replace an existing one or get an existing one. When data is provided the relationship is set, without data the relationship is returned. @@ -89,26 +76,7 @@ def set_relationship(self, name, data): return self def to_json(self): - """Convert a Resource to a dict according to the JSON-API format. - - :return: The dict with attributes according to JSON-API spec. - """ - json = { - 'type': self.type(), - 'attributes': self.attributes(), - - } - - if self.__id: - json['id'] = self.__id - - if self.__relationships: - json['relationships'] = self.get_json_relationships() - - return json - - def to_json_resource_identifier(self): - """Convert a Resource to JSON, including only the type and ID. + """Convert a ResourceIdentifier to JSON. :return: A dict with the Resources type and ID. """ diff --git a/exonetapi/structures/__init__.py b/exonetapi/structures/__init__.py new file mode 100755 index 0000000..4553a32 --- /dev/null +++ b/exonetapi/structures/__init__.py @@ -0,0 +1,2 @@ +from .Resource import Resource +from .ResourceIdentifier import ResourceIdentifier From 1d7998709451a6235952514f3397f1af695ac0d5 Mon Sep 17 00:00:00 2001 From: Styxit Date: Tue, 25 Jun 2019 16:51:34 +0200 Subject: [PATCH 02/16] Refactor in progress --- examples/dns_zone_details.py | 2 +- examples/dns_zones.py | 2 +- exonetapi/Client.py | 3 +- exonetapi/RequestBuilder.py | 53 +++++---------- exonetapi/auth/Authenticator.py | 20 +----- exonetapi/create_resource.py | 18 +++-- exonetapi/result/Parser.py | 70 ++++++++++++++++---- exonetapi/structures/Relation.py | 42 +++--------- exonetapi/structures/Relationship.py | 7 ++ exonetapi/structures/Resource.py | 59 +++-------------- exonetapi/structures/ResourceIdentifier.py | 22 +++--- tests/{result => structures}/testResource.py | 4 +- 12 files changed, 127 insertions(+), 175 deletions(-) create mode 100644 exonetapi/structures/Relationship.py rename tests/{result => structures}/testResource.py (98%) diff --git a/examples/dns_zone_details.py b/examples/dns_zone_details.py index 938874b..82ba0f9 100644 --- a/examples/dns_zone_details.py +++ b/examples/dns_zone_details.py @@ -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: diff --git a/examples/dns_zones.py b/examples/dns_zones.py index e49d29a..039cffa 100644 --- a/examples/dns_zones.py +++ b/examples/dns_zones.py @@ -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') diff --git a/exonetapi/Client.py b/exonetapi/Client.py index 24189df..bfe254f 100755 --- a/exonetapi/Client.py +++ b/exonetapi/Client.py @@ -28,11 +28,10 @@ class Client(metaclass=Singleton): authenticator = None def __init__(self, host=None): - if host: self.set_host(host) - self.authenticator = Authenticator(self.__host, self.authentication_endpoint) + self.authenticator = Authenticator(self.get_host(), self.authentication_endpoint) def set_host(self, host): """ diff --git a/exonetapi/RequestBuilder.py b/exonetapi/RequestBuilder.py index 91c613d..282c739 100755 --- a/exonetapi/RequestBuilder.py +++ b/exonetapi/RequestBuilder.py @@ -2,56 +2,36 @@ Build requests to send to the API. """ import requests -from urllib.parse import urlencode -from exonetapi.auth import Authenticator from .result import Parser from exonetapi.exceptions.ValidationException import ValidationException -class RequestBuilder(): -# class RequestBuilder(): - """Create and make requests to the API. - Takes care of Authentication, accessing resources and related data. +class RequestBuilder(object): + """Create and make requests to the API. """ - - # The url to access the resource. - __resource = None - # An Authenticator instance to use when making requests to the API. __client = None - - # The query params that will be used in the GET requests. Can contain filters and page options. - __query_params = {} - def __init__(self, resource, client=None): + if not resource.startswith('/'): + resource = '/' + resource + self.__resource = resource + # The query params that will be used in the GET requests. Can contain filters and page options. + self.__query_params = {} if client: self.__client = client - else: + elif not self.__client: from exonetapi import Client self.__client = Client() - - 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 - """ - - - # Make ResourceIdentifier - - return self - 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): @@ -85,22 +65,21 @@ 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 get(self, identifier = None): + def get(self, identifier=None): """Make a call to the API using the previously set options. :return: A Resource or a Collection of Resources. @@ -143,15 +122,15 @@ def store(self, resource): return Parser(response.content).parse() - def __build_url(self, id=None): + def __build_url(self, identifier=None): """Get the URL to call, based on all previously called setter methods. :return: A URL. """ - url = self.__client.get_host() + '/' + self.__resource + url = self.__client.get_host() + self.__resource - if id: - url += '/' + id + if identifier: + url += '/' + identifier return url diff --git a/exonetapi/auth/Authenticator.py b/exonetapi/auth/Authenticator.py index 43d1148..8d81009 100755 --- a/exonetapi/auth/Authenticator.py +++ b/exonetapi/auth/Authenticator.py @@ -3,31 +3,15 @@ """ import requests -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 Authenticator(metaclass=Singleton): +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. diff --git a/exonetapi/create_resource.py b/exonetapi/create_resource.py index 9e55440..a68cb84 100755 --- a/exonetapi/create_resource.py +++ b/exonetapi/create_resource.py @@ -1,14 +1,12 @@ from inflection import camelize from .structures.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. +def create_resource(resource): + """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) + :param resource: The resource. + :return: A Resource instance. + """ + resource_type = resource['type'] + + return type(camelize(resource_type), (Resource,), {})(resource) diff --git a/exonetapi/result/Parser.py b/exonetapi/result/Parser.py index 2ecf7c6..7a46b24 100755 --- a/exonetapi/result/Parser.py +++ b/exonetapi/result/Parser.py @@ -3,6 +3,8 @@ """ import json from exonetapi.create_resource import create_resource +from exonetapi.structures import ResourceIdentifier +from exonetapi.structures.Relationship import Relationship class Parser: @@ -22,18 +24,62 @@ def parse(self): if type(self.__json_data) is list: resources = [] for resource_data in self.__json_data: - resources.append(create_resource.create_resource( - resource_data.get('type'), - resource_data.get('attributes', {}), - resource_data.get('id'), - resource_data.get('relationships', {}) - )) + resources.append(self.make_resource(resource_data)) return resources else: - return create_resource.create_resource( - self.__json_data.get('type'), - self.__json_data.get('attributes', {}), - self.__json_data.get('id'), - self.__json_data.get('relationships', {}) - ) + return self.make_resource(self.__json_data) + + + def make_resource(self, resource_data): + resource = create_resource({ + 'type': resource_data['type'], + 'id': resource_data['id'] + }) + + # Set attributes. + if 'attributes' in resource_data.keys(): + for attribute_name, attribute_value in resource_data['attributes'].items(): + resource.attribute(attribute_name, attribute_value) + + # Extract and parse all included relations. + if 'relationships' in resource_data.keys(): + parsedRelations = self.parse_relations(resource_data['relationships'], resource.type(), resource.id()) + + for k, r in parsedRelations.items(): + resource.set_relationship(k, r) + + return resource + + + def parse_relations(self, relationships, origin_type, origin_id): + parsedRelations = {} + + if relationships: + for relationName, relation in relationships.items(): + # set a relation + if ('data' in relation.keys()) and relation['data']: + relationship = Relationship(relationName, origin_type, origin_id) + + # Single. + if 'type' in relation['data']: + relationship.set_resource_identifiers( + ResourceIdentifier(relation['data']['type'], relation['data']['id']) + ) + + elif isinstance(relation['data'], list): + # Multi. + relationships = [] + for relationItem in relation['data'] : + if 'attributes' in relationItem: + relationships.append( + create_resource(relationItem) + ) + else: + relationships.append(ResourceIdentifier(relationItem['type'], relationItem['id'])) + + relationship.set_resource_identifiers(relationships) + + parsedRelations[relationName] = relationship + + return parsedRelations diff --git a/exonetapi/structures/Relation.py b/exonetapi/structures/Relation.py index 6b7dd59..6285203 100644 --- a/exonetapi/structures/Relation.py +++ b/exonetapi/structures/Relation.py @@ -1,24 +1,8 @@ - - -class Relation(): - +class Relation(object): # string Pattern to create the relation url. __urlPattern = '/%s/%s/%s' - # string The url for the relation data. - __url = None - - # string The name of the relation. - __name = None - - # Request The prepared request to get the relation data. - __request = None - - # ApiResourceSet|ApiResourceIdentifier The related resource identifier or a ApiResourceSet. - __resourceIdentifiers = None; - - def __init__(self, relation_name, origin_type, origin_id): """Relation constructor. @@ -27,12 +11,19 @@ def __init__(self, relation_name, origin_type, origin_id): :param: string $originId The resource ID of the origin resource. """ self.__name = relation_name - self.__url = self.__urlPattern % (origin_type, origin_id, relation_name) + # ApiResourceSet|ApiResourceIdentifier The related resource identifier or a ApiResourceSet. + __resourceIdentifiers = None + from exonetapi import RequestBuilder self.__request = RequestBuilder(self.__url) + def __len__(self): + if self.__resourceIdentifiers: + return len(self.__resourceIdentifiers) + + return 0 def __getattr__(self, name): def method(*args): @@ -40,19 +31,6 @@ def method(*args): return method - # /** - # * Pass unknown calls to the Request instance. - # * - # * @param string $methodName The method to call. - # * @param array $arguments The method arguments. - # * - # * @return Request|ApiResource|ApiResourceSet The request instance or retrieved resource (set). - # */ - # public function __call($methodName, $arguments) - # { - # return call_user_func_array([$this->request, $methodName], $arguments); - # } - def get_resource_identifiers(self): """ Get the resource identifiers for this relation. @@ -69,6 +47,6 @@ def set_resource_identifiers(self, newRelationship): :param ApiResourceSet|ApiResourceIdentifier $newRelationship A new resource identifier or a new resource set. :return self """ - self.__resourceIdentifiersresourceIdentifiers = newRelationship + self.__resourceIdentifiers = newRelationship return self diff --git a/exonetapi/structures/Relationship.py b/exonetapi/structures/Relationship.py new file mode 100644 index 0000000..857098a --- /dev/null +++ b/exonetapi/structures/Relationship.py @@ -0,0 +1,7 @@ +from exonetapi.structures.Relation import Relation + + +class Relationship(Relation): + + # string Pattern to create the relationship url. + __urlPattern = '/%s/%s/relationships/%s' diff --git a/exonetapi/structures/Resource.py b/exonetapi/structures/Resource.py index 01c34f9..746cd8e 100755 --- a/exonetapi/structures/Resource.py +++ b/exonetapi/structures/Resource.py @@ -1,7 +1,6 @@ """ Work with API resources. """ -from inflection import underscore from exonetapi.structures.ResourceIdentifier import ResourceIdentifier @@ -9,23 +8,21 @@ class Resource(ResourceIdentifier): """Basic Resource with attributes. """ - - def __init__(self, attributes, id=None): - - type = underscore(self.__class__.__name__) + def __init__(self, data): # Call parent init method. - super().__init__(type, id) - - # The Resource attributes. - self.__attributes = attributes` + super().__init__(data['type'], data['id']) + self.__attributes = {} - def attribute(self, item): + def attribute(self, item, value=None): """Get Resource attributes if available. :param item: The name of the Resource attribute. :return: The attribute or None when attribute does not exist. """ + if value: + self.__attributes[item] = value + return self.__attributes.get(item) def attributes(self): @@ -34,19 +31,6 @@ def attributes(self): """ return self.__attributes - def relationship(self, name, *data): - """Define a new relationship for this resource, replace an existing one or get an existing one. - When data is provided the relationship is set, without data the relationship is returned. - - :param name: The name of the relation to set. - :param data: The value of the relation, can be a Resource or a dict of Resources. - :return: self when setting a relationship, or the actual relationship when getting it - """ - if len(data) is 1: - return self.set_relationship(name, data[0]) - - return self.get_relationship(name) - def to_json(self): """Convert a Resource to a dict according to the JSON-API format. @@ -59,8 +43,8 @@ def to_json(self): } - if self.__id: - json['id'] = self.__id + if self.id(): + json['id'] = self.id() if self.__relationships: json['relationships'] = self.get_json_relationships() @@ -72,28 +56,7 @@ def to_json_resource_identifier(self): :return: A dict with the Resources type and ID. """ - return { - 'type': self.type(), - 'id': self.__id, - } - def get_json_relationships(self): - """Get a dict representing the relations for the resource in JSON-API format. + return super().to_json() + - :return: A dict with the relationships. - """ - relationships = {} - - for relation_name, relation in self.__relationships.items(): - relationships[relation_name] = {} - if type(relation) is list: - relation_list = [] - for relation_resource in relation: - relation_list.append(relation_resource.to_json_resource_identifier()) - relationships[relation_name]['data'] = relation_list - elif type(relation) is dict: - relationships[relation_name]['data'] = relation['data'] - else: - relationships[relation_name]['data'] = relation.to_json_resource_identifier() - - return relationships diff --git a/exonetapi/structures/ResourceIdentifier.py b/exonetapi/structures/ResourceIdentifier.py index 2ded435..ba09b87 100755 --- a/exonetapi/structures/ResourceIdentifier.py +++ b/exonetapi/structures/ResourceIdentifier.py @@ -1,23 +1,22 @@ """ Work with API resources. """ -from inflection import underscore - from exonetapi.structures.Relation import Relation +from exonetapi.structures.Relationship import Relationship -class ResourceIdentifier(): - """Basic Resource with attributes. +class ResourceIdentifier(object): + """Basic Resource identifier. """ - - __relationships = {} - def __init__(self, type, id=None): # Keep track of the resource type. self.__type = type # Keep track of the resource id. self.__id = id + self.__relationships = {} + + def type(self): """Get the resource type of this Resource instance. @@ -56,11 +55,10 @@ def get_relationship(self, name): :param name: The name of the relation to get. :return: The defined relation or None """ + if not name in self.__relationships.keys(): + self.__relationships[name] = Relationship(name, self.type(), self.id()) - if name in self.__relationships: - return self.__relationships[name] - - return None + return self.__relationships[name] def set_relationship(self, name, data): """Define a new relationship for this resource or replace an existing one. @@ -82,7 +80,7 @@ def to_json(self): """ return { 'type': self.type(), - 'id': self.__id, + 'id': self.id(), } def get_json_relationships(self): diff --git a/tests/result/testResource.py b/tests/structures/testResource.py similarity index 98% rename from tests/result/testResource.py rename to tests/structures/testResource.py index 76fde84..d8f9118 100755 --- a/tests/result/testResource.py +++ b/tests/structures/testResource.py @@ -4,9 +4,9 @@ from exonetapi.RequestBuilder import RequestBuilder from exonetapi.auth.Authenticator import Authenticator -from exonetapi.result.Resource import Resource +from exonetapi.structures.Resource import Resource from exonetapi.exceptions.ValidationException import ValidationException -from exonetapi.create_resource import create_resource +from exonetapi import create_resource import json From d589d412c64e396d59f22ab7e9db8ecedf0f27ba Mon Sep 17 00:00:00 2001 From: Styxit Date: Thu, 1 Aug 2019 13:41:39 +0200 Subject: [PATCH 03/16] Update package versions --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 357ea9f..b160ea7 100755 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,5 @@ chardet==3.0.4 coverage==4.5.1 idna==2.6 inflection==0.3.1 -requests==2.18.4 -urllib3==1.22 +requests==2.20.0 +urllib3==1.24.2 From 7931ad0c282724e2e391036e12f530bdaa17605c Mon Sep 17 00:00:00 2001 From: Styxit Date: Thu, 1 Aug 2019 13:42:01 +0200 Subject: [PATCH 04/16] Update client --- exonetapi/Client.py | 7 ++----- tests/testClient.py | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/exonetapi/Client.py b/exonetapi/Client.py index bfe254f..895abed 100755 --- a/exonetapi/Client.py +++ b/exonetapi/Client.py @@ -18,16 +18,13 @@ class Client(metaclass=Singleton): Manages connection details. """ - # The API host. - __host = 'https://api.exonet.nl' # 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' + if host: self.set_host(host) diff --git a/tests/testClient.py b/tests/testClient.py index 901cf49..5dd39d3 100755 --- a/tests/testClient.py +++ b/tests/testClient.py @@ -11,13 +11,13 @@ class testClient(unittest.TestCase): def test_init_arguments(self, mock_authenticator): client = Client('https://test.url') - self.assertEqual(client._Client__host, 'https://test.url') + self.assertEqual(client.get_host(), 'https://test.url') mock_authenticator.assert_called_with('https://test.url', '/oauth/token') def test_set_host(self): client = Client('https://test.url') client.set_host('http://new.host') - self.assertEqual(client._Client__host, 'http://new.host') + self.assertEqual(client.get_host(), 'http://new.host') def test_set_host_invalid_protocol(self): client = Client('https://test.url') From e663435c4203b58816d099fdc0fb0e9186cd149d Mon Sep 17 00:00:00 2001 From: Styxit Date: Thu, 1 Aug 2019 15:05:18 +0200 Subject: [PATCH 05/16] test request builder --- exonetapi/RequestBuilder.py | 9 ++--- tests/testRequestBuilder.py | 76 ++++++++++++++----------------------- 2 files changed, 31 insertions(+), 54 deletions(-) diff --git a/exonetapi/RequestBuilder.py b/exonetapi/RequestBuilder.py index 282c739..8fd1fe3 100755 --- a/exonetapi/RequestBuilder.py +++ b/exonetapi/RequestBuilder.py @@ -10,7 +10,7 @@ class RequestBuilder(object): """Create and make requests to the API. """ - __client = None + def __init__(self, resource, client=None): if not resource.startswith('/'): resource = '/' + resource @@ -21,7 +21,7 @@ def __init__(self, resource, client=None): if client: self.__client = client - elif not self.__client: + elif not hasattr(self, '__client'): from exonetapi import Client self.__client = Client() @@ -88,7 +88,7 @@ def get(self, identifier=None): raise ValueError('Setting a resource is required before making a call.') response = requests.get( - self.__build_url(), + self.__build_url(identifier), headers=self.__get_headers(), params=self.__query_params if not identifier else None ) @@ -104,9 +104,6 @@ def store(self, resource): :param resource: The Resource to use as POST data. :return: A Resource or a Collection of Resources. """ - if not self.__resource: - raise ValueError('Setting a resource is required before making a call.') - response = requests.post( self.__build_url(), headers=self.__get_headers(), diff --git a/tests/testRequestBuilder.py b/tests/testRequestBuilder.py index d444495..e0a8425 100755 --- a/tests/testRequestBuilder.py +++ b/tests/testRequestBuilder.py @@ -2,17 +2,21 @@ from unittest.mock import MagicMock from unittest import mock +from exonetapi import Client from exonetapi.RequestBuilder import RequestBuilder from exonetapi.auth.Authenticator import Authenticator -from exonetapi.result.Resource import Resource +from exonetapi.structures.Resource import Resource from exonetapi.exceptions.ValidationException import ValidationException class testRequestBuilder(unittest.TestCase): - authenticator = Authenticator('https://test.url', '/auth/token') - authenticator.get_token = MagicMock(return_value='test_token') + def setUp(self): + client = Client('https://test.url') + self.request_builder = RequestBuilder('things', client) + + def tearDown(self): + self.request_builder = None - request_builder = RequestBuilder('https://test.url', authenticator) class MockResponse: def __init__(self, content, status_code=200): @@ -23,16 +27,7 @@ def raise_for_status(self): return None def test_init_arguments(self): - self.assertEqual(self.request_builder._RequestBuilder__host, 'https://test.url') - self.assertEqual(self.request_builder._RequestBuilder__authenticator, self.authenticator) - - def test_set_resource(self): - self.request_builder.set_resource('/test') - self.assertEqual(self.request_builder._RequestBuilder__resource_name, '/test') - - def test_id(self): - self.request_builder.id('testId') - self.assertEqual(self.request_builder._RequestBuilder__id, 'testId') + self.assertEqual(self.request_builder._RequestBuilder__resource, '/things') def test_filter(self): self.request_builder.filter('firstFilterName', 'firstFilterValue') @@ -55,26 +50,17 @@ def test_sort_default(self): self.assertEqual(self.request_builder._RequestBuilder__query_params['sort'], 'domain') def test_sortAsc(self): - self.request_builder.sortAsc('domain') + self.request_builder.sort_asc('domain') self.assertEqual(self.request_builder._RequestBuilder__query_params['sort'], 'domain') def test_sortDesc(self): - self.request_builder.sortDesc('domain') + self.request_builder.sort_desc('domain') self.assertEqual(self.request_builder._RequestBuilder__query_params['sort'], '-domain') - def test_related(self): - self.request_builder.related('relatedResource') - self.assertEqual(self.request_builder._RequestBuilder__related, 'relatedResource') + @mock.patch('exonetapi.auth.Authenticator.get_token') + def test_get_headers(self, mock_authenticator_get_token): + mock_authenticator_get_token.return_value = 'test_token' - def test_build_url(self): - self.request_builder.set_resource('testResource') - self.request_builder.id('testId') - self.request_builder.related('relatedResource') - - url = self.request_builder._RequestBuilder__build_url() - self.assertEqual(url, 'https://test.url/testResource/testId/relatedResource') - - def test_get_headers(self): headers = self.request_builder._RequestBuilder__get_headers() self.assertEqual(headers, { 'Accept': 'application/vnd.Exonet.v1+json', @@ -90,14 +76,14 @@ def test_get(self, mock_requests_get, mock_parser_init, mock_parser_parse): mock_parser_init.return_value = None mock_requests_get.return_value = self.MockResponse('{"data": "getReturnData"}') - result = self.request_builder.set_resource('test').related(None).id(None).get('testId') + result = self.request_builder.get('testId') mock_requests_get.assert_called_with( - 'https://test.url/test/testId', + 'https://test.url/things/testId', headers={ 'Accept': 'application/vnd.Exonet.v1+json', 'Content-Type': 'application/json', - 'Authorization': 'Bearer test_token' + 'Authorization': 'Bearer None' }, params=None) @@ -107,59 +93,53 @@ def test_get(self, mock_requests_get, mock_parser_init, mock_parser_parse): self.assertEqual('parsedReturnValue', result) def test_get_without_resource_name(self): - self.request_builder.set_resource(None).related(None).id(None) self.assertRaises(ValueError, self.request_builder.get) @mock.patch('exonetapi.result.Parser.parse') @mock.patch('exonetapi.result.Parser.__init__') @mock.patch('requests.post') def test_store(self, mock_requests_get, mock_parser_init, mock_parser_parse): - resource = Resource('{"name": "test"}') - resource.to_json = MagicMock(return_value={"name": "test"}) + resource = Resource({'type': 'things', 'id': 'someId'}) + resource.to_json = MagicMock(return_value={'name': 'my_name'}) mock_parser_parse.return_value = 'parsedReturnValue' mock_parser_init.return_value = None mock_requests_get.return_value = self.MockResponse('{"data": "getReturnData"}') - result = self.request_builder.set_resource('test').related(None).id(None).store(resource) + result = self.request_builder.store(resource) mock_requests_get.assert_called_with( - 'https://test.url/test', + 'https://test.url/things', headers={ 'Accept': 'application/vnd.Exonet.v1+json', 'Content-Type': 'application/json', - 'Authorization': 'Bearer test_token' + 'Authorization': 'Bearer None' }, - json={'data': {'name': 'test'}}) + json={'data': {'name': 'my_name'}}) mock_parser_init.assert_called_with('{"data": "getReturnData"}') self.assertTrue(mock_parser_parse.called) self.assertEqual('parsedReturnValue', result) - def test_store_without_resource_name(self): - resource = Resource('{"name": "test"}') - self.request_builder.set_resource(None).related(None).id(None) - self.assertRaises(ValueError, self.request_builder.store, resource) - @mock.patch('requests.post') @mock.patch('exonetapi.exceptions.ValidationException.__init__', return_value=None) def test_store_validation_error(self, mock_validation_exception, mock_requests_get): - resource = Resource('{"name": "test"}') - resource.to_json = MagicMock(return_value={"name": "test"}) + resource = Resource({'type': 'things', 'id': 'someId'}) + resource.to_json = MagicMock(return_value={'name': 'my_name'}) mock_requests_get.return_value = self.MockResponse('{"data": "getReturnData"}', 422) self.assertRaises(ValidationException, self.request_builder.store, resource) mock_requests_get.assert_called_with( - 'https://test.url/test', + 'https://test.url/things', headers={ 'Accept': 'application/vnd.Exonet.v1+json', 'Content-Type': 'application/json', - 'Authorization': 'Bearer test_token' + 'Authorization': 'Bearer None' }, - json={'data': {'name': 'test'}}) + json={'data': {'name': 'my_name'}}) if __name__ == '__main__': From abe7f2d0cb51689cdbaf7a30544303bdfa7fee0e Mon Sep 17 00:00:00 2001 From: Styxit Date: Thu, 1 Aug 2019 15:06:50 +0200 Subject: [PATCH 06/16] test parser --- exonetapi/result/Parser.py | 5 +-- tests/result/testParser.py | 73 ++++++++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 25 deletions(-) diff --git a/exonetapi/result/Parser.py b/exonetapi/result/Parser.py index 7a46b24..7f80b7d 100755 --- a/exonetapi/result/Parser.py +++ b/exonetapi/result/Parser.py @@ -67,8 +67,9 @@ def parse_relations(self, relationships, origin_type, origin_id): ResourceIdentifier(relation['data']['type'], relation['data']['id']) ) + # Multi. elif isinstance(relation['data'], list): - # Multi. + relationships = [] for relationItem in relation['data'] : if 'attributes' in relationItem: @@ -80,6 +81,6 @@ def parse_relations(self, relationships, origin_type, origin_id): relationship.set_resource_identifiers(relationships) - parsedRelations[relationName] = relationship + parsedRelations[relationName] = relationship return parsedRelations diff --git a/tests/result/testParser.py b/tests/result/testParser.py index 564dad2..bc9ebd5 100755 --- a/tests/result/testParser.py +++ b/tests/result/testParser.py @@ -2,12 +2,11 @@ from unittest import mock from unittest.mock import call -from exonetapi.result.Parser import Parser +from exonetapi.result import Parser class testParser(unittest.TestCase): - @mock.patch('exonetapi.create_resource.create_resource', create=True) - def test_parse_list(self, mock_create_resource): + def test_parse_list(self): json_data_list = """ { "data": [ @@ -53,21 +52,15 @@ def test_parse_list(self, mock_create_resource): } """ - Parser(json_data_list).parse() + result = Parser(json_data_list).parse() - mock_create_resource.assert_has_calls([ - call('comments', {'subject': 'Can you help me?'}, 'DV6axK4GwNEb', {'author': { - 'links': {'self': 'https://api.exonet.nl/comments/DV6axK4GwNEb/relationships/author', - 'related': 'https://api.exonet.nl/comments/DV6axK4GwNEb/author'}, - 'data': {'type': 'employees', 'id': 'ypPe9wqp7gxb'}}}), - call('comments', {'subject': 'Yes I can!'}, 'zWX9r7exA28G', {'author': { - 'links': {'self': 'https://api.exonet.nl/comments/zWX9r7exA28G/relationships/author', - 'related': 'https://api.exonet.nl/comments/zWX9r7exA28G/author'}, - 'data': {'type': 'employees', 'id': 'dbJEx7go7WN0'}}}) - ]) + self.assertEqual(result[0].id(), 'DV6axK4GwNEb') + self.assertEqual(result[0].type(), 'comments') - @mock.patch('exonetapi.create_resource.create_resource', create=True) - def test_parse_single(self, mock_create_resource): + self.assertEqual(result[1].id(), 'zWX9r7exA28G') + self.assertEqual(result[1].type(), 'comments') + + def test_parse_single(self): json_data_list = """ { "data": @@ -93,14 +86,48 @@ def test_parse_single(self, mock_create_resource): } """ - Parser(json_data_list).parse() + result = Parser(json_data_list).parse() + + self.assertEqual(result.id(), 'DV6axK4GwNEb') + self.assertEqual(result.type(), 'comments') + + def test_parse_single_with_multi_relation(self): + json_data_list = """ + { + "data": + { + "type": "comments", + "id": "DV6axK4GwNEb", + "attributes": { + "subject": "Can you help me?" + }, + "relationships": { + "tags": { + "links": { + "self": "https://api.exonet.nl/comments/DV6axK4GwNEb/relationships/tags", + "related": "https://api.exonet.nl/comments/DV6axK4GwNEb/tags" + }, + "data": [ + { + "type": "tags", + "id": "ABC" + }, + { + "type": "tags", + "id": "XYZ" + } + + ] + } + } + } + } + """ + + result = Parser(json_data_list).parse().relationship('tags').get_resource_identifiers() + + self.assertEqual(len(result), 2) - mock_create_resource.assert_has_calls([ - call('comments', {'subject': 'Can you help me?'}, 'DV6axK4GwNEb', {'author': { - 'links': {'self': 'https://api.exonet.nl/comments/DV6axK4GwNEb/relationships/author', - 'related': 'https://api.exonet.nl/comments/DV6axK4GwNEb/author'}, - 'data': {'type': 'employees', 'id': 'ypPe9wqp7gxb'}}}), - ]) if __name__ == '__main__': From 22a4d1b53c980703b3269bf0be90de056f1f4172 Mon Sep 17 00:00:00 2001 From: Styxit Date: Thu, 1 Aug 2019 15:22:51 +0200 Subject: [PATCH 07/16] test create_resource --- exonetapi/structures/Resource.py | 5 ++++- tests/test_create_resource.py | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/exonetapi/structures/Resource.py b/exonetapi/structures/Resource.py index 746cd8e..b035bff 100755 --- a/exonetapi/structures/Resource.py +++ b/exonetapi/structures/Resource.py @@ -10,7 +10,10 @@ class Resource(ResourceIdentifier): """ def __init__(self, data): # Call parent init method. - super().__init__(data['type'], data['id']) + super().__init__( + data['type'], + data['id'] if 'id' in data else None + ) self.__attributes = {} diff --git a/tests/test_create_resource.py b/tests/test_create_resource.py index 1e40a4f..6a9dc71 100755 --- a/tests/test_create_resource.py +++ b/tests/test_create_resource.py @@ -3,9 +3,13 @@ from exonetapi.create_resource import create_resource -class testClient(unittest.TestCase): +class test_create_resource(unittest.TestCase): def test_create_resource(self): - resource = create_resource.create_resource('test_resource') + resource_data = { + 'type': 'test_resource' + } + + resource = create_resource(resource_data) self.assertEqual(resource.__class__.__name__, 'TestResource') From 4615185a84fae5b20ff43b54811dc3d46686c37a Mon Sep 17 00:00:00 2001 From: Styxit Date: Thu, 8 Aug 2019 11:30:18 +0200 Subject: [PATCH 08/16] Create base testcase that destroys singletons --- tests/auth/testAuthenticator.py | 3 ++- tests/exceptions/testValidationException.py | 3 ++- tests/result/testParser.py | 3 ++- tests/structures/testResource.py | 16 +++------------- tests/testCase.py | 8 ++++++++ tests/testClient.py | 3 ++- tests/testRequestBuilder.py | 8 ++++---- tests/test_create_resource.py | 4 +++- 8 files changed, 26 insertions(+), 22 deletions(-) create mode 100755 tests/testCase.py diff --git a/tests/auth/testAuthenticator.py b/tests/auth/testAuthenticator.py index 643e627..a643293 100755 --- a/tests/auth/testAuthenticator.py +++ b/tests/auth/testAuthenticator.py @@ -2,10 +2,11 @@ from unittest import mock from unittest.mock import MagicMock +from testCase import testCase from exonetapi.auth import Authenticator -class testAuthenticator(unittest.TestCase): +class testAuthenticator(testCase): class MockResponse: def __init__(self, content, status_code=200): self.content = content diff --git a/tests/exceptions/testValidationException.py b/tests/exceptions/testValidationException.py index 1e63bc1..134501f 100755 --- a/tests/exceptions/testValidationException.py +++ b/tests/exceptions/testValidationException.py @@ -3,10 +3,11 @@ from requests import Response +from testCase import testCase from exonetapi.exceptions import ValidationException -class testValidationException(unittest.TestCase): +class testValidationException(testCase): def test_no_errors(self): # Construct the request response. response = create_autospec(Response, spec_set=True) diff --git a/tests/result/testParser.py b/tests/result/testParser.py index bc9ebd5..0004aab 100755 --- a/tests/result/testParser.py +++ b/tests/result/testParser.py @@ -2,10 +2,11 @@ from unittest import mock from unittest.mock import call +from testCase import testCase from exonetapi.result import Parser -class testParser(unittest.TestCase): +class testParser(testCase): def test_parse_list(self): json_data_list = """ { diff --git a/tests/structures/testResource.py b/tests/structures/testResource.py index d8f9118..8e1d381 100755 --- a/tests/structures/testResource.py +++ b/tests/structures/testResource.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock from unittest import mock +from testCase import testCase + from exonetapi.RequestBuilder import RequestBuilder from exonetapi.auth.Authenticator import Authenticator from exonetapi.structures.Resource import Resource @@ -11,19 +13,7 @@ import json -class testResource(unittest.TestCase): - # authenticator = Authenticator('https://test.url', '/auth/token') - # authenticator.get_token = MagicMock(return_value='test_token') - # - # request_builder = RequestBuilder('https://test.url', authenticator) - # - # class MockResponse: - # def __init__(self, content, status_code=200): - # self.content = content - # self.status_code = status_code - # - # def raise_for_status(self): - # return None +class testResource(testCase): def test_init(self): resource = create_resource.create_resource( diff --git a/tests/testCase.py b/tests/testCase.py new file mode 100755 index 0000000..4768fff --- /dev/null +++ b/tests/testCase.py @@ -0,0 +1,8 @@ +import unittest +from exonetapi.Client import Singleton + +class testCase(unittest.TestCase): + + def setUp(self): + # Reset Client singleton. + Singleton._instances = {} diff --git a/tests/testClient.py b/tests/testClient.py index 5dd39d3..dcfd0df 100755 --- a/tests/testClient.py +++ b/tests/testClient.py @@ -1,11 +1,12 @@ import unittest from unittest import mock +from testCase import testCase from exonetapi.Client import Client from exonetapi.RequestBuilder import RequestBuilder -class testClient(unittest.TestCase): +class testClient(testCase): @mock.patch('exonetapi.auth.Authenticator.__init__', return_value=None) def test_init_arguments(self, mock_authenticator): diff --git a/tests/testRequestBuilder.py b/tests/testRequestBuilder.py index e0a8425..d8a4c07 100755 --- a/tests/testRequestBuilder.py +++ b/tests/testRequestBuilder.py @@ -2,6 +2,7 @@ from unittest.mock import MagicMock from unittest import mock +from testCase import testCase from exonetapi import Client from exonetapi.RequestBuilder import RequestBuilder from exonetapi.auth.Authenticator import Authenticator @@ -9,12 +10,14 @@ from exonetapi.exceptions.ValidationException import ValidationException -class testRequestBuilder(unittest.TestCase): +class testRequestBuilder(testCase): def setUp(self): + super().setUp() client = Client('https://test.url') self.request_builder = RequestBuilder('things', client) def tearDown(self): + super().tearDown() self.request_builder = None @@ -92,9 +95,6 @@ def test_get(self, mock_requests_get, mock_parser_init, mock_parser_parse): self.assertTrue(mock_parser_parse.called) self.assertEqual('parsedReturnValue', result) - def test_get_without_resource_name(self): - self.assertRaises(ValueError, self.request_builder.get) - @mock.patch('exonetapi.result.Parser.parse') @mock.patch('exonetapi.result.Parser.__init__') @mock.patch('requests.post') diff --git a/tests/test_create_resource.py b/tests/test_create_resource.py index 6a9dc71..37f29ce 100755 --- a/tests/test_create_resource.py +++ b/tests/test_create_resource.py @@ -1,9 +1,11 @@ import unittest +from testCase import testCase + from exonetapi.create_resource import create_resource -class test_create_resource(unittest.TestCase): +class test_create_resource(testCase): def test_create_resource(self): resource_data = { 'type': 'test_resource' From b4c9f0692730e6e683c167feca180fefb9da1889 Mon Sep 17 00:00:00 2001 From: Styxit Date: Thu, 8 Aug 2019 11:45:24 +0200 Subject: [PATCH 09/16] testcase import --- tests/auth/testAuthenticator.py | 2 +- tests/exceptions/testValidationException.py | 2 +- tests/result/testParser.py | 2 +- tests/structures/testResource.py | 2 +- tests/testClient.py | 2 +- tests/testRequestBuilder.py | 2 +- tests/test_create_resource.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/auth/testAuthenticator.py b/tests/auth/testAuthenticator.py index a643293..fec2c07 100755 --- a/tests/auth/testAuthenticator.py +++ b/tests/auth/testAuthenticator.py @@ -2,7 +2,7 @@ from unittest import mock from unittest.mock import MagicMock -from testCase import testCase +from tests.testCase import testCase from exonetapi.auth import Authenticator diff --git a/tests/exceptions/testValidationException.py b/tests/exceptions/testValidationException.py index 134501f..d696b8a 100755 --- a/tests/exceptions/testValidationException.py +++ b/tests/exceptions/testValidationException.py @@ -3,7 +3,7 @@ from requests import Response -from testCase import testCase +from tests.testCase import testCase from exonetapi.exceptions import ValidationException diff --git a/tests/result/testParser.py b/tests/result/testParser.py index 0004aab..b3ade83 100755 --- a/tests/result/testParser.py +++ b/tests/result/testParser.py @@ -2,7 +2,7 @@ from unittest import mock from unittest.mock import call -from testCase import testCase +from tests.testCase import testCase from exonetapi.result import Parser diff --git a/tests/structures/testResource.py b/tests/structures/testResource.py index 8e1d381..9419f11 100755 --- a/tests/structures/testResource.py +++ b/tests/structures/testResource.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock from unittest import mock -from testCase import testCase +from tests.testCase import testCase from exonetapi.RequestBuilder import RequestBuilder from exonetapi.auth.Authenticator import Authenticator diff --git a/tests/testClient.py b/tests/testClient.py index dcfd0df..9f75fc9 100755 --- a/tests/testClient.py +++ b/tests/testClient.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from testCase import testCase +from tests.testCase import testCase from exonetapi.Client import Client from exonetapi.RequestBuilder import RequestBuilder diff --git a/tests/testRequestBuilder.py b/tests/testRequestBuilder.py index d8a4c07..63ee11e 100755 --- a/tests/testRequestBuilder.py +++ b/tests/testRequestBuilder.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock from unittest import mock -from testCase import testCase +from tests.testCase import testCase from exonetapi import Client from exonetapi.RequestBuilder import RequestBuilder from exonetapi.auth.Authenticator import Authenticator diff --git a/tests/test_create_resource.py b/tests/test_create_resource.py index 37f29ce..5fbbee2 100755 --- a/tests/test_create_resource.py +++ b/tests/test_create_resource.py @@ -1,6 +1,6 @@ import unittest -from testCase import testCase +from tests.testCase import testCase from exonetapi.create_resource import create_resource From 89f032315d407ed7778e87b6a9a42e2f47e8a1ab Mon Sep 17 00:00:00 2001 From: Styxit Date: Thu, 8 Aug 2019 16:37:29 +0200 Subject: [PATCH 10/16] Unittests --- exonetapi/RequestBuilder.py | 2 - exonetapi/result/Parser.py | 5 - exonetapi/structures/Relation.py | 2 +- exonetapi/structures/Resource.py | 6 +- exonetapi/structures/ResourceIdentifier.py | 6 + tests/result/testParser.py | 37 +++++ tests/structures/__init__.py | 0 tests/structures/testRelation.py | 41 ++++++ tests/structures/testResource.py | 157 +++++---------------- tests/structures/testResourceIdentifier.py | 129 +++++++++++++++++ tests/testRequestBuilder.py | 3 + 11 files changed, 259 insertions(+), 129 deletions(-) create mode 100755 tests/structures/__init__.py create mode 100755 tests/structures/testRelation.py create mode 100755 tests/structures/testResourceIdentifier.py diff --git a/exonetapi/RequestBuilder.py b/exonetapi/RequestBuilder.py index 8fd1fe3..2d5d3d7 100755 --- a/exonetapi/RequestBuilder.py +++ b/exonetapi/RequestBuilder.py @@ -84,8 +84,6 @@ def get(self, identifier=None): :return: A Resource or a Collection of Resources. """ - if not self.__resource: - raise ValueError('Setting a resource is required before making a call.') response = requests.get( self.__build_url(identifier), diff --git a/exonetapi/result/Parser.py b/exonetapi/result/Parser.py index 7f80b7d..3559f1f 100755 --- a/exonetapi/result/Parser.py +++ b/exonetapi/result/Parser.py @@ -72,11 +72,6 @@ def parse_relations(self, relationships, origin_type, origin_id): relationships = [] for relationItem in relation['data'] : - if 'attributes' in relationItem: - relationships.append( - create_resource(relationItem) - ) - else: relationships.append(ResourceIdentifier(relationItem['type'], relationItem['id'])) relationship.set_resource_identifiers(relationships) diff --git a/exonetapi/structures/Relation.py b/exonetapi/structures/Relation.py index 6285203..2c0acdc 100644 --- a/exonetapi/structures/Relation.py +++ b/exonetapi/structures/Relation.py @@ -14,7 +14,7 @@ def __init__(self, relation_name, origin_type, origin_id): self.__url = self.__urlPattern % (origin_type, origin_id, relation_name) # ApiResourceSet|ApiResourceIdentifier The related resource identifier or a ApiResourceSet. - __resourceIdentifiers = None + self.__resourceIdentifiers = None from exonetapi import RequestBuilder self.__request = RequestBuilder(self.__url) diff --git a/exonetapi/structures/Resource.py b/exonetapi/structures/Resource.py index b035bff..557a628 100755 --- a/exonetapi/structures/Resource.py +++ b/exonetapi/structures/Resource.py @@ -49,8 +49,10 @@ def to_json(self): if self.id(): json['id'] = self.id() - if self.__relationships: - json['relationships'] = self.get_json_relationships() + + relationships = self.get_json_relationships() + if relationships: + json['relationships'] = relationships return json diff --git a/exonetapi/structures/ResourceIdentifier.py b/exonetapi/structures/ResourceIdentifier.py index ba09b87..ec9f484 100755 --- a/exonetapi/structures/ResourceIdentifier.py +++ b/exonetapi/structures/ResourceIdentifier.py @@ -33,6 +33,12 @@ def id(self): def related(self, name): + """Define a new relation for the resource. Can be used to make new requests to the API. + + + :param name: The name of the relation. + :return: Relation The new relation. + """ return Relation(name, self.type(), self.id()) diff --git a/tests/result/testParser.py b/tests/result/testParser.py index b3ade83..92e8d21 100755 --- a/tests/result/testParser.py +++ b/tests/result/testParser.py @@ -129,6 +129,43 @@ def test_parse_single_with_multi_relation(self): self.assertEqual(len(result), 2) + def test_parse_single_with_multi_relation(self): + json_data_list = """ + { + "data": + { + "type": "comments", + "id": "DV6axK4GwNEb", + "attributes": { + "subject": "Can you help me?" + }, + "relationships": { + "tags": { + "links": { + "self": "https://api.exonet.nl/comments/DV6axK4GwNEb/relationships/tags", + "related": "https://api.exonet.nl/comments/DV6axK4GwNEb/tags" + }, + "data": [ + { + "type": "tags", + "id": "ABC" + }, + { + "type": "tags", + "id": "XYZ" + } + + ] + } + } + } + } + """ + + result = Parser(json_data_list).parse().relationship('tags').get_resource_identifiers() + + self.assertEqual(len(result), 2) + if __name__ == '__main__': diff --git a/tests/structures/__init__.py b/tests/structures/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/tests/structures/testRelation.py b/tests/structures/testRelation.py new file mode 100755 index 0000000..c128251 --- /dev/null +++ b/tests/structures/testRelation.py @@ -0,0 +1,41 @@ +import unittest +from unittest.mock import MagicMock +from unittest import mock + +from tests.testCase import testCase + +from exonetapi.RequestBuilder import RequestBuilder +from exonetapi.auth.Authenticator import Authenticator +from exonetapi.structures.Relation import Relation +from exonetapi.exceptions.ValidationException import ValidationException +from exonetapi import create_resource + +import json + + +class testRelation(testCase): + + def test_len_empty(self): + relation = Relation('author', 'posts', 'postID') + + self.assertEqual(0, len(relation)) + + def test_len_filled(self): + relation = Relation('author', 'posts', 'postID') + relation.set_resource_identifiers([ + 'a', 'b', 'c' + ]) + + self.assertEqual(3, len(relation)) + + @mock.patch('exonetapi.RequestBuilder.get', return_value='get_response') + def test_getattr(self, mock_requestBuilder): + """Call a method on the relation and expect it to be passed to the RequestBuilder.""" + relation = Relation('author', 'posts', 'postID') + + self.assertEqual('get_response', relation.get()) + mock_requestBuilder.assert_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/structures/testResource.py b/tests/structures/testResource.py index 9419f11..f0503b4 100755 --- a/tests/structures/testResource.py +++ b/tests/structures/testResource.py @@ -16,13 +16,11 @@ class testResource(testCase): def test_init(self): - resource = create_resource.create_resource( - 'fake', - attributes={ - 'first_name': 'John', - 'last_name': 'Doe', - } - ) + resource = create_resource({ + 'type': 'fake', + }) + resource.attribute('first_name', 'John') + resource.attribute('last_name', 'Doe') self.assertEqual(resource.attribute('first_name'), 'John') self.assertEqual(resource.type(), 'fake') @@ -30,15 +28,16 @@ def test_init(self): self.assertEqual(resource.attributes(), {'first_name': 'John', 'last_name': 'Doe', }) def test_init_relationship(self): - resource = create_resource.create_resource( - 'fake', - attributes={ - 'first_name': 'John', - 'last_name': 'Doe', - }, - relationships={ - 'account': create_resource.create_resource('account', id='someAccountID') - } + resource = create_resource({ + 'type': 'fake', + }) + + resource.set_relationship( + 'account', + create_resource({ + 'type': 'account', + 'id': 'someAccountID', + }) ) self.assertEqual(resource.get_json_relationships(), { @@ -46,17 +45,19 @@ def test_init_relationship(self): }) def test_set_relationship(self): - resource = create_resource.create_resource( - 'fake', - attributes={ - 'first_name': 'John', - 'last_name': 'Doe', - } - ) + resource = create_resource({ + 'type': 'fake' + }) resource.relationship('messages', [ - create_resource.create_resource('message', id='messageOne'), - create_resource.create_resource('message', id='messageTwo') + create_resource({ + 'type': 'message', + 'id': 'messageOne' + }), + create_resource({ + 'type': 'message', + 'id': 'messageTwo' + }) ]) self.assertEqual(resource.get_json_relationships(), { @@ -67,24 +68,24 @@ def test_set_relationship(self): }) def test_to_json(self): - resource = create_resource.create_resource( - 'fake', - id='FakeID', - attributes={ - 'first_name': 'John' - }, - relationships={ - 'thing': create_resource.create_resource('things', id='thingID') - } + resource = Resource({ + 'type': 'fake', + 'id': 'FakeID', + }) + resource.set_relationship( + 'thing', + create_resource({ + 'type': 'things', + 'id': 'thingID', + }) ) + self.assertEqual( json.dumps(resource.to_json()), json.dumps({ 'type': 'fake', - 'attributes': { - 'first_name': 'John' - }, + 'attributes': { }, 'id': 'FakeID', 'relationships': { 'thing': { @@ -97,87 +98,5 @@ def test_to_json(self): }) ) - def test_get_json_relationships(self): - resource = create_resource.create_resource( - 'fake', - relationships={ - 'thing': { - 'data' : { - 'type': 'things', - 'id' : 'thingID' - } - } - } - ) - - self.assertEqual( - json.dumps(resource.get_json_relationships()), - json.dumps( - { - 'thing': { - 'data': { - 'type': 'things', - 'id': 'thingID' - } - } - } - ) - ) - - def test_get_relationship_single(self): - resource = create_resource.create_resource('fake') - resource.set_relationship( - 'thing', - create_resource.create_resource('things', id='thingID') - ) - - self.assertEqual( - resource.get_relationship('thing').to_json_resource_identifier(), - { - 'type': 'things', - 'id': 'thingID' - } - ) - - def test_get_relationship_multi(self): - resource = create_resource.create_resource('fake') - resource.set_relationship( - 'thingies', - [ - create_resource.create_resource('things', id='thingOneID'), - create_resource.create_resource('things', id='thingTwoID') - ] - ) - - relations = resource.get_relationship('thingies') - - relationlist = [] - for relation in relations: - relationlist.append(relation.to_json_resource_identifier()) - - - self.assertEqual( - relationlist, - [ - { - 'type': 'things', - 'id': 'thingOneID' - }, - { - 'type': 'things', - 'id': 'thingTwoID' - } - ] - ) - - def test_relationship_none(self): - resource = create_resource.create_resource('dummy') - - self.assertEqual( - resource.relationship('invalid'), - None - ) - - if __name__ == '__main__': unittest.main() diff --git a/tests/structures/testResourceIdentifier.py b/tests/structures/testResourceIdentifier.py new file mode 100755 index 0000000..e94e70e --- /dev/null +++ b/tests/structures/testResourceIdentifier.py @@ -0,0 +1,129 @@ +import unittest +from unittest.mock import MagicMock +from unittest import mock + +from tests.testCase import testCase + +from exonetapi.RequestBuilder import RequestBuilder +from exonetapi.auth.Authenticator import Authenticator +from exonetapi.structures.Resource import Resource +from exonetapi.structures.Relationship import Relationship +from exonetapi.structures.Relation import Relation +from exonetapi.exceptions.ValidationException import ValidationException +from exonetapi import create_resource + +import json + + +class testResourceIdentifier(testCase): + + def test_init(self): + resource = create_resource({ + 'type': 'fake', + }) + resource.attribute('first_name', 'John') + resource.attribute('last_name', 'Doe') + + self.assertEqual(resource.attribute('first_name'), 'John') + self.assertEqual(resource.type(), 'fake') + self.assertIsNone(resource.id()) + self.assertEqual(resource.attributes(), {'first_name': 'John', 'last_name': 'Doe', }) + + def test_get_relationship_create(self): + resource = create_resource({ + 'type': 'fake', + }) + + relationship = resource.get_relationship('something') + self.assertIsInstance(relationship, Relationship) + + def test_set_relationship(self): + resource = create_resource({ + 'type': 'fake' + }) + + resource.relationship('messages', [ + create_resource({ + 'type': 'message', + 'id': 'messageOne' + }), + create_resource({ + 'type': 'message', + 'id': 'messageTwo' + }) + ]) + + self.assertEqual(resource.get_json_relationships(), { + 'messages': {'data': [ + {'id': 'messageOne', 'type': 'message'}, + {'id': 'messageTwo', 'type': 'message'} + ]} + }) + + def test_get_json_relationships(self): + resource = create_resource({ + 'type': 'fake' + }) + + resource.relationship('messages', { + 'data' : { + 'type': 'this', + 'id': 'that', + } + }) + + self.assertEqual( + resource.get_json_relationships(), + { + 'messages': { + 'data': { + 'id': 'that', + 'type': 'this' + } + } + } + ) + + + def test_to_json(self): + resource = Resource({ + 'type': 'fake', + 'id': 'FakeID', + }) + resource.set_relationship( + 'thing', + create_resource({ + 'type': 'things', + 'id': 'thingID', + }) + ) + + + self.assertEqual( + json.dumps(resource.to_json()), + json.dumps({ + 'type': 'fake', + 'attributes': { }, + 'id': 'FakeID', + 'relationships': { + 'thing': { + 'data': { + 'type': 'things', + 'id': 'thingID' + } + } + } + }) + ) + + def test_related(self): + resource = create_resource({ + 'type': 'fake', + }) + + relation = resource.related('something') + self.assertIsInstance(relation, Relation) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testRequestBuilder.py b/tests/testRequestBuilder.py index 63ee11e..61e4447 100755 --- a/tests/testRequestBuilder.py +++ b/tests/testRequestBuilder.py @@ -52,6 +52,9 @@ def test_sort_default(self): self.request_builder.sort('domain') self.assertEqual(self.request_builder._RequestBuilder__query_params['sort'], 'domain') + def test_sort_invalid(self): + self.assertRaises(ValueError, self.request_builder.sort, 'domain', 'topdown') + def test_sortAsc(self): self.request_builder.sort_asc('domain') self.assertEqual(self.request_builder._RequestBuilder__query_params['sort'], 'domain') From 3c08746aaedda7ca3e326e57da2a9d1f19b2f36b Mon Sep 17 00:00:00 2001 From: Styxit Date: Tue, 13 Aug 2019 16:07:52 +0200 Subject: [PATCH 11/16] Remove default url from examples --- examples/dns_zone_details.py | 2 +- examples/dns_zones.py | 2 +- examples/ticket_details.py | 2 +- examples/tickets.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/dns_zone_details.py b/examples/dns_zone_details.py index 82ba0f9..fcc345b 100644 --- a/examples/dns_zone_details.py +++ b/examples/dns_zone_details.py @@ -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]) diff --git a/examples/dns_zones.py b/examples/dns_zones.py index 039cffa..2fb0596 100644 --- a/examples/dns_zones.py +++ b/examples/dns_zones.py @@ -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]) diff --git a/examples/ticket_details.py b/examples/ticket_details.py index 3b60473..ea055a6 100644 --- a/examples/ticket_details.py +++ b/examples/ticket_details.py @@ -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]) diff --git a/examples/tickets.py b/examples/tickets.py index 5d038f6..c00b022 100644 --- a/examples/tickets.py +++ b/examples/tickets.py @@ -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]) From 48dc533798b7ff42daface4647f76f7567e255d1 Mon Sep 17 00:00:00 2001 From: Styxit Date: Tue, 13 Aug 2019 16:09:27 +0200 Subject: [PATCH 12/16] Use docstring for multiline comments --- examples/ticket_details.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/ticket_details.py b/examples/ticket_details.py index ea055a6..7dedf0f 100644 --- a/examples/ticket_details.py +++ b/examples/ticket_details.py @@ -8,9 +8,11 @@ # 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. From 99f24e819b747f164495244a48ed6f116a1d6877 Mon Sep 17 00:00:00 2001 From: Styxit Date: Wed, 14 Aug 2019 09:23:09 +0200 Subject: [PATCH 13/16] Apply code conventions --- examples/ticket_details.py | 2 +- exonetapi/Client.py | 2 ++ exonetapi/RequestBuilder.py | 13 ++++++----- exonetapi/auth/Authenticator.py | 3 ++- exonetapi/create_resource.py | 1 + exonetapi/result/Parser.py | 26 +++++++++++++--------- exonetapi/structures/Relation.py | 19 ++++++++-------- exonetapi/structures/Resource.py | 3 +-- exonetapi/structures/ResourceIdentifier.py | 10 ++++----- 9 files changed, 43 insertions(+), 36 deletions(-) diff --git a/examples/ticket_details.py b/examples/ticket_details.py index 7dedf0f..61c35ed 100644 --- a/examples/ticket_details.py +++ b/examples/ticket_details.py @@ -35,7 +35,7 @@ ) # Get the emails in the ticket. -emails = ticket.related('emails').get() +emails = ticket.related('emails').get() print('This ticket has {mailCount} emails'.format( mailCount=len(emails) diff --git a/exonetapi/Client.py b/exonetapi/Client.py index 895abed..56912f7 100755 --- a/exonetapi/Client.py +++ b/exonetapi/Client.py @@ -5,8 +5,10 @@ from .RequestBuilder import RequestBuilder from urllib.parse import urlparse + class Singleton(type): _instances = {} + def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) diff --git a/exonetapi/RequestBuilder.py b/exonetapi/RequestBuilder.py index 2d5d3d7..fd08f64 100755 --- a/exonetapi/RequestBuilder.py +++ b/exonetapi/RequestBuilder.py @@ -16,7 +16,10 @@ def __init__(self, resource, client=None): resource = '/' + resource self.__resource = resource - # The query params that will be used in the GET requests. Can contain filters and page options. + """ + The query params that will be used in the GET requests. + Can contain filters and page options. + """ self.__query_params = {} if client: @@ -81,7 +84,7 @@ def sort_desc(self, sort_field): 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. """ @@ -97,10 +100,10 @@ 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. """ response = requests.post( self.__build_url(), diff --git a/exonetapi/auth/Authenticator.py b/exonetapi/auth/Authenticator.py index 8d81009..a8dd95a 100755 --- a/exonetapi/auth/Authenticator.py +++ b/exonetapi/auth/Authenticator.py @@ -3,7 +3,8 @@ """ import requests -class Authenticator(): + +class Authenticator: """ Manage the authentication and keep track of (valid) tokens. """ diff --git a/exonetapi/create_resource.py b/exonetapi/create_resource.py index a68cb84..b1d4d81 100755 --- a/exonetapi/create_resource.py +++ b/exonetapi/create_resource.py @@ -1,6 +1,7 @@ from inflection import camelize from .structures.Resource import Resource + def create_resource(resource): """Create a dynamic Resource based on the type that is provided in the data. diff --git a/exonetapi/result/Parser.py b/exonetapi/result/Parser.py index 3559f1f..c9acaa5 100755 --- a/exonetapi/result/Parser.py +++ b/exonetapi/result/Parser.py @@ -19,7 +19,7 @@ def __init__(self, data): def parse(self): """Parse JSON string into a Resource or a list of Resources. - :return list|Resource: List with Resources or a single Resource. + :return: list|Resource: List with Resources or a single Resource. """ if type(self.__json_data) is list: resources = [] @@ -30,7 +30,6 @@ def parse(self): else: return self.make_resource(self.__json_data) - def make_resource(self, resource_data): resource = create_resource({ 'type': resource_data['type'], @@ -44,35 +43,40 @@ def make_resource(self, resource_data): # Extract and parse all included relations. if 'relationships' in resource_data.keys(): - parsedRelations = self.parse_relations(resource_data['relationships'], resource.type(), resource.id()) + parsed_relations = self.parse_relations( + resource_data['relationships'], + resource.type(), + resource.id() + ) - for k, r in parsedRelations.items(): + for k, r in parsed_relations.items(): resource.set_relationship(k, r) return resource - def parse_relations(self, relationships, origin_type, origin_id): parsedRelations = {} if relationships: for relationName, relation in relationships.items(): - # set a relation + # Set a relation if ('data' in relation.keys()) and relation['data']: relationship = Relationship(relationName, origin_type, origin_id) - # Single. + # Set a single relationship. if 'type' in relation['data']: relationship.set_resource_identifiers( ResourceIdentifier(relation['data']['type'], relation['data']['id']) ) - # Multi. + # Set a multi relationship. elif isinstance(relation['data'], list): - relationships = [] - for relationItem in relation['data'] : - relationships.append(ResourceIdentifier(relationItem['type'], relationItem['id'])) + for relationItem in relation['data']: + relationships.append(ResourceIdentifier( + relationItem['type'], + relationItem['id']) + ) relationship.set_resource_identifiers(relationships) diff --git a/exonetapi/structures/Relation.py b/exonetapi/structures/Relation.py index 2c0acdc..c6ff4ae 100644 --- a/exonetapi/structures/Relation.py +++ b/exonetapi/structures/Relation.py @@ -6,9 +6,9 @@ class Relation(object): def __init__(self, relation_name, origin_type, origin_id): """Relation constructor. - :param: string $relationName The name of the relation. - :param: string $originType The resource type of the origin resource. - :param: string $originId The resource ID of the origin resource. + :param: str relation_name The name of the relation. + :param: str origin_type The resource type of the origin resource. + :param: str origin_id The resource ID of the origin resource. """ self.__name = relation_name self.__url = self.__urlPattern % (origin_type, origin_id, relation_name) @@ -26,7 +26,7 @@ def __len__(self): return 0 def __getattr__(self, name): - def method(*args): + def method(): return getattr(self.__request,name)() return method @@ -35,18 +35,17 @@ def get_resource_identifiers(self): """ Get the resource identifiers for this relation. - return ApiResourceSet|ApiResourceIdentifier The resource identifier or a resource set. + :return ApiResourceSet|ApiResourceIdentifier The resource identifier or a resource set. """ return self.__resourceIdentifiers - - def set_resource_identifiers(self, newRelationship): + def set_resource_identifiers(self, new_relationship): """ Replace the related resource identifiers with new data. - :param ApiResourceSet|ApiResourceIdentifier $newRelationship A new resource identifier or a new resource set. + :param ApiResourceSet|ApiResourceIdentifier new_relationship A new resource identifier or a new resource set. :return self """ - self.__resourceIdentifiers = newRelationship + self.__resourceIdentifiers = new_relationship - return self + return self diff --git a/exonetapi/structures/Resource.py b/exonetapi/structures/Resource.py index 557a628..c9c8ee4 100755 --- a/exonetapi/structures/Resource.py +++ b/exonetapi/structures/Resource.py @@ -21,6 +21,7 @@ def attribute(self, item, value=None): """Get Resource attributes if available. :param item: The name of the Resource attribute. + :param value: The new value of the attribute. :return: The attribute or None when attribute does not exist. """ if value: @@ -34,7 +35,6 @@ def attributes(self): """ return self.__attributes - def to_json(self): """Convert a Resource to a dict according to the JSON-API format. @@ -49,7 +49,6 @@ def to_json(self): if self.id(): json['id'] = self.id() - relationships = self.get_json_relationships() if relationships: json['relationships'] = relationships diff --git a/exonetapi/structures/ResourceIdentifier.py b/exonetapi/structures/ResourceIdentifier.py index ec9f484..ec0a9c1 100755 --- a/exonetapi/structures/ResourceIdentifier.py +++ b/exonetapi/structures/ResourceIdentifier.py @@ -16,7 +16,6 @@ def __init__(self, type, id=None): self.__relationships = {} - def type(self): """Get the resource type of this Resource instance. @@ -31,7 +30,6 @@ def id(self): """ return self.__id - def related(self, name): """Define a new relation for the resource. Can be used to make new requests to the API. @@ -41,10 +39,10 @@ def related(self, name): """ return Relation(name, self.type(), self.id()) - def relationship(self, name, *data): - """Define a new relationship for this resource, replace an existing one or get an existing one. - When data is provided the relationship is set, without data the relationship is returned. + """Define a new relationship for this resource, replace an existing one or get an + existing one. When data is provided the relationship is set, without data the relationship + is returned. :param name: The name of the relation to set. :param data: The value of the relation, can be a Resource or a dict of Resources. @@ -82,7 +80,7 @@ def set_relationship(self, name, data): def to_json(self): """Convert a ResourceIdentifier to JSON. - :return: A dict with the Resources type and ID. + :return: A dict with the resource type and ID. """ return { 'type': self.type(), From a87962521e143f7510cd59da8015ad9a8c11998a Mon Sep 17 00:00:00 2001 From: Styxit Date: Wed, 14 Aug 2019 09:52:16 +0200 Subject: [PATCH 14/16] Apply conventions in tests --- tests/exceptions/testValidationException.py | 10 ++++- tests/result/testParser.py | 49 +++++++++------------ tests/structures/testRelation.py | 11 +---- tests/structures/testResource.py | 7 +-- tests/structures/testResourceIdentifier.py | 7 --- tests/testCase.py | 1 + tests/testClient.py | 1 + tests/testRequestBuilder.py | 1 - tests/test_create_resource.py | 1 + 9 files changed, 36 insertions(+), 52 deletions(-) diff --git a/tests/exceptions/testValidationException.py b/tests/exceptions/testValidationException.py index d696b8a..da22f99 100755 --- a/tests/exceptions/testValidationException.py +++ b/tests/exceptions/testValidationException.py @@ -49,7 +49,10 @@ def test_one_error(self): response.json.assert_called_once() # Make sure the right message is set. - self.assertEqual(v.args[0], 'Field: start_date, failed rule: iso8601-date(Date must be in iso8601 format).') + self.assertEqual( + v.args[0], + 'Field: start_date, failed rule: iso8601-date(Date must be in iso8601 format).' + ) def test_twoErrors(self): # Construct the request response. @@ -86,7 +89,10 @@ def test_twoErrors(self): response.json.assert_called_once() # Make sure the right message is set. - self.assertEqual(v.args[0], 'Field: data.end_date, failed rule: Required(). The provided data is invalid.') + self.assertEqual( + v.args[0], + 'Field: data.end_date, failed rule: Required(). The provided data is invalid.' + ) def test_otherErrors(self): # Construct the request response. diff --git a/tests/result/testParser.py b/tests/result/testParser.py index 92e8d21..7837f5d 100755 --- a/tests/result/testParser.py +++ b/tests/result/testParser.py @@ -1,6 +1,4 @@ import unittest -from unittest import mock -from unittest.mock import call from tests.testCase import testCase from exonetapi.result import Parser @@ -95,33 +93,31 @@ def test_parse_single(self): def test_parse_single_with_multi_relation(self): json_data_list = """ { - "data": - { - "type": "comments", - "id": "DV6axK4GwNEb", - "attributes": { - "subject": "Can you help me?" - }, - "relationships": { - "tags": { - "links": { - "self": "https://api.exonet.nl/comments/DV6axK4GwNEb/relationships/tags", - "related": "https://api.exonet.nl/comments/DV6axK4GwNEb/tags" + "data": { + "type": "comments", + "id": "DV6axK4GwNEb", + "attributes": { + "subject": "Can you help me?" + }, + "relationships": { + "tags": { + "links": { + "self": "https://api.exonet.nl/comments/DV6axK4GwNEb/relationships/tags", + "related": "https://api.exonet.nl/comments/DV6axK4GwNEb/tags" + }, + "data": [ + { + "type": "tags", + "id": "ABC" }, - "data": [ - { - "type": "tags", - "id": "ABC" - }, - { - "type": "tags", - "id": "XYZ" - } - - ] - } + { + "type": "tags", + "id": "XYZ" + } + ] } } + } } """ @@ -167,6 +163,5 @@ def test_parse_single_with_multi_relation(self): self.assertEqual(len(result), 2) - if __name__ == '__main__': unittest.main() diff --git a/tests/structures/testRelation.py b/tests/structures/testRelation.py index c128251..3533eae 100755 --- a/tests/structures/testRelation.py +++ b/tests/structures/testRelation.py @@ -1,16 +1,9 @@ import unittest -from unittest.mock import MagicMock from unittest import mock from tests.testCase import testCase -from exonetapi.RequestBuilder import RequestBuilder -from exonetapi.auth.Authenticator import Authenticator from exonetapi.structures.Relation import Relation -from exonetapi.exceptions.ValidationException import ValidationException -from exonetapi import create_resource - -import json class testRelation(testCase): @@ -29,12 +22,12 @@ def test_len_filled(self): self.assertEqual(3, len(relation)) @mock.patch('exonetapi.RequestBuilder.get', return_value='get_response') - def test_getattr(self, mock_requestBuilder): + def test_getattr(self, mock_request_builder): """Call a method on the relation and expect it to be passed to the RequestBuilder.""" relation = Relation('author', 'posts', 'postID') self.assertEqual('get_response', relation.get()) - mock_requestBuilder.assert_called() + mock_request_builder.assert_called() if __name__ == '__main__': diff --git a/tests/structures/testResource.py b/tests/structures/testResource.py index f0503b4..592d17e 100755 --- a/tests/structures/testResource.py +++ b/tests/structures/testResource.py @@ -1,13 +1,8 @@ import unittest -from unittest.mock import MagicMock -from unittest import mock from tests.testCase import testCase -from exonetapi.RequestBuilder import RequestBuilder -from exonetapi.auth.Authenticator import Authenticator from exonetapi.structures.Resource import Resource -from exonetapi.exceptions.ValidationException import ValidationException from exonetapi import create_resource import json @@ -80,7 +75,6 @@ def test_to_json(self): }) ) - self.assertEqual( json.dumps(resource.to_json()), json.dumps({ @@ -98,5 +92,6 @@ def test_to_json(self): }) ) + if __name__ == '__main__': unittest.main() diff --git a/tests/structures/testResourceIdentifier.py b/tests/structures/testResourceIdentifier.py index e94e70e..a9a1a1c 100755 --- a/tests/structures/testResourceIdentifier.py +++ b/tests/structures/testResourceIdentifier.py @@ -1,15 +1,10 @@ import unittest -from unittest.mock import MagicMock -from unittest import mock from tests.testCase import testCase -from exonetapi.RequestBuilder import RequestBuilder -from exonetapi.auth.Authenticator import Authenticator from exonetapi.structures.Resource import Resource from exonetapi.structures.Relationship import Relationship from exonetapi.structures.Relation import Relation -from exonetapi.exceptions.ValidationException import ValidationException from exonetapi import create_resource import json @@ -84,7 +79,6 @@ def test_get_json_relationships(self): } ) - def test_to_json(self): resource = Resource({ 'type': 'fake', @@ -98,7 +92,6 @@ def test_to_json(self): }) ) - self.assertEqual( json.dumps(resource.to_json()), json.dumps({ diff --git a/tests/testCase.py b/tests/testCase.py index 4768fff..cb6c01d 100755 --- a/tests/testCase.py +++ b/tests/testCase.py @@ -1,6 +1,7 @@ import unittest from exonetapi.Client import Singleton + class testCase(unittest.TestCase): def setUp(self): diff --git a/tests/testClient.py b/tests/testClient.py index 9f75fc9..eaca947 100755 --- a/tests/testClient.py +++ b/tests/testClient.py @@ -30,5 +30,6 @@ def test_resource(self): self.assertIsInstance(resource, RequestBuilder) + if __name__ == '__main__': unittest.main() diff --git a/tests/testRequestBuilder.py b/tests/testRequestBuilder.py index 61e4447..25095de 100755 --- a/tests/testRequestBuilder.py +++ b/tests/testRequestBuilder.py @@ -5,7 +5,6 @@ from tests.testCase import testCase from exonetapi import Client from exonetapi.RequestBuilder import RequestBuilder -from exonetapi.auth.Authenticator import Authenticator from exonetapi.structures.Resource import Resource from exonetapi.exceptions.ValidationException import ValidationException diff --git a/tests/test_create_resource.py b/tests/test_create_resource.py index 5fbbee2..e0d77e3 100755 --- a/tests/test_create_resource.py +++ b/tests/test_create_resource.py @@ -15,5 +15,6 @@ def test_create_resource(self): self.assertEqual(resource.__class__.__name__, 'TestResource') + if __name__ == '__main__': unittest.main() From a1aba6d458b069d2491e88ed4e2ddc70822e0344 Mon Sep 17 00:00:00 2001 From: Styxit Date: Wed, 14 Aug 2019 10:05:30 +0200 Subject: [PATCH 15/16] Docstrings --- exonetapi/result/Parser.py | 2 +- exonetapi/structures/Relation.py | 12 ++++++------ exonetapi/structures/Resource.py | 1 - exonetapi/structures/ResourceIdentifier.py | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/exonetapi/result/Parser.py b/exonetapi/result/Parser.py index c9acaa5..f36168d 100755 --- a/exonetapi/result/Parser.py +++ b/exonetapi/result/Parser.py @@ -19,7 +19,7 @@ def __init__(self, data): def parse(self): """Parse JSON string into a Resource or a list of Resources. - :return: list|Resource: List with Resources or a single Resource. + :return list|Resource: List with Resources or a single Resource. """ if type(self.__json_data) is list: resources = [] diff --git a/exonetapi/structures/Relation.py b/exonetapi/structures/Relation.py index c6ff4ae..b5ee36e 100644 --- a/exonetapi/structures/Relation.py +++ b/exonetapi/structures/Relation.py @@ -6,9 +6,9 @@ class Relation(object): def __init__(self, relation_name, origin_type, origin_id): """Relation constructor. - :param: str relation_name The name of the relation. - :param: str origin_type The resource type of the origin resource. - :param: str origin_id The resource ID of the origin resource. + :param str relation_name: The name of the relation. + :param str origin_type: The resource type of the origin resource. + :param str origin_id: The resource ID of the origin resource. """ self.__name = relation_name self.__url = self.__urlPattern % (origin_type, origin_id, relation_name) @@ -35,7 +35,7 @@ def get_resource_identifiers(self): """ Get the resource identifiers for this relation. - :return ApiResourceSet|ApiResourceIdentifier The resource identifier or a resource set. + :return ApiResourceSet|ApiResourceIdentifier: The resource identifier or a resource set. """ return self.__resourceIdentifiers @@ -43,8 +43,8 @@ def set_resource_identifiers(self, new_relationship): """ Replace the related resource identifiers with new data. - :param ApiResourceSet|ApiResourceIdentifier new_relationship A new resource identifier or a new resource set. - :return self + :param ApiResourceSet|ApiResourceIdentifier new_relationship: A new resource identifier or a new resource set. + :return self: """ self.__resourceIdentifiers = new_relationship diff --git a/exonetapi/structures/Resource.py b/exonetapi/structures/Resource.py index c9c8ee4..291fcc1 100755 --- a/exonetapi/structures/Resource.py +++ b/exonetapi/structures/Resource.py @@ -63,4 +63,3 @@ def to_json_resource_identifier(self): return super().to_json() - diff --git a/exonetapi/structures/ResourceIdentifier.py b/exonetapi/structures/ResourceIdentifier.py index ec0a9c1..8f113d4 100755 --- a/exonetapi/structures/ResourceIdentifier.py +++ b/exonetapi/structures/ResourceIdentifier.py @@ -35,7 +35,7 @@ def related(self, name): :param name: The name of the relation. - :return: Relation The new relation. + :return Relation: The new relation. """ return Relation(name, self.type(), self.id()) @@ -46,7 +46,7 @@ def relationship(self, name, *data): :param name: The name of the relation to set. :param data: The value of the relation, can be a Resource or a dict of Resources. - :return: self when setting a relationship, or the actual relationship when getting it + :return self: when setting a relationship, or the actual relationship when getting it """ if len(data) is 1: return self.set_relationship(name, data[0]) From 9cecf3b4d656007c852340c44ab76ef55cffa9ea Mon Sep 17 00:00:00 2001 From: Styxit Date: Wed, 14 Aug 2019 10:13:57 +0200 Subject: [PATCH 16/16] Update version for new release --- CHANGELOG.md | 7 ++++++- setup.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbed8f2..ce8f086 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/setup.py b/setup.py index 273cf68..20f71f1 100755 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name='exonetapi', - version='0.0.5', + version='1.0.0', description='Library to interact with the Exonet API.', long_description=long_description, @@ -35,7 +35,7 @@ # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ - 'Development Status :: 2 - Pre-Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries :: Python Modules', 'License :: OSI Approved :: MIT License',