Permalink
Browse files

Add initial set of resource classes with GET functionality

  • Loading branch information...
1 parent 9f4cdf2 commit 0790fc5b9b60f9f8d0bc65eb554d08d073d73820 @tgsergeant tgsergeant committed with tgsergeant Feb 7, 2014
View
3 .gitignore
@@ -1 +1,2 @@
-*.pyc
+*.pyc
+ENV/
View
2 bigcommerce/api/__init__.py
@@ -0,0 +1,2 @@
+from resources import *
+import auth
View
22 bigcommerce/api/auth.py
@@ -0,0 +1,22 @@
+import connection
+import logging
+
+log = logging.getLogger("bigcommerce.api.auth")
+
+
+def basic_login(host, username, apikey):
+ log.debug("Login to %s as %s".format(host, username))
+ connection.client = connection.Connection(host, (username, apikey))
+
+
+def oauth_configure(client_id, store_hash, access_token=None):
+ connection.client = connection.OAuthConnection(client_id, store_hash, access_token)
+
+
+def oauth_fetch_token(client_secret, code, context, scope, redirect_uri):
+ if connection.client and isinstance(connection.client, connection.OAuthConnection):
+ return connection.client.fetch_token(client_secret, code, context, scope, redirect_uri)
+
+
+def oauth_verify_payload(signed_payload, client_secret):
+ return connection.OAuthConnection.verify_payload(signed_payload, client_secret)
View
118 bigcommerce/api/connection.py
@@ -3,25 +3,32 @@
Handles put and get operations to the Bigcommerce REST API
"""
+import base64
+import hashlib
+import hmac
-import urllib, json # only used for urlencode querystr
+import urllib
+import json # only used for urlencode querystr
import logging
+import streql
from pprint import pformat # only used once for logging, in __load_urls
import requests
from resources.mapping import Mapping
-from httpexception import *
+from exception import *
-log = logging.getLogger("Bigcommerce.com")
+log = logging.getLogger("bigcommerce.api.connection")
+
+client = None
class Connection(object):
"""
Connection class manages the connection to the Bigcommerce REST API.
"""
-
- def __init__(self, host, auth, api_path='/api/v2/{}', map_wrap=True):
+
+ def __init__(self, host, auth, api_path='/api/v2/{}', map_wrap=False):
"""
On creation, an initial call is made to load the mappings of resources to URLs.
@@ -30,9 +37,9 @@ def __init__(self, host, auth, api_path='/api/v2/{}', map_wrap=True):
"""
self.host = host
self.api_path = api_path
-
+
self.timeout = 7.0 # need to catch timeout?
-
+
log.info("API Host: %s/%s" % (self.host, self.api_path))
self._map_wrap = map_wrap
@@ -44,21 +51,21 @@ def __init__(self, host, auth, api_path='/api/v2/{}', map_wrap=True):
self.__resource_meta = self.get() # retrieve metadata about urls and resources
log.debug(pformat(self.__resource_meta))
-
+
self._last_response = None # for debugging
-
+
def meta_data(self):
"""
Return a JSON string representation of resource-to-url mappings
"""
return json.dumps(self.__resource_meta)
-
+
def get_url(self, resource_name):
"""
Lookup the "url" for the resource name from the internally stored resource mappings
"""
return self.__resource_meta.get(resource_name, {}).get("url", None)
-
+
def get_resource_url(self, resource_name):
"""
Lookup the "resource" for the resource name from the internally stored resource mappings
@@ -67,7 +74,7 @@ def get_resource_url(self, resource_name):
def full_path(self, url):
return "https://" + self.host + self.api_path.format(url)
-
+
def _run_method(self, method, url, data=None, query={}, headers={}):
# make full path if not given
if url and url[:4] != "http":
@@ -111,7 +118,7 @@ def get(self, resource="", rid=None, **query):
resource += str(rid)
response = self._run_method('GET', resource, query=query)
return self._handle_response(resource, response)
-
+
def update(self, resource, rid, updates):
"""
Updates the resource with id 'rid' with the given updates dictionary.
@@ -138,6 +145,10 @@ def delete(self, resource, rid=None): # note that rid can't be 0 - problem?
# Raw-er stuff
+ def make_request(self, method, url, data=None, params = {}, headers = {}):
+ response = self._run_method(method, url, data, params, headers)
+ return self._handle_response(url, response)
+
def put(self, url, data):
"""
Make a PUT request to save data.
@@ -166,13 +177,14 @@ def _handle_response(self, url, res, suppress_empty=False):
result = res.json()
except Exception as e: # json might be invalid, or store might be down
e.message += " (_handle_response failed to decode JSON: " + str(res.content) + ")"
- raise # TODO better exception
+ raise # TODO better exception
if self._map_wrap:
if isinstance(result, list):
return map(Mapping, result)
else:
return Mapping(result)
- else: return result
+ else:
+ return result
elif res.status_code == 204 and not suppress_empty:
raise EmptyResponseWarning("%d %s @ %s: %s" % (res.status_code, res.reason, url, res.content), res)
elif res.status_code >= 500:
@@ -185,3 +197,79 @@ def _handle_response(self, url, res, suppress_empty=False):
def __repr__(self):
return "%s %s%s" % (self.__class__.__name__, self.host, self.api_path)
+
+
+class OAuthConnection(Connection):
+ """
+ Class for making OAuth requests on the Bigcommerce v2 API
+
+ Providing a value for access_token allows immediate access to resources within registered scope.
+ Otherwise, you may use fetch_token with the code, context, and scope passed to your application's callback url
+ to retrieve an access token.
+
+ The verify_payload method is also provided for authenticating signed payloads passed to an application's load url.
+ """
+
+ def __init__(self, client_id, store_hash, access_token=None,
+ host='api.bigcommerceapp.com', api_path='/stores/{}/v2/{}', map_wrap=False):
+ self.client_id = client_id
+ self.store_hash = store_hash
+
+ self.host = host
+ self.api_path = api_path
+
+ self.timeout = 7.0 # can attach to session?
+ self._map_wrap = map_wrap
+
+ self._session = requests.Session()
+ self._session.headers = {"Accept": "application/json"}
+ if access_token and store_hash:
+ self._session.headers.update(self._oauth_headers(client_id, access_token))
+
+ # TODO find meta info new path (/store gives a "your scope does not include this resource")
+ self.__resource_meta = {} # self.get() # retrieve metadata about urls and resources
+ self._last_response = None # for debugging
+
+ def full_path(self, url):
+ return "https://" + self.host + self.api_path.format(self.store_hash, url)
+
+ @staticmethod
+ def _oauth_headers(cid, atoken):
+ return {'X-Auth-Client': cid,
+ 'X-Auth-Token': atoken}
+
+ @staticmethod
+ def verify_payload(signed_payload, client_secret):
+ """
+ Given a signed payload (usually passed as parameter in a GET request to the app's load URL) and a client secret,
+ authenticates the payload and returns the user's data, or False on fail.
+
+ Uses constant-time str comparison to prevent vulnerability to timing attacks.
+ """
+ encoded_json, encoded_hmac = signed_payload.split('.')
+ dc_json = base64.b64decode(encoded_json)
+ signature = base64.b64decode(encoded_hmac)
+ expected_sig = hmac.new(client_secret, base64.b64decode(encoded_json), hashlib.sha256).hexdigest()
+ authorised = streql.equals(signature, expected_sig)
+ return json.loads(dc_json) if authorised else False
+
+ def fetch_token(self, client_secret, code, context, scope, redirect_uri,
+ token_url='https://login.bigcommerce.com/oauth2/token'):
+ """
+ Fetches a token from given token_url, using given parameters, and sets up session headers for
+ future requests.
+ redirect_uri should be the same as your callback URL.
+ code, context, and scope should be passed as parameters to your callback URL on app installation.
+
+ Raises HttpException on failure (same as Connection methods).
+ """
+ res = self.post(token_url, {'client_id': self.client_id,
+ 'client_secret': client_secret,
+ 'code': code,
+ 'context': context,
+ 'scope': scope,
+ 'grant_type': 'authorization_code',
+ 'redirect_uri': redirect_uri},
+ headers={'Content-Type': 'application/x-www-form-urlencoded'})
+ self._session.headers.update(self._oauth_headers(self.client_id, res['access_token']))
+ return res
View
4 bigcommerce/api/httpexception.py → bigcommerce/api/exception.py
@@ -36,4 +36,6 @@ class ServerException(HttpException): pass
# class UnsupportedRequest(ClientRequestException, ServerException): pass
# 3xx codes
-class RedirectionException(HttpException): pass
+class RedirectionException(HttpException): pass
+
+class NotLoggedInException(Exception): pass
View
90 bigcommerce/api/oauthconnection.py
@@ -1,90 +0,0 @@
-"""
-Connection Module
-
-Handles put and get operations to the Bigcommerce REST API
-"""
-import base64
-import hashlib
-import hmac
-import json
-
-import streql
-import requests
-
-from bigcommerce import Connection
-
-
-class OAuthConnection(Connection):
- """
- Class for making OAuth requests on the Bigcommerce v2 API as an integrated app.
-
- Providing a value for access_token allows immediate access to resources within registered scope.
- Otherwise, you may use fetch_token with the code, context, and scope passed to your application's callback url
- to retrieve an access token.
-
- The verify_payload method is also provided for authenticating signed payloads passed to an application's load url.
- """
-
- def __init__(self, client_id, store_hash, access_token=None,
- host='api.bigcommerceapp.com', api_path='/stores/{}/v2/{}', map_wrap=True):
- self.client_id = client_id
- self.store_hash = store_hash
-
- self.host = host
- self.api_path = api_path
-
- self.timeout = 7.0 # can attach to session?
- self._map_wrap = map_wrap
-
- self._session = requests.Session()
- self._session.headers = {"Accept": "application/json"}
- if access_token and store_hash:
- self._session.headers.update(self._oauth_headers(client_id, access_token))
-
- # TODO find meta info new path (/store gives a "your scope does not include this resource")
- self.__resource_meta = {} # self.get() # retrieve metadata about urls and resources
- self._last_response = None # for debugging
-
- def full_path(self, url):
- return "https://" + self.host + self.api_path.format(self.store_hash, url)
-
- @staticmethod
- def _oauth_headers(cid, atoken):
- return {'X-Auth-Client': cid,
- 'X-Auth-Token': atoken}
-
- @staticmethod
- def verify_payload(signed_payload, client_secret):
- """
- Given a signed payload (usually passed as parameter in a GET request to the app's load URL) and a client secret,
- authenticates the payload and returns the user's data, or False on fail.
-
- Uses constant-time str comparison to prevent vulnerability to timing attacks.
- """
- encoded_json, encoded_hmac = signed_payload.split('.')
- dc_json = base64.b64decode(encoded_json)
- signature = base64.b64decode(encoded_hmac)
- expected_sig = hmac.new(client_secret, base64.b64decode(encoded_json), hashlib.sha256).hexdigest()
- authorised = streql.equals(signature, expected_sig)
- return json.loads(dc_json) if authorised else False
-
- def fetch_token(self, client_secret, code, context, scope, redirect_uri,
- token_url='https://login.bigcommerce.com/oauth2/token'):
- """
- Fetches a token from given token_url, using given parameters, and sets up session headers for
- future requests.
- redirect_uri should be the same as your callback URL.
- code, context, and scope should be passed as parameters to your callback URL on app installation.
-
- Raises HttpException on failure (same as Connection methods).
- """
- res = self.post(token_url, {'client_id': self.client_id,
- 'client_secret': client_secret,
- 'code': code,
- 'context': context,
- 'scope': scope,
- 'grant_type': 'authorization_code',
- 'redirect_uri': redirect_uri},
- headers={'Content-Type': 'application/x-www-form-urlencoded'})
- self._session.headers.update(self._oauth_headers(self.client_id, res['access_token']))
- return res
View
18 bigcommerce/api/resources/__init__.py
@@ -1 +1,17 @@
-from mapping import Mapping
+from brands import *
+from categories import *
+from countries import *
+from coupons import *
+from customer_groups import *
+from customers import *
+from option_sets import *
+from options import *
+from order_statuses import *
+from orders import *
+from payments import *
+from products import *
+from redirects import *
+from shipping import *
+from store import *
+from tax_classes import *
+from time import *
View
81 bigcommerce/api/resources/base.py
@@ -0,0 +1,81 @@
+from bigcommerce.api import connection
+from bigcommerce.api.exception import NotLoggedInException
+
+
+class Mapping(dict):
+ """
+ Mapping
+
+ provides '.' access to dictionary keys
+ """
+ def __init__(self, mapping, *args, **kwargs):
+ filter_args = {k: mapping[k] for k in mapping if k not in dir(self)}
+ self.__dict__ = self
+ dict.__init__(self, filter_args, *args, **kwargs)
+
+class ApiResource(Mapping):
+ resource_name = ""
+
+ @classmethod
+ def _create_object(cls, response):
+ if isinstance(response, list):
+ return [cls._create_object(obj) for obj in response]
+ else:
+ return cls(response)
+
+ @classmethod
+ def _make_request(cls, method, url, data=None, params={}, headers={}):
+ if connection.client:
+ return connection.client.make_request(method, url, data, params, headers)
+ else:
+ raise NotLoggedInException()
+
+ @classmethod
+ def get(cls, id, **params):
+ return cls._create_object(cls._make_request('GET', "%s/%s" % (cls.resource_name, id), params=params))
+
+
+class ApiSubResource(ApiResource):
+ parent_resource = ""
+
+ @classmethod
+ def get(cls, parentid, id, **params):
+ path = "%s/%s/%s/%s" % (cls.parent_resource, parentid, cls.resource_name, id)
+ return cls._create_object(cls._make_request('GET', path, params=params))
+
+
+class CreateableApiResource(ApiResource):
+ pass
+
+
+class CreateableApiSubResource(ApiSubResource):
+ pass
+
+
+class ListableApiResource(ApiResource):
+ @classmethod
+ def all(cls, **params):
+ return cls._create_object(cls._make_request('GET', cls.resource_name, params=params))
+
+
+class ListableApiSubResource(ApiSubResource):
+ @classmethod
+ def all(cls, parentid, **params):
+ path = "%s/%s/%s" % (cls.parent_resource, parentid, cls.resource_name)
+ return cls._create_object(cls._make_request('GET', path, params=params))
+
+
+class UpdateableApiResource(ApiResource):
+ pass
+
+
+class UpdateableApiSubResource(ApiResource):
+ pass
+
+
+class DeleteableApiResource(ApiResource):
+ pass
+
+
+class DeleteableApiSubResource(ApiResource):
+ pass
View
4 bigcommerce/api/resources/brands.py
@@ -0,0 +1,4 @@
+from base import *
+
+class Brands(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'brands'
View
4 bigcommerce/api/resources/categories.py
@@ -0,0 +1,4 @@
+from base import *
+
+class Categories(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'categories'
View
15 bigcommerce/api/resources/countries.py
@@ -0,0 +1,15 @@
+from base import *
+
+class Countries(ListableApiResource):
+ resource_name = 'countries'
+
+ def states(self, id=None):
+ if id:
+ return CountryStates.get(self.id, id)
+ else:
+ return CountryStates.all(self.id)
+
+class CountryStates(ListableApiSubResource):
+ resource_name = 'states'
+ parent_resource = 'countries'
+
View
4 bigcommerce/api/resources/coupons.py
@@ -0,0 +1,4 @@
+from base import *
+
+class Coupons(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'coupons'
View
4 bigcommerce/api/resources/customer_groups.py
@@ -0,0 +1,4 @@
+from base import *
+
+class CustomerGroups(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'customer_groups'
View
15 bigcommerce/api/resources/customers.py
@@ -0,0 +1,15 @@
+from base import *
+
+class Customers(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'customers'
+
+ def addresses(self, id=None):
+ if id:
+ return CustomerAddresses.get(self.id, id)
+ else:
+ return CustomerAddresses.all(self.id)
+
+class CustomerAddresses(ListableApiSubResource, CreateableApiSubResource, UpdateableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'addresses'
+ parent_resource = 'customers'
+
View
10 bigcommerce/api/resources/mapping.py
@@ -1,10 +0,0 @@
-"""
-Mapping
-
-provides '.' access to dictionary keys
-"""
-
-class Mapping(dict):
- def __init__(self, *args, **kwargs):
- self.__dict__ = self
- dict.__init__(self, *args, **kwargs)
View
15 bigcommerce/api/resources/option_sets.py
@@ -0,0 +1,15 @@
+from base import *
+
+class OptionSets(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'option_sets'
+
+ def options(self, id=None):
+ if id:
+ return OptionSetOptions.get(self.id, id)
+ else:
+ return OptionSetOptions.all(self.id)
+
+class OptionSetOptions(ListableApiSubResource, CreateableApiSubResource, UpdateableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'options'
+ parent_resource = 'option_sets'
+
View
15 bigcommerce/api/resources/options.py
@@ -0,0 +1,15 @@
+from base import *
+
+class Options(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'options'
+
+ def values(self, id=None):
+ if id:
+ return OptionValues.get(self.id, id)
+ else:
+ return OptionValues.all(self.id)
+
+class OptionValues(ListableApiSubResource, CreateableApiSubResource, UpdateableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'values'
+ parent_resource = 'options'
+
View
4 bigcommerce/api/resources/order_statuses.py
@@ -0,0 +1,4 @@
+from base import *
+
+class OrderStatuses(ListableApiResource):
+ resource_name = 'order_statuses'
View
48 bigcommerce/api/resources/orders.py
@@ -0,0 +1,48 @@
+from base import *
+
+class Orders(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'orders'
+
+ def coupons(self, id=None):
+ if id:
+ return OrderCoupons.get(self.id, id)
+ else:
+ return OrderCoupons.all(self.id)
+
+ def products(self, id=None):
+ if id:
+ return OrderProducts.get(self.id, id)
+ else:
+ return OrderProducts.all(self.id)
+
+ def shipments(self, id=None):
+ if id:
+ return OrderShipments.get(self.id, id)
+ else:
+ return OrderShipments.all(self.id)
+
+ def shipping_addresses(self, id=None):
+ if id:
+ return OrderShippingAddresses.get(self.id, id)
+ else:
+ return OrderShippingAddresses.all(self.id)
+
+class OrderCoupons(ListableApiSubResource):
+ resource_name = 'coupons'
+ parent_resource = 'orders'
+
+
+class OrderProducts(ListableApiSubResource):
+ resource_name = 'products'
+ parent_resource = 'orders'
+
+
+class OrderShipments(ListableApiSubResource, CreateableApiSubResource, UpdateableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'shipments'
+ parent_resource = 'orders'
+
+
+class OrderShippingAddresses(ListableApiSubResource):
+ resource_name = 'shipping_addresses'
+ parent_resource = 'orders'
+
View
4 bigcommerce/api/resources/payments.py
@@ -0,0 +1,4 @@
+from base import *
+
+class PaymentMethods(ListableApiResource):
+ resource_name = 'payments/methods'
View
92 bigcommerce/api/resources/products.py
@@ -0,0 +1,92 @@
+from base import *
+
+class Products(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'products'
+
+ def configurable_fields(self, id=None):
+ if id:
+ return ProductConfigurableFields.get(self.id, id)
+ else:
+ return ProductConfigurableFields.all(self.id)
+
+ def custom_fields(self, id=None):
+ if id:
+ return ProductCustomFields.get(self.id, id)
+ else:
+ return ProductCustomFields.all(self.id)
+
+ def discount_rules(self, id=None):
+ if id:
+ return ProductDiscountRules.get(self.id, id)
+ else:
+ return ProductDiscountRules.all(self.id)
+
+ def images(self, id=None):
+ if id:
+ return ProductImages.get(self.id, id)
+ else:
+ return ProductImages.all(self.id)
+
+ def options(self, id=None):
+ if id:
+ return ProductOptions.get(self.id, id)
+ else:
+ return ProductOptions.all(self.id)
+
+ def rules(self, id=None):
+ if id:
+ return ProductRules.get(self.id, id)
+ else:
+ return ProductRules.all(self.id)
+
+ def skus(self, id=None):
+ if id:
+ return ProductSkus.get(self.id, id)
+ else:
+ return ProductSkus.all(self.id)
+
+ def videos(self, id=None):
+ if id:
+ return ProductVideos.get(self.id, id)
+ else:
+ return ProductVideos.all(self.id)
+
+class ProductConfigurableFields(ListableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'configurable_fields'
+ parent_resource = 'products'
+
+
+class ProductCustomFields(ListableApiSubResource, CreateableApiSubResource, UpdateableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'custom_fields'
+ parent_resource = 'products'
+
+
+class ProductDiscountRules(ListableApiSubResource, CreateableApiSubResource, UpdateableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'discount_rules'
+ parent_resource = 'products'
+
+
+class ProductImages(ListableApiSubResource, CreateableApiSubResource, UpdateableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'images'
+ parent_resource = 'products'
+
+
+class ProductOptions(ListableApiSubResource):
+ resource_name = 'options'
+ parent_resource = 'products'
+
+
+class ProductRules(ListableApiSubResource, CreateableApiSubResource, UpdateableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'rules'
+ parent_resource = 'products'
+
+
+class ProductSkus(ListableApiSubResource, CreateableApiSubResource, UpdateableApiSubResource, DeleteableApiSubResource):
+ resource_name = 'skus'
+ parent_resource = 'products'
+
+
+class ProductVideos(ListableApiSubResource):
+ resource_name = 'videos'
+ parent_resource = 'products'
+
View
4 bigcommerce/api/resources/redirects.py
@@ -0,0 +1,4 @@
+from base import *
+
+class Redirects(ListableApiResource, CreateableApiResource, UpdateableApiResource, DeleteableApiResource):
+ resource_name = 'redirects'
View
4 bigcommerce/api/resources/shipping.py
@@ -0,0 +1,4 @@
+from base import *
+
+class ShippingMethods(ListableApiResource):
+ resource_name = 'shipping/methods'
View
4 bigcommerce/api/resources/store.py
@@ -0,0 +1,4 @@
+from base import *
+
+class Store(ListableApiResource):
+ resource_name = 'store'
View
4 bigcommerce/api/resources/tax_classes.py
@@ -0,0 +1,4 @@
+from base import *
+
+class TaxClasses(ListableApiResource):
+ resource_name = 'tax_classes'
View
4 bigcommerce/api/resources/time.py
@@ -0,0 +1,4 @@
+from base import *
+
+class Time(ListableApiResource):
+ resource_name = 'time'

0 comments on commit 0790fc5

Please sign in to comment.