Source code for fnapy_manager

#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2016  <>
#
# Distributed under terms of the MIT license.

"""

**fnapy** is a Python library using the FnacMarketPlace API to connect to your
own sales application to your FnacMarketplace seller account.  It uses the REST
WebService protocol to exchange data.

"""

# Python modules
from string import Template

# Third-party modules
import requests

# Project modules
from utils import *
from config import REQUEST_ELEMENTS, URL, XHTML_NAMESPACE, HEADERS, XML_OPTIONS


def _create_docstring(query_type):
    """Create the docstring for a given query method"""
    _query_docstring = Template(FnapyManager._query.__doc__)
    service = query_type + '_query'
    param_fmt = ':param {param.name}: {param.desc}'.format
    parameters = "\n\t".join(param_fmt(param=param) for param in REQUEST_ELEMENTS[service])
    return _query_docstring.substitute(query_type=query_type,
                                       parameters=parameters)


[docs]class FnapyManager(object): """A class to manage the different services provided by the FNAC API""" VALID_QUERY_TYPES = ('offers', 'orders', 'client_order_comments', 'messages', 'incidents', 'shop_invoices') def __init__(self, connection): """Initialize the manager""" self.connection = connection self.auth_request = None self.offers_query_request = None self.offers_update_request = None self.orders_query_request = None self.batch_query_request = None self.batch_status_request = None self.orders_update_request = None self.carriers_query_request = None self.client_order_comments_query_request = None self.client_order_comments_update_request = None self.messages_query_request = None self.messages_update_request = None self.incidents_query_request = None self.incidents_update_request = None self.pricing_query_request = None self.shop_invoices_query_request = None # The batch_id updated every time an offer is updated self.batch_id = None # Authenticate when the manager is instanciated self.authenticate() def _get_response(self, element, xml): """Send the request and return the response as a dictionary :type element: lxml.etree.Element :param element: the XML element :type xml: str :param xml: the XML string sent in the request :returns: :class:`Response <Response>` object """ service = element.tag response = requests.post(URL + service, xml, headers=HEADERS) response = Response(response.text) if response.dict.get(service + '_response', {}).get('error'): print 'The token expired. Reauthenticating...' # Reauthenticate and update the element element.attrib['token'] = self.authenticate() setattr(self, service + '_request', Request(etree.tostring(element, **XML_OPTIONS))) # Resend the updated request response = requests.post(URL + service, getattr(self, service + '_request').xml, headers=HEADERS) response = Response(response.text) return response
[docs] def authenticate(self): """Authenticate to the FNAC API and return a token Usage:: token = manager.authenticate() :returns: token :rtype: str """ auth = etree.Element('auth', nsmap={None: XHTML_NAMESPACE}) etree.SubElement(auth, 'partner_id').text = self.connection.partner_id etree.SubElement(auth, 'shop_id').text = self.connection.shop_id etree.SubElement(auth, 'key').text = self.connection.key self.auth_request = Request(etree.tostring(auth, **XML_OPTIONS)) response = requests.post(URL + 'auth', self.auth_request.xml, headers=HEADERS) self.token = parse_xml(response, 'token') return self.token
# TODO Allow update_offers to delete an offer # TODO Create a dictionary for the product_state
[docs] def update_offers(self, offers_data): """Post the update offers and return the response Usage:: response = manager.update_offers(offers_data) :type offers_data: list :param offers_data: the list of data to create the offers where data is dictionary with the keys: * offer_reference : the SKU (mandatory) * product_reference: the EAN * price : the price of the offer * product_state : an integer representing the state of the product (documentation needed) * quantity : the quantity * description : (optional) a description of the offer :returns: :class:`Response <Response>` object """ offers_update = create_xml_element(self.connection, self.token, 'offers_update') for offer_data in offers_data: offer = create_offer_element(**offer_data) offers_update.append(offer) self.offers_update_request = Request(etree.tostring(offers_update, **XML_OPTIONS)) # the response contains the element batch_id response = self._get_response(offers_update, self.offers_update_request.xml) self.batch_id = response.dict['offers_update_response']['batch_id'] return response
# TODO Improve the documentation
[docs] def update_orders(self, order_id, order_update_action, actions): """Update the selected order with an order_update_action Usage:: response = manager.update_orders(order_id, order_update_action, actions) :type order_id: str :param order_id: Order unique identifier from FNAC :type order_update_action: str :param order_update_action: Group action type for order detail action :type actions: list :param actions: a list of dictionaries with 2 keys: `'order_detail_id'` and `'action'` :returns: :class:`Response <Response>` object Available order_update_action: * accept_order : The action for the order is accepting orders by the seller * confirm_to_send : The action for the order is confirming sending orders by the seller * update : The action for the order is updating orders by the seller * accept_all_orders : The action for the order is accepting or refusing all order_details of the order by the seller * confirm_all_to_send: The action for the order is confirming sending all order_details by the seller * update_all : The action for the order is to update tracking information for all order_details Example: For this order (whose `order_id` is `'LDJEDEAS123'`), we have 2 items. We decide to accept the first item and refuse the second:: action1 = {"order_detail_id": 1, "action": "Accepted"} action2 = {"order_detail_id": 2, "action": "Refused"} response = manager.update_orders('LDJEDEAS123', 'accept_order', [action1, action2]) """ order_id = str(order_id) orders_update = create_xml_element(self.connection, self.token, 'orders_update') order = etree.Element('order', order_id=order_id, action=order_update_action) for action in actions: order_detail = etree.Element("order_detail") etree.SubElement(order_detail, 'order_detail_id').text = str(action['order_detail_id']) etree.SubElement(order_detail, 'action').text = str(action['action']) order.append(order_detail) orders_update.append(order) self.orders_update_request = Request(etree.tostring(orders_update, **XML_OPTIONS)) return self._get_response(orders_update, self.orders_update_request.xml)
# FIXME The batch_status_response doesn't contain the attributes (status)
[docs] def get_batch_status(self, batch_id=None): """Return the status for the given batch id Usage:: response = manager.get_batch_status(batch_id=batch_id) ..note:: :class:`FnapyManager <FnapyManager>` stores the last `batch_id` but you can provide a new one if needed. :param batch_id: the batch id (optional) :returns: :class:`Response <Response>` object """ if batch_id is not None: self.batch_id = batch_id batch_status = create_xml_element(self.connection, self.token, 'batch_status') etree.SubElement(batch_status, 'batch_id').text = self.batch_id self.batch_status_request = Request(etree.tostring(batch_status, **XML_OPTIONS)) return self._get_response(batch_status, self.batch_status_request.xml)
# TODO Implement this method that should handle any arguments to create # the xml properly def _check_elements(self, valid_elements, selected_elements): for element in selected_elements: if element not in (x.name for x in valid_elements): raise ValueError('{} is not a valid element.'.format(element)) def _query(self, query_type, results_count='', **elements): """Query your catalog and return the ${query_type} response Usage:: response = manager.query_${query_type}(results_count=results_count, **elements) The available XML elements are the following parameters: ${parameters} :returns: :class:`Response <Response>` object Examples: Find the 2 first items of the catalog:: response = manager.query_${query_type}(results_count=2, paging=1) Find the ${query_type} created between 2 dates:: >>> from fnapy.utils import Query >>> date = Query('date', type='Modified')\ .between(min="2016-08-23T17:00:00+00:00", max="2016-08-26T17:00:00+00:00") >>> response = manager.query_${query_type}(date=date) """ print 'Querying {}...'.format(query_type) if query_type in FnapyManager.VALID_QUERY_TYPES: query_type += "_query" else: raise ValueError("The query_type must be in {}".format(FnapyManager.VALID_QUERY_TYPES)) # TODO Refactor: Use a dictionary to prevent code duplication # Check the queried elements self._check_elements(REQUEST_ELEMENTS[query_type], elements.keys()) # Make sure we have unicode # paging = str(paging).decode('utf-8') results_count = str(results_count).decode('utf-8') # Create the XML element query = create_xml_element(self.connection, self.token, query_type) if results_count: query.attrib['results_count'] = results_count # Create the XML from the queried elements if len(elements): for key, value in elements.iteritems(): # Handle cases where Query is used if isinstance(value, Query): value = value.dict d = {key: value} queried_elements = etree.XML(dict2xml(d)) query.append(queried_elements) setattr(self, query_type + '_request', Request(etree.tostring(query, **XML_OPTIONS))) query_xml = getattr(self, query_type + '_request').xml return self._get_response(query, query_xml) # TODO generator for the paging # TODO Allow to specify the type of date ('Created', 'Modified'...)
[docs] def query_offers(self, results_count='', **elements): return self._query('offers', results_count, **elements)
[docs] def query_orders(self, results_count='', **elements): return self._query('orders', results_count, **elements)
[docs] def query_pricing(self, ean, sellers="all"): """Compare price between all marketplace shop and fnac for a specific product (designated by its ean) Usage:: response = manager.query_pricing(ean, sellers=sellers) :returns: response """ pricing_query = create_xml_element(self.connection, self.token, 'pricing_query') pricing_query.attrib['sellers'] = sellers product_reference = etree.Element("product_reference", type="Ean") product_reference.text = str(ean) pricing_query.append(product_reference) self.pricing_query_request = Request(etree.tostring(pricing_query, **XML_OPTIONS)) return self._get_response(pricing_query, self.pricing_query_request.xml)
[docs] def query_batch(self): """Return information about your currently processing import batches Usage:: response = manager.query_batch() :returns: :class:`Response <Response>` object """ batch_query = create_xml_element(self.connection, self.token, 'batch_query') self.batch_query_request = Request(etree.tostring(batch_query, **XML_OPTIONS)) return self._get_response(batch_query, self.batch_query_request.xml)
[docs] def query_carriers(self): """Return the available carriers managed on FNAC Marketplace platform Usage:: response = manager.query_carriers() :returns: :class:`Response <Response>` object """ carriers_query = create_xml_element(self.connection, self.token, 'carriers_query') etree.SubElement(carriers_query, "query").text = etree.CDATA("all") self.carriers_query_request = Request(etree.tostring(carriers_query, **XML_OPTIONS)) return self._get_response(carriers_query, self.carriers_query_request.xml)
[docs] def query_client_order_comments(self, results_count='', **elements): return self._query('client_order_comments', **elements)
[docs] def update_client_order_comments(self, seller_comment, order_fnac_id): """Reply to client order comments Usage:: response = manager.update_client_order_comments(seller_comment, order_fnac_id) :type seller_comment: str :param seller_comment: The seller comment :type order_fnac_id: str :param order_fnac_id: Order unique identifier filter from FNAC :returns: :class:`Response <Response>` object """ client_order_comments_update = create_xml_element(self.connection, self.token, 'client_order_comments_update') comment = etree.Element('comment', id=order_fnac_id) etree.SubElement(comment, 'comment_reply').text = etree.CDATA(seller_comment) client_order_comments_update.append(comment) self.client_order_comments_update_request = \ Request(etree.tostring(client_order_comments_update, **XML_OPTIONS)) return self._get_response(client_order_comments_update, self.client_order_comments_update_request.xml)
[docs] def query_messages(self, results_count='', **elements): return self._query('messages', results_count, **elements)
[docs] def update_messages(self, messages): """Update message sent on your offers or orders : reply, set as read, ... Usage:: response = manager.update_messages(messages) :type messages: Message :param messages: the specified messages we want to update :returns: :class:`Response <Response>` object Example:: >>> m1 = Message(action='mark_as_read', id='12345') >>> m2 = Message(action='reply', id='12345') >>> m2.description = 'Your order has been shipped' >>> m2.subject = 'order_information' >>> m2.type = 'ORDER' >>> response = manager.update_messages([m1, m2]) """ messages_update = create_xml_element(self.connection, self.token, 'messages_update') for m in messages: message = etree.XML(dict2xml(m.to_dict())) messages_update.append(message) self.messages_update_request = Request(etree.tostring(messages_update, **XML_OPTIONS)) return self._get_response(messages_update, self.messages_update_request.xml)
[docs] def query_incidents(self, results_count='', **elements): return self._query('incidents', results_count, **elements)
[docs] def update_incidents(self, order_id, incident_update_action, reasons): """Handle incidents created on orders Usage:: response = manager.update_incidents(order_id, incident_update_action, reasons) :type order_id: str :param order_id: the unique FNAC identified for an order :type incident_update_action: str :param incident_update_action: the action to perform (`'refund'` is the only available action for the moment) :type reasons: list :param reasons: the reasons of the incident for this order Example:: reason = {"order_detail_id": 1, "refund_reason": 'no_stock'} response = manager.update_incidents('07LWQ6278YJUI', 'refund', [reason]) :returns: :class:`Response <Response>` object """ incidents_update = create_xml_element(self.connection, self.token, 'incidents_update') order = etree.Element('order', order_id=order_id, action=incident_update_action) for reason in reasons: order_detail = etree.Element("order_detail") etree.SubElement(order_detail, 'order_detail_id').text = str(reason['order_detail_id']) etree.SubElement(order_detail, 'refund_reason').text = str(reason['refund_reason']) order.append(order_detail) incidents_update.append(order) self.incidents_update_request = \ Request(etree.tostring(incidents_update, **XML_OPTIONS)) return self._get_response(incidents_update, self.incidents_update_request.xml)
[docs] def query_shop_invoices(self, results_count='', **elements): return self._query('shop_invoices', results_count, **elements)
# Dynamically set the docstrings for some query methods for query_type in FnapyManager.VALID_QUERY_TYPES: method = getattr(FnapyManager, 'query_' + query_type) method.__func__.__doc__ = _create_docstring(query_type)