#! /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_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)