├── allegroapi ├── entities │ ├── __init__.py │ ├── users.py │ ├── offers.py │ ├── root.py │ ├── userratingssummary.py │ ├── aftersalesserviceconditions.py │ ├── sale.py │ ├── salecategoryparameters.py │ ├── aftersalesserviceconditionswarranties.py │ ├── aftersalesserviceconditionreturnpolicies.py │ ├── aftersalesserviceconditionsimpliedwarranties.py │ ├── saleimages.py │ ├── saleofferpublicationcommandtasks.py │ ├── saleshippingrates.py │ ├── offerchangepricecommands.py │ ├── saleofferpublicationcommands.py │ ├── salecategories.py │ ├── offerrenewcommands.py │ ├── saleoffervariants.py │ └── saleoffers.py ├── error.py ├── base.py ├── __init__.py ├── oauth.py └── allegroclient.py ├── requirements.txt ├── Makefile ├── setup.py ├── LICENSE ├── .gitignore └── README.md /allegroapi/entities/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | pip install -r requirements.txt 3 | -------------------------------------------------------------------------------- /allegroapi/error.py: -------------------------------------------------------------------------------- 1 | class AllegroError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | setup( 7 | name='allegroapi', 8 | version='0.0.1', 9 | description='Simple wrapper (client) for Allegro.pl REST API written in Python (using requests)', 10 | url='https://github.com/chawel/python-allegro.git', 11 | license=license, 12 | packages=find_packages(exclude=('tests', 'docs')), 13 | ) 14 | -------------------------------------------------------------------------------- /allegroapi/entities/users.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | The Users endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | from .userratingssummary import UserRatingsSummary 12 | 13 | 14 | class Users(BaseApi): 15 | """ 16 | Base Endpoint 17 | """ 18 | def __init__(self, *args, **kwargs): 19 | """ 20 | Initialize the endpoint 21 | """ 22 | super(Users, self).__init__(*args, **kwargs) 23 | self.endpoint = 'users' 24 | self.ratings_summary = UserRatingsSummary(self) 25 | -------------------------------------------------------------------------------- /allegroapi/entities/offers.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | The Offers endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | from .offerrenewcommands import OfferRenewCommands 12 | from .offerchangepricecommands import OfferChangePriceCommands 13 | 14 | 15 | class Offers(BaseApi): 16 | """ 17 | Base Endpoint 18 | """ 19 | def __init__(self, *args, **kwargs): 20 | """ 21 | Initialize the endpoint 22 | """ 23 | super(Offers, self).__init__(*args, **kwargs) 24 | self.endpoint = 'offers' 25 | self.change_price_commands = OfferChangePriceCommands(self) 26 | self.renew_commands = OfferRenewCommands(self) 27 | -------------------------------------------------------------------------------- /allegroapi/entities/root.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | The API Root endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class Root(BaseApi): 14 | """ 15 | The API root resource links to all other resources available in the API. 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(Root, self).__init__(*args, **kwargs) 22 | self.endpoint = '' 23 | 24 | def get(self, **queryparams): 25 | """ 26 | Get links to all other resources available in the API. 27 | 28 | :param queryparams: The query string parameters 29 | """ 30 | return self._a_client._get(url=self._build_path(), **queryparams) 31 | -------------------------------------------------------------------------------- /allegroapi/entities/userratingssummary.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | The User Ratings-Summary endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class UserRatingsSummary(BaseApi): 14 | """ 15 | Get user ratings-summary 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(UserRatingsSummary, self).__init__(*args, **kwargs) 22 | self.endpoint = 'users' 23 | self.user_id = None 24 | 25 | def get(self, user_id): 26 | """ 27 | Get information about specific user's ratings summary 28 | 29 | :param user_id: The user's id 30 | :type user_id: :py:class:`str` 31 | """ 32 | self.user_id = user_id 33 | return self._a_client._get(url=self._build_path(user_id, 'ratings-summary')) 34 | -------------------------------------------------------------------------------- /allegroapi/base.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | The base API object that allows constructions of various endpoint paths 4 | """ 5 | from __future__ import unicode_literals 6 | from itertools import chain 7 | 8 | 9 | class BaseApi(object): 10 | """ 11 | Simple class to buid path for entities 12 | """ 13 | 14 | def __init__(self, a_client): 15 | """ 16 | Initialize the class with you user_id and secret_key 17 | 18 | :param a_client: Allegro.pl client 19 | :type a_client: :mod:`allegroapi.allegroclient.AllegroClient` 20 | """ 21 | super(BaseApi, self).__init__() 22 | self._a_client = a_client 23 | self.endpoint = '' 24 | 25 | def _build_path(self, *args): 26 | """ 27 | Build path with endpoint and args 28 | 29 | :param args: Path elements in the endpoint URL 30 | :type args: :py:class:`unicode` 31 | """ 32 | return '/'.join(chain((self.endpoint,), map(str, args))) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paweł 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /allegroapi/entities/aftersalesserviceconditions.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | The After Sale Service Conditions endpoint 4 | 5 | 6 | Documentation: https://developer.allegro.pl/documentation/#/ 7 | Schema: 8 | """ 9 | from __future__ import unicode_literals 10 | 11 | from ..base import BaseApi 12 | from .aftersalesserviceconditionreturnpolicies import AfterSaleServiceConditionReturnPolicies 13 | from .aftersalesserviceconditionsimpliedwarranties import AfterSaleServiceConditionImpliedWarranties 14 | from .aftersalesserviceconditionswarranties import AfterSaleServiceConditionWarranties 15 | 16 | 17 | class AfterSaleServiceConditions(BaseApi): 18 | """ 19 | Base Endpoint 20 | """ 21 | def __init__(self, *args, **kwargs): 22 | """ 23 | Initialize the endpoint 24 | """ 25 | super(AfterSaleServiceConditions, self).__init__(*args, **kwargs) 26 | self.endpoint = 'after-sales-service-conditions' 27 | self.return_policies = AfterSaleServiceConditionReturnPolicies(self) 28 | self.implied_warranties = AfterSaleServiceConditionImpliedWarranties(self) 29 | self.warranties = AfterSaleServiceConditionWarranties(self) 30 | -------------------------------------------------------------------------------- /allegroapi/entities/sale.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | The Sale endpoint 4 | 5 | 6 | Documentation: https://developer.allegro.pl/documentation/#/ 7 | Schema: 8 | """ 9 | from __future__ import unicode_literals 10 | 11 | from ..base import BaseApi 12 | from .saleoffers import SaleOffers 13 | from .salecategories import SaleCategories 14 | from .saleshippingrates import SaleShippingRates 15 | from .saleimages import SaleImages 16 | from .saleofferpublicationcommands import SaleOfferPublicationCommands 17 | from .saleoffervariants import SaleOfferVariants 18 | 19 | 20 | class Sale(BaseApi): 21 | """ 22 | Base Endpoint 23 | """ 24 | def __init__(self, *args, **kwargs): 25 | """ 26 | Initialize the endpoint 27 | """ 28 | super(Sale, self).__init__(*args, **kwargs) 29 | self.endpoint = 'sale' 30 | self.offers = SaleOffers(self) 31 | self.categories = SaleCategories(self) 32 | self.shipping_rates = SaleShippingRates(self) 33 | self.images = SaleImages(self) 34 | self.offer_publication_commands = SaleOfferPublicationCommands(self) 35 | self.offer_variants = SaleOfferVariants(self) 36 | 37 | -------------------------------------------------------------------------------- /allegroapi/entities/salecategoryparameters.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The Sale Category Parameters endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class SaleCategoryParameters(BaseApi): 14 | """ 15 | Manage category parameters 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(SaleCategoryParameters, self).__init__(*args, **kwargs) 22 | self.endpoint = 'sale/categories' 23 | self.category_id = None 24 | 25 | # Custom header for this endpoint 26 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 27 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 28 | 29 | def get(self, category_id): 30 | """ 31 | Get parameters of specific category 32 | 33 | :param category_id: The unique id (UUID) for the category. 34 | :type category_id: :py:class:`str` 35 | :return: The JSON response from API or error or None (if 204) 36 | :rtype: :py:class:`dict` or :py:data:`none` 37 | """ 38 | 39 | self.category_id = category_id 40 | return self._a_client._get(url=self._build_path(category_id, 'parameters'), headers=self._headers) 41 | -------------------------------------------------------------------------------- /allegroapi/entities/aftersalesserviceconditionswarranties.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The After Sale Service Condition Implied Warranties endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class AfterSaleServiceConditionWarranties(BaseApi): 14 | """ 15 | Manage category tree 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(AfterSaleServiceConditionWarranties, self).__init__(*args, **kwargs) 22 | self.endpoint = '/after-sales-service-conditions/warranties' 23 | self.seller_id = None 24 | 25 | # Custom header for this endpoint 26 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 27 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 28 | 29 | def all(self, seller_id=None): 30 | """ 31 | Get a list of categories 32 | 33 | :param seller_id: The seller's unique id. 34 | :type seller_id: :py:class:`str` 35 | :return: The JSON response from API or error or None (if 204) 36 | :rtype: :py:class:`dict` or :py:data:`none` 37 | """ 38 | 39 | self.seller_id = seller_id 40 | _params = {'sellerId': seller_id} 41 | # TODO: Maybe iterate or pagination? 42 | return self._a_client._get(url=self._build_path(), params=_params, headers=self._headers) 43 | 44 | -------------------------------------------------------------------------------- /allegroapi/entities/aftersalesserviceconditionreturnpolicies.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The After Sale Service Condition Return Policies endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class AfterSaleServiceConditionReturnPolicies(BaseApi): 14 | """ 15 | Manage category tree 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(AfterSaleServiceConditionReturnPolicies, self).__init__(*args, **kwargs) 22 | self.endpoint = '/after-sales-service-conditions/return-policies' 23 | self.seller_id = None 24 | 25 | # Custom header for this endpoint 26 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 27 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 28 | 29 | def all(self, seller_id=None): 30 | """ 31 | Get a list of categories 32 | 33 | :param seller_id: The seller's unique id. 34 | :type seller_id: :py:class:`str` 35 | :return: The JSON response from API or error or None (if 204) 36 | :rtype: :py:class:`dict` or :py:data:`none` 37 | """ 38 | 39 | self.seller_id = seller_id 40 | _params = {'sellerId': seller_id} 41 | # TODO: Maybe iterate or pagination? 42 | return self._a_client._get(url=self._build_path(), params=_params, headers=self._headers) 43 | 44 | -------------------------------------------------------------------------------- /allegroapi/entities/aftersalesserviceconditionsimpliedwarranties.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The After Sale Service Condition Implied Warranties endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class AfterSaleServiceConditionImpliedWarranties(BaseApi): 14 | """ 15 | Manage category tree 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(AfterSaleServiceConditionImpliedWarranties, self).__init__(*args, **kwargs) 22 | self.endpoint = '/after-sales-service-conditions/implied-warranties' 23 | self.seller_id = None 24 | 25 | # Custom header for this endpoint 26 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 27 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 28 | 29 | def all(self, seller_id=None): 30 | """ 31 | Get a list of categories 32 | 33 | :param seller_id: The seller's unique id. 34 | :type seller_id: :py:class:`str` 35 | :return: The JSON response from API or error or None (if 204) 36 | :rtype: :py:class:`dict` or :py:data:`none` 37 | """ 38 | 39 | self.seller_id = seller_id 40 | _params = {'sellerId': seller_id} 41 | # TODO: Maybe iterate or pagination? 42 | return self._a_client._get(url=self._build_path(), params=_params, headers=self._headers) 43 | 44 | -------------------------------------------------------------------------------- /allegroapi/entities/saleimages.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The Sale Images endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class SaleImages(BaseApi): 14 | """ 15 | Manage offer images 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(SaleImages, self).__init__(*args, **kwargs) 22 | self.endpoint = 'sale/images' 23 | 24 | # Custom header for this endpoint 25 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 26 | 'Content-type': 'image/jpeg'} 27 | 28 | def upload(self, image_location, image_type='JPG'): 29 | """ 30 | Upload local image 31 | ex. upload("/imgs/1.jpg", "JPG") 32 | 33 | :param image_location: The location of image file (must not be URL but local!) 34 | :type image_location: :py:class:`str` 35 | :param image_type: (JPG, PNG, GIF) Image format for proper headers 36 | :type image_type: :py:class:`str` 37 | :return: The JSON response from API or error or None (if 204) 38 | :rtype: :py:class:`dict` or :py:data:`none` 39 | """ 40 | image_format_map = {'JPG': 'image/jpeg', 41 | 'PNG': 'image/png', 42 | 'GIF': 'image/gif'} 43 | 44 | self._headers['Content-type'] = image_format_map.get(image_type) 45 | 46 | with open(image_location, 'rb') as f: 47 | _file_read = f.read() 48 | 49 | return self._a_client._post(url=self._build_path(), data=_file_read, headers=self._headers) 50 | 51 | -------------------------------------------------------------------------------- /allegroapi/entities/saleofferpublicationcommandtasks.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8#346 Podpinam się pod ten temat, bo podobny. 2 | """ 3 | [BETA] The Sale Offer Publication Command Tasks endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class SaleOfferPublicationCommandTasks(BaseApi): 14 | """ 15 | Manage publication offer tasks 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(SaleOfferPublicationCommandTasks, self).__init__(*args, **kwargs) 22 | self.endpoint = 'sale/offer-publication-commands' 23 | self.command_uuid = None 24 | 25 | # Custom header for this endpoint 26 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 27 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 28 | 29 | def get(self, command_uuid, limit=100, offset=0): 30 | """ 31 | [BETA] Provides detailed report for single command task 32 | 33 | :param command_uuid: The global unique command ID (UUID) 34 | :type command_uuid: :py:class:`str` 35 | :param limit: Limit for page 36 | :type limit: :py:class:`int` 37 | :param offset: Offset position 38 | :type offset: :py:class:`int` 39 | :return: The JSON response from API or error or None (if 204) 40 | :rtype: :py:class:`dict` or :py:data:`none` 41 | """ 42 | 43 | self.command_uuid = command_uuid 44 | _params = {'limit': limit, 'offset': offset} 45 | return self._a_client._get(url=self._build_path(command_uuid, 'tasks'), params=_params, headers=self._headers) 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | allegroapi-env/ 4 | settings.json 5 | 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | 56 | # Translations 57 | *.mo 58 | *.pot 59 | 60 | # Django stuff: 61 | *.log 62 | local_settings.py 63 | db.sqlite3 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # celery beat schedule file 85 | celerybeat-schedule 86 | 87 | # SageMath parsed files 88 | *.sage.py 89 | 90 | # Environments 91 | .env 92 | .venv 93 | env/ 94 | venv/ 95 | ENV/ 96 | env.bak/ 97 | venv.bak/ 98 | 99 | # Spyder project settings 100 | .spyderproject 101 | .spyproject 102 | 103 | # Rope project settings 104 | .ropeproject 105 | 106 | # mkdocs documentation 107 | /site 108 | 109 | # mypy 110 | .mypy_cache/ 111 | -------------------------------------------------------------------------------- /allegroapi/entities/saleshippingrates.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The Sale Shipping Rates endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class SaleShippingRates(BaseApi): 14 | """ 15 | Manage shipping rates (cenniki dostaw) 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(SaleShippingRates, self).__init__(*args, **kwargs) 22 | self.endpoint = 'sale/shipping-rates' 23 | self.seller_id = None 24 | self.shipping_rate_id = None 25 | 26 | # Custom header for this endpoint 27 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 28 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 29 | 30 | def get(self, shipping_rate_id): 31 | """ 32 | Get information about specific shipping rate 33 | 34 | :param shipping_rate_id: The unique id (UUID) for the shipping rate. 35 | :type shipping_rate_id: :py:class:`str` 36 | :return: The JSON response from API or error or None (if 204) 37 | :rtype: :py:class:`dict` or :py:data:`none` 38 | """ 39 | 40 | self.shipping_rate_id = shipping_rate_id 41 | self.seller_id = None 42 | return self._a_client._get(url=self._build_path(shipping_rate_id), headers=self._headers) 43 | 44 | def all(self, seller_id): 45 | """ 46 | Get a list of categories 47 | 48 | :param seller_id: The seller's unique id (UUID). 49 | :type seller_id: :py:class:`str` 50 | :return: The JSON response from API or error or None (if 204) 51 | :rtype: :py:class:`dict` or :py:data:`none` 52 | """ 53 | 54 | self.seller_id = seller_id 55 | self.shipping_rate_id = None 56 | _params = {'seller.id': seller_id} 57 | # TODO: Maybe iterate or pagination? 58 | return self._a_client._get(url=self._build_path(), params=_params, headers=self._headers) 59 | 60 | -------------------------------------------------------------------------------- /allegroapi/entities/offerchangepricecommands.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | The Offer Change Price Commands endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class OfferChangePriceCommands(BaseApi): 14 | """ 15 | Manage offer price change commands 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(OfferChangePriceCommands, self).__init__(*args, **kwargs) 22 | self.endpoint = 'offers' 23 | self.command_uuid = None 24 | self.offer_id = None 25 | 26 | # Custom header for this endpoint 27 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 28 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 29 | 30 | def create(self, offer_id, command_uuid, body): 31 | """ 32 | Use it to change a Buy Now price in a single offer 33 | 34 | :param command_uuid: The global unique ID (UUID) for this specific command 35 | :type command_uuid: :py:class:`str` 36 | :param offer_id: The unique id for the offer. 37 | :type offer_id: :py:class:`str` 38 | :param body: The request's body, Command input data. 39 | Note that the amount field must be transferred as a string to avoid rounding errors. 40 | A currency must be provided as a 3-letter code as defined in ISO 4217. 41 | (https://en.wikipedia.org/wiki/ISO_4217#Active_codes) 42 | :type body: :py:class:`dict` 43 | :return: The JSON response from API or error or None (if 204) 44 | :rtype: :py:class:`dict` or :py:data:`none` 45 | """ 46 | 47 | self.offer_id = offer_id 48 | self.command_uuid = command_uuid 49 | if not isinstance(body, dict): 50 | raise KeyError('The command must have a data') 51 | return self._a_client._put(url=self._build_path(offer_id, 'change-price-commands'), json=body, headers=self._headers) 52 | 53 | # def get(self, command_uuid): 54 | # pass 55 | 56 | -------------------------------------------------------------------------------- /allegroapi/entities/saleofferpublicationcommands.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The Sale Offer Publication Commands endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | from .saleofferpublicationcommandtasks import SaleOfferPublicationCommandTasks 12 | 13 | 14 | class SaleOfferPublicationCommands(BaseApi): 15 | """ 16 | Manage offer commands 17 | """ 18 | def __init__(self, *args, **kwargs): 19 | """ 20 | Initialize the endpoint 21 | """ 22 | super(SaleOfferPublicationCommands, self).__init__(*args, **kwargs) 23 | self.endpoint = 'sale/offer-publication-commands' 24 | self.command_uuid = None 25 | self.tasks = SaleOfferPublicationCommandTasks(self) 26 | 27 | # Custom header for this endpoint 28 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 29 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 30 | 31 | def create(self, command_uuid, body): 32 | """ 33 | Allows modification of multiple offers' publication at once. 34 | 35 | :param command_uuid: The global unique ID (UUID) for this specific command 36 | :type command_uuid: :py:class:`str` 37 | :param body: The request body parameters 38 | :type body: :py:class:`dict` 39 | :return: The JSON response from API or error or None (if 204) 40 | :rtype: :py:class:`dict` or :py:data:`none` 41 | """ 42 | 43 | self.command_uuid = command_uuid 44 | if not isinstance(body, dict): 45 | raise KeyError('The command must have a data') 46 | return self._a_client._put(url=self._build_path(command_uuid), json=body, headers=self._headers) 47 | 48 | def get(self, command_uuid): 49 | """ 50 | Provides report summary for given command id 51 | 52 | :param command_uuid: The global unique command ID (UUID) 53 | :type command_uuid: :py:class:`str` 54 | :return: The JSON response from API or error or None (if 204) 55 | :rtype: :py:class:`dict` or :py:data:`none` 56 | """ 57 | 58 | self.command_uuid = command_uuid 59 | return self._a_client._get(url=self._build_path(command_uuid), headers=self._headers) 60 | 61 | -------------------------------------------------------------------------------- /allegroapi/entities/salecategories.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The Sale Category endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | from .salecategoryparameters import SaleCategoryParameters 12 | 13 | 14 | class SaleCategories(BaseApi): 15 | """ 16 | Manage category tree 17 | """ 18 | def __init__(self, *args, **kwargs): 19 | """ 20 | Initialize the endpoint 21 | """ 22 | super(SaleCategories, self).__init__(*args, **kwargs) 23 | self.endpoint = 'sale/categories' 24 | self.parent_id = None 25 | self.category_id = None 26 | self.parameters = SaleCategoryParameters(self) 27 | 28 | # Custom header for this endpoint 29 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 30 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 31 | 32 | def get(self, category_id): 33 | """ 34 | Get information about specific category 35 | 36 | :param category_id: The unique id (UUID) for the category. 37 | :type category_id: :py:class:`str` 38 | :return: The JSON response from API or error or None (if 204) 39 | :rtype: :py:class:`dict` or :py:data:`none` 40 | """ 41 | 42 | self.category_id = category_id 43 | self.parent_id = None 44 | return self._a_client._get(url=self._build_path(category_id), headers=self._headers) 45 | 46 | def all(self, parent_id=None, limit=100, offset=0): 47 | """ 48 | Get a list of categories 49 | 50 | :param parent_id: The unique id (UUID) for the parent category. 51 | :type parent_id: :py:class:`str` 52 | :param limit: Limit for page 53 | :type limit: :py:class:`int` 54 | :param offset: Offset position 55 | :type offset: :py:class:`int` 56 | :return: The JSON response from API or error or None (if 204) 57 | :rtype: :py:class:`dict` or :py:data:`none` 58 | """ 59 | 60 | self.parent_id = parent_id 61 | self.category_id = None 62 | _params = {'parent.id': parent_id, 'limit': limit, 'offset': offset} 63 | # TODO: Maybe iterate or pagination? 64 | return self._a_client._get(url=self._build_path(), params=_params, headers=self._headers) 65 | 66 | -------------------------------------------------------------------------------- /allegroapi/entities/offerrenewcommands.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The Offer Renew Commands endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class OfferRenewCommands(BaseApi): 14 | """ 15 | Manage offer renew commands 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(OfferRenewCommands, self).__init__(*args, **kwargs) 22 | self.endpoint = 'offers' 23 | self.command_uuid = None 24 | self.offer_id = None 25 | 26 | # Custom header for this endpoint 27 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 28 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 29 | 30 | def create(self, offer_id, command_uuid, body): 31 | """ 32 | Renew a single offer by id. 33 | 34 | :param offer_id: The unique id for the offer. 35 | :type offer_id: :py:class:`str` 36 | :param command_uuid: The global unique ID (UUID) for this specific command 37 | :type command_uuid: :py:class:`str` 38 | :param body: The request's body 39 | :type body: :py:class:`dict` 40 | :return: The JSON response from API or error or None (if 204) 41 | :rtype: :py:class:`dict` or :py:data:`none` 42 | """ 43 | 44 | self.offer_id = offer_id 45 | self.command_uuid = command_uuid 46 | if not isinstance(body, dict): 47 | raise KeyError('The command must have a data') 48 | return self._a_client._put(url=self._build_path(offer_id, 'renew-commands', command_uuid), 49 | json=body, 50 | headers=self._headers) 51 | 52 | def get(self, offer_id, command_uuid): 53 | """ 54 | Provides report summary for given command id 55 | 56 | :param offer_id: The unique id for the offer. 57 | :type offer_id: :py:class:`str` 58 | :param command_uuid: The global unique command ID (UUID) 59 | :type command_uuid: :py:class:`str` 60 | :return: The JSON response from API or error or None (if 204) 61 | :rtype: :py:class:`dict` or :py:data:`none` 62 | """ 63 | 64 | self.offer_id = offer_id 65 | self.command_uuid = command_uuid 66 | return self._a_client._get(url=self._build_path(offer_id, 'renew-commands', command_uuid), 67 | headers=self._headers) 68 | 69 | 70 | -------------------------------------------------------------------------------- /allegroapi/__init__.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | 3 | # allegroapi 4 | # Copyright 2018 Pawel Chaniewski 5 | # See LICENSE for details. 6 | 7 | """ 8 | Allegro.pl REST API Wrapper 9 | """ 10 | __version__ = '1.0.0' 11 | __author__ = 'Pawel Chaniewski' 12 | __license__ = 'MIT' 13 | 14 | # API Client 15 | from .allegroclient import AllegroClient 16 | 17 | # API Root 18 | from .entities.root import Root 19 | # Offers 20 | from .entities.offers import Offers 21 | from .entities.offerchangepricecommands import OfferChangePriceCommands 22 | from .entities.offerrenewcommands import OfferRenewCommands 23 | # Sale 24 | from .entities.sale import Sale 25 | from .entities.saleoffers import SaleOffers 26 | from .entities.salecategories import SaleCategories 27 | from .entities.salecategoryparameters import SaleCategoryParameters 28 | from .entities.saleshippingrates import SaleShippingRates 29 | from .entities.saleoffervariants import SaleOfferVariants 30 | from .entities.saleofferpublicationcommands import SaleOfferPublicationCommands 31 | from .entities.saleofferpublicationcommandtasks import SaleOfferPublicationCommandTasks 32 | # Sale images 33 | from .entities.saleimages import SaleImages 34 | # After-sale 35 | from .entities.aftersalesserviceconditions import AfterSaleServiceConditions 36 | from .entities.aftersalesserviceconditionswarranties import AfterSaleServiceConditionWarranties 37 | from .entities.aftersalesserviceconditionsimpliedwarranties import AfterSaleServiceConditionImpliedWarranties 38 | from .entities.aftersalesserviceconditionreturnpolicies import AfterSaleServiceConditionReturnPolicies 39 | # Users 40 | from .entities.users import Users 41 | from .entities.userratingssummary import UserRatingsSummary 42 | 43 | 44 | class Allegro(AllegroClient): 45 | """ 46 | Allegro.pl class to communicate with the REST API 47 | """ 48 | 49 | def __init__(self, *args, **kwargs): 50 | """ 51 | Initialize the class with your api_key and user_id and attach all of 52 | the endpoints 53 | """ 54 | super(Allegro, self).__init__(*args, **kwargs) 55 | # API Root 56 | self.root = self.api_root = Root(self) 57 | # Offers 58 | self.offers = Offers(self) 59 | self.offers.change_price_commands = OfferChangePriceCommands(self) 60 | self.offers.renew_commands = OfferRenewCommands(self) 61 | # Sale 62 | self.sale = Sale(self) 63 | self.sale.offers = SaleOffers(self) 64 | self.sale.offer_variants = SaleOfferVariants(self) 65 | self.sale.categories = SaleCategories(self) 66 | self.sale.categories.parameters = SaleCategoryParameters(self) 67 | self.sale.shipping_rates = SaleShippingRates(self) 68 | # Sale Command 69 | self.sale.offer_publication_commands = SaleOfferPublicationCommands(self) 70 | self.sale.offer_publication_commands.tasks = SaleOfferPublicationCommandTasks(self) 71 | # Sale Images 72 | self.sale.images = SaleImages(self) 73 | # After-sale 74 | self.after_sale_conditions = AfterSaleServiceConditions(self) 75 | self.after_sale_conditions.warranties = AfterSaleServiceConditionWarranties(self) 76 | self.after_sale_conditions.implied_warranties = AfterSaleServiceConditionImpliedWarranties(self) 77 | self.after_sale_conditions.return_policies = AfterSaleServiceConditionReturnPolicies(self) 78 | # Users 79 | self.users = Users(self) 80 | self.users.ratings_summary = UserRatingsSummary(self) 81 | -------------------------------------------------------------------------------- /allegroapi/entities/saleoffervariants.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The Sale Offer Variants endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class SaleOfferVariants(BaseApi): 14 | """ 15 | Manage offer variants (aukcje wielowariantowe) 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(SaleOfferVariants, self).__init__(*args, **kwargs) 22 | self.endpoint = 'sale/offer-variants' 23 | self.set_id = None 24 | 25 | # Custom header for this endpoint 26 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 27 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 28 | 29 | def get(self, set_id): 30 | """ 31 | Use this resource to get variant set by set id. 32 | 33 | :param set_id: The unique id (UUID) for the variant set 34 | :type set_id: :py:class:`str` 35 | :return: The JSON response from API or error or None (if 204) 36 | :rtype: :py:class:`dict` or :py:data:`none` 37 | """ 38 | 39 | self.set_id = set_id 40 | return self._a_client._get(url=self._build_path(set_id), headers=self._headers) 41 | 42 | def create(self, set_id, body): 43 | """ 44 | Use this resource to create or update variant set. 45 | 46 | :param set_id: The global unique ID (UUID) for this specific command 47 | :type set_id: :py:class:`str` 48 | :param body: The request body parameters 49 | :type body: :py:class:`dict` 50 | :return: The JSON response from API or error or None (if 204) 51 | :rtype: :py:class:`dict` or :py:data:`none` 52 | 53 | A valid variant set must consist of three required elements: 54 | 55 | name: it can't be blank and must not be longer than 50 characters 56 | 57 | parameters: it should contain parameter identifiers used for offer grouping 58 | parameter identifiers from the offers and special color/pattern value (for grouping via image) are permitted 59 | it must contain at least one element (up to 2) 60 | 61 | offers: it must contain at least 2 offers (500 at most) 62 | 63 | colorPattern value must be set for every offer if color/pattern parameter is used 64 | colorPattern value can't be blank and must not be longer than 50 characters 65 | colorPattern can take arbitrary string value like red, b323592c-522f-4ec1-b9ea-3764538e0ac4 (UUID), etc. 66 | offers having the same image should have identical colorPattern value 67 | """ 68 | 69 | self.set_id = set_id 70 | if not isinstance(body, dict): 71 | raise KeyError('The command must have a data') 72 | return self._a_client._put(url=self._build_path(set_id), json=body, headers=self._headers) 73 | 74 | def delete(self, set_id): 75 | """ 76 | Use this resource to delete variant set by id. 77 | Offers included in variant set will not be stopped or modified by this operation. 78 | 79 | :param set_id: The unique id (UUID) for the variant set 80 | :type set_id: :py:class:`str` 81 | :return: The JSON response from API or error or None (if 204) 82 | :rtype: :py:class:`dict` or :py:data:`none` 83 | """ 84 | 85 | self.set_id = set_id 86 | if not set_id: 87 | raise KeyError('Variant set ID not specified!') 88 | return self._a_client._delete(url=self._build_path(set_id), headers=self._headers) 89 | 90 | -------------------------------------------------------------------------------- /allegroapi/entities/saleoffers.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | [BETA] The Sale Offer endpoint 4 | 5 | Documentation: https://developer.allegro.pl/documentation/#/ 6 | Schema: 7 | """ 8 | from __future__ import unicode_literals 9 | 10 | from ..base import BaseApi 11 | 12 | 13 | class SaleOffers(BaseApi): 14 | """ 15 | Manage Offers 16 | """ 17 | def __init__(self, *args, **kwargs): 18 | """ 19 | Initialize the endpoint 20 | """ 21 | super(SaleOffers, self).__init__(*args, **kwargs) 22 | self.endpoint = 'sale/offers' 23 | self.offer_id = None 24 | 25 | # Custom header for this endpoint 26 | self._headers = {'Accept': 'application/vnd.allegro.public.v1+json', 27 | 'Content-type': 'application/vnd.allegro.public.v1+json'} 28 | 29 | def get(self, offer_id): 30 | """ 31 | Get information about a single offer from Your Allegro.pl account 32 | 33 | :param offer_id: The unique id for the offer. 34 | :type offer_id: :py:class:`str` 35 | :return: The JSON response from API or error or None (if 204) 36 | :rtype: :py:class:`dict` or :py:data:`none` 37 | """ 38 | 39 | self.offer_id = offer_id 40 | return self._a_client._get(url=self._build_path(offer_id), headers=self._headers) 41 | 42 | def update(self, offer_id, body): 43 | """ 44 | Update a single offer (auction or draft) by id. 45 | 46 | Caution! If updating ongoing offer - you must provide ALL fields in `data` parameter 47 | (even for single field update!) 48 | 49 | :param offer_id: The unique id for the offer. 50 | :type offer_id: :py:class:`str` 51 | :param body: The request's body (when updating ongoing offer - provide whole structure of the offer) 52 | :type body: :py:class:`dict` 53 | :return: The JSON response from API or error or None (if 204) 54 | :rtype: :py:class:`dict` or :py:data:`none` 55 | """ 56 | 57 | self.offer_id = offer_id 58 | if not isinstance(body, dict): 59 | raise KeyError('The offer must have a data') 60 | return self._a_client._put(url=self._build_path(offer_id), json=body, headers=self._headers) 61 | 62 | def create(self, body): 63 | """ 64 | Create a new offer or draft. 65 | 66 | :param body: The request body parameters 67 | :type body: :py:class:`dict` 68 | :return: The JSON response from API or error or None (if 204) 69 | :rtype: :py:class:`dict` or :py:data:`none` 70 | """ 71 | if 'name' not in body: 72 | raise KeyError('The offer must have a name') 73 | if 'category' not in body: 74 | raise KeyError('The offer must have category') 75 | if 'id' not in body['category']: 76 | raise KeyError('The offer must have category id') 77 | 78 | response = self._a_client._post(url=self._build_path(), json=body, headers=self._headers) 79 | if response is not None: 80 | self.offer_id = response['id'] 81 | else: 82 | self.offer_id = None 83 | return response 84 | 85 | def all(self, text_query=None, offer_status='ACTIVE', offer_format='BUY_NOW', limit=100, offset=0): 86 | """ 87 | [BETA] Get list of offers from Your Allegro.pl account 88 | 89 | :param text_query: Search query (in offer title). 90 | :type text_query: :py:class:`str 91 | :param offer_status: Publication statuses may contain more than one comma separated values (ex. 'INACTIVE, ACTIVE, ACTIVATING, ENDED') 92 | :type offer_status: :py:class:`str 93 | :param offer_format: Selling mode may contain more than one comma separated values (ex. 'BUY_NOW, ADVERTISEMENT, AUCTION') 94 | :type offer_format: :py:class:`str 95 | :param limit: Limit for page 96 | :type limit: :py:class:`int` 97 | :param offset: Offset position 98 | :type offset: :py:class:`int` 99 | :return: The JSON response from API or error or None (if 204) 100 | :rtype: :py:class:`dict` or :py:data:`none` 101 | """ 102 | 103 | _headers = {'Accept': 'application/vnd.allegro.beta.v1+json', 104 | 'Content-type': 'application/vnd.allegro.beta.v1+json'} 105 | 106 | _params = { 107 | 'name': text_query, 108 | 'publication.status': offer_status, 109 | 'sellingMode.type': offer_format, 110 | # TODO: Implement more filters - not tested yet! 111 | # 'sellingMode.price.amount.gte': offer_min_price, 112 | # 'sellingMode.price.amount.lte': offer_max_price, 113 | 'limit': limit, 114 | 'offset': offset 115 | } 116 | return self._a_client._get(url=self._build_path(), params=_params, headers=_headers) 117 | 118 | 119 | -------------------------------------------------------------------------------- /allegroapi/oauth.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import requests 4 | from requests.auth import HTTPBasicAuth 5 | import webbrowser 6 | 7 | # Handle library reorganisation Python 2 > Python 3. 8 | try: 9 | from http.server import BaseHTTPRequestHandler, HTTPServer 10 | except ImportError: 11 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 12 | 13 | from .error import AllegroError 14 | 15 | 16 | class OAuth2Bearer(requests.auth.AuthBase): 17 | """ 18 | Authentication class for storing access token and api key 19 | """ 20 | def __init__(self, api_key, access_token): 21 | """ 22 | Initialize the OAuth and save the access token 23 | 24 | :param api_key: Api Key 25 | :type api_key: :py:class:`str` 26 | :param access_token: The access token provided by OAuth authentication 27 | :type access_token: :py:class:`str` 28 | """ 29 | self._api_key = api_key 30 | self._access_token = access_token 31 | 32 | def __call__(self, r): 33 | """ 34 | Authorize with the access token and api_key provided in __init__ 35 | """ 36 | # r.headers['Api-Key'] = self._api_key 37 | r.headers['Authorization'] = "Bearer {}".format(self._access_token) 38 | 39 | return r 40 | 41 | 42 | class HTTPServerHandler(BaseHTTPRequestHandler): 43 | """ 44 | HTTP Server callbacks to handle OAuth redirects 45 | """ 46 | 47 | def __init__(self, request, address, server): 48 | # For Python 3.x you can use just super().__init__(...) 49 | super(HTTPServerHandler, self).__init__(request, address, server) 50 | 51 | def do_GET(self): 52 | self.send_response(200) 53 | self.send_header('Content-type', 'text/html') 54 | self.end_headers() 55 | if 'code' in self.path: 56 | self.server.access_code = self.path.rsplit('?code=', 1)[-1] 57 | # Display to the user that they no longer need the browser window 58 | self.wfile.write(bytes('

You may now close this window.' 59 | + '

', 'utf-8')) 60 | 61 | 62 | class AllegroAuthHandler(object): 63 | """ 64 | Class used to handle OAuth2 flow 65 | Documentation: https://developer.allegro.pl/auth/ 66 | """ 67 | 68 | def __init__(self, client_id, client_secret, api_auth_url, redirect_uri, api_key=None, 69 | access_token=None, refresh_token=None, token_expires_in=None): 70 | """ 71 | Initialize the class with required client_id, api_key, api_auth_url and redirect_uri. 72 | 73 | :param client_id: Client_id 74 | :type client_id: :py:class:`str` 75 | :param client_secret: Client_secret 76 | :type client_secret: :py:class:`str` 77 | :param api_key: Api_key (deprecated - not used) 78 | :type api_key: :py:class:`str` 79 | :param api_auth_url: URL to Allegro REST API Auth endpoint 80 | :type api_auth_url: :py:class:`str` 81 | :param redirect_uri: Redirect URI (ex. "http://localhost:8000/") 82 | :type redirect_uri: :py:class:`str` 83 | """ 84 | self._id = client_id 85 | self._secret = client_secret 86 | self._api_key = api_key 87 | # Remove suffix if necessary 88 | self._auth_url = api_auth_url[:-1] if api_auth_url.endswith('/') else api_auth_url 89 | self._redirect_uri = redirect_uri 90 | 91 | self.access_token = access_token 92 | self.refresh_token = refresh_token 93 | self.token_expires_in = token_expires_in 94 | 95 | def get_oauth_url(self): 96 | """ 97 | Returns authentication (OAuth 2) URI generated using provided parameters 98 | :return: URI for authentication 99 | :rtype: :py:class:`str` 100 | """ 101 | base_url = """{auth_url}/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_uri}""" 102 | 103 | # Just in case requote to ensure correct url format 104 | return requests.utils.requote_uri(base_url.format(auth_url=self._auth_url, 105 | client_id=self._id, 106 | redirect_uri=self._redirect_uri)) 107 | 108 | def fetch_oauth_code(self): 109 | """ 110 | Authorize application with use of graphical web browser, returns authorization code 111 | :return: Authorization Code 112 | :rtype: :py:class:`str` 113 | """ 114 | _parsed_redirect_uri = requests.utils.urlparse(self._redirect_uri) 115 | _server_address = _parsed_redirect_uri.hostname, _parsed_redirect_uri.port 116 | _auth_url = self.get_oauth_url() 117 | 118 | httpd = HTTPServer(_server_address, HTTPServerHandler) 119 | 120 | # Open prepared auth url in default web browser 121 | webbrowser.open(_auth_url) 122 | 123 | # Listen for redirected request 124 | httpd.handle_request() 125 | 126 | # Then close our local http server 127 | httpd.server_close() 128 | 129 | # Extract injected access_token variable from HTTPServer class 130 | auth_code = httpd.access_code 131 | 132 | return auth_code 133 | 134 | def fetch_access_token(self, code=None): 135 | """ 136 | Requests access token with provided optional authorization code (if not provided, runs get_oauth_code() method) 137 | :param code: Authorization Code 138 | :type code: :py:data:`none` or :py:class:`str` 139 | :return: Dictionary with Access Token, Refresh Token, Expiration Time 140 | :rtype: :py:class:`dict` 141 | """ 142 | access_code = self.fetch_oauth_code() if code is None else code 143 | 144 | _url = self._auth_url + '/token' 145 | 146 | access_token_data = {'grant_type': 'authorization_code', 147 | 'code': access_code, 148 | 'redirect_uri': self._redirect_uri 149 | } 150 | 151 | try: 152 | r = requests.post(url=_url, 153 | auth=HTTPBasicAuth(self._id, self._secret), 154 | data=access_token_data) 155 | except requests.exceptions.RequestException as e: 156 | raise e 157 | else: 158 | if r.status_code >= 400: 159 | raise AllegroError(r.json()) 160 | 161 | response = r.json() 162 | self.access_token = response['access_token'] 163 | self.refresh_token = response['refresh_token'] 164 | self.token_expires_in = response['expires_in'] 165 | 166 | return response 167 | 168 | def refresh_access_token(self, refresh_token=None): 169 | """ 170 | Gets new access token using provided refresh_token 171 | :param refresh_token: Refresh Token 172 | :type refresh_token: :py:class:`str` 173 | :return: Dictionary with Access Token, Refresh Token, Expiration Time 174 | :rtype: :py:class:`dict` 175 | """ 176 | 177 | _refresh_token = self.refresh_token if refresh_token is None else refresh_token 178 | 179 | _url = self._auth_url + '/token' 180 | 181 | refresh_token_data = {'grant_type': 'refresh_token', 182 | 'refresh_token': _refresh_token, 183 | 'redirect_uri': self._redirect_uri 184 | } 185 | 186 | try: 187 | r = requests.post(url=_url, 188 | auth=HTTPBasicAuth(self._id, self._secret), 189 | data=refresh_token_data) 190 | except requests.exceptions.RequestException as e: 191 | raise e 192 | else: 193 | if r.status_code >= 400: 194 | raise AllegroError(r.json()) 195 | 196 | response = r.json() 197 | self.access_token = response['access_token'] 198 | self.refresh_token = response['refresh_token'] 199 | self.token_expires_in = response['expires_in'] 200 | 201 | return response 202 | 203 | def apply_auth(self): 204 | return OAuth2Bearer(self._api_key, self.access_token) 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-allegro 2 | Simple wrapper (client) for Allegro.pl REST API written in Python (using requests) 3 | 4 | ## Wstęp 5 | README napisane w języku polskim, ze względu na oczywisty "userbase". 6 | 7 | Prosty klient dla REST API platformy Allegro.pl napisany w Python z użyciem biblioteki requests. 8 | Nie korzysta z magii, każdy endpoint jest zdefiniowany. 9 | Obsługuje autentykację OAuth2, automatycznie odświeża token oraz obługuje metody CRUD dla zasobów. 10 | 11 | ## Instalacja 12 | Będąc w katalogu w którym jest plik `setup.py`, wystaczy polecenie: 13 | ```bash 14 | pip install -e . 15 | ``` 16 | 17 | ## Użycie 18 | Potrzebujemy danych dostępowych dla procesu uwierzytelnienia naszej aplikacji. Jak je zdobyć opisałem na blogu: 19 | [Allegro.pl REST API w Pythonie - wprowadzenie (OAuth)](https://cwsi.pl/ecommerce/allegro/allegro-pl-rest-api-w-pythonie-wprowadzenie/#wst%C4%99p) 20 | 21 | ### Autentykacja 22 | Aby korzystać z zasobów REST API Allegro.pl potrzebujemy uzyskać `access_token` służący do uwierzytelnienia naszych żądań. 23 | W tym celu musimy zatańczyć (przejść flow) z OAuth2. Token możemy przekazać podczas instancjonowania klienta 24 | 25 | ```python 26 | from allegroapi import Allegro 27 | 28 | allegro = Allegro(client_id=, 29 | client_secret=, 30 | redirect_uri=, # np. "http://localhost:80" 31 | access_token=, 32 | refresh_token=, 33 | sandbox=True) 34 | ``` 35 | 36 | Jeżeli chcemy by klient przeprowadził nas przez cały OAuth2 flow, należy skorzystać z metody `sign_in()` klasy `Allegro` 37 | 38 | ```python 39 | from allegroapi import Allegro 40 | 41 | allegro = Allegro(client_id=, 42 | client_secret=, 43 | redirect_uri=, # np. "http://localhost:80" 44 | sandbox=True) 45 | 46 | allegro.sign_in() 47 | ``` 48 | 49 | #### Automatyczne odświeżanie tokena 50 | Można zdefiniować `callback` w przypadku wywołania się automatycznego odświeżenia tokena przez klienta. 51 | Klasa `Allegro` przy napotkaniu na kod odpowiedzi serwera `401 - Unauthorized` spróbuje automatycznie odświeżyć token - w przypadku sukcesu wywoła funkcję przekazaną w parametrze `on_token_refresh_hook`. 52 | Rozwiązanie jest szczególnie przydatne, kiedy przechowujemy `access_token` w bezpiecznym miejscu (np. baza danych) i używamy go stale do uwierzytelnienia. 53 | 54 | Funkcja ta powina konsumować 2 parametry `func(access_token, refresh_token)`. 55 | 56 | ```python 57 | from allegroapi import Allegro 58 | 59 | # ex. save to file 60 | def save_token_on_refresh(access_token, refresh_token): 61 | with open('insecure_tokens.txt', 'w') as token_file: 62 | tokens = access_token + '#' + refresh_token 63 | token_file.write(tokens) 64 | 65 | # ex. read saved token from file 66 | def load_token_on_start(): 67 | with open('insecure_tokens.txt', 'r') as token_file: 68 | tokens = f.readline() 69 | 70 | return {'access_token': tokens.split('#')[0], 'refresh_token': tokens.split('#')[1]} 71 | 72 | _token_container = load_token_on_start() 73 | 74 | allegro = Allegro(client_id=, 75 | client_secret=, 76 | redirect_uri=, # np. "http://localhost:80" 77 | access_token=_token_container.access_token, 78 | refresh_token=_token_container.refresh_token, 79 | on_token_refresh_hook=save_token_on_refresh 80 | sandbox=True) 81 | ``` 82 | 83 | 84 | 85 | ### Korzystanie z zasobów REST API 86 | Uwierzytelniony klient ma dostęp do zasobów REST API platformy Allegro.pl 87 | 88 | #### Przykład użycia: Kategorie 89 | ```python 90 | # --- CATEGORIES --- 91 | # Get main categories 92 | print(allegro.sale.categories.all()) 93 | # Get child categories with provided parent_id 94 | print(allegro.sale.categories.all(parent_id='122233')) 95 | # Get specific category tree 96 | print(allegro.sale.categories.get('122233')) 97 | # Get specific category 98 | print(allegro.sale.categories.get('122285')) 99 | # Get category parameters 100 | print(allegro.sale.categories.parameters.get('122285')) 101 | ``` 102 | 103 | #### OUTPUT: 104 | ``` 105 | {'categories': [{'id': 'bfad3525-dd91-491a-a66f-036c77ca3269', 'name': 'Dom i zdrowie', 'parent': None, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '11763', 'name': 'Dziecko', 'parent': None, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '42540aec-367a-4e5e-b411-17c09b08e41f', 'name': 'Elektronika', 'parent': None, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '4bd97d96-f0ff-46cb-a52c-2992bd972bb1', 'name': 'Firma i usługi', 'parent': None, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': 'a408e75a-cede-4587-8526-54e9be600d9f', 'name': 'Kolekcje i sztuka', 'parent': None, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '38d588fd-7e9c-4c42-a4ae-6831775eca45', 'name': 'Kultura i rozrywka', 'parent': None, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': 'ea5b98dd-4b6f-4bd0-8c80-22c2629132d0', 'name': 'Moda i uroda', 'parent': None, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '3', 'name': 'Motoryzacja', 'parent': None, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '889eca47-1df3-40f4-a655-150d6938488e', 'name': 'Sport i wypoczynek', 'parent': None, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}]} 106 | {'categories': [{'id': '122237', 'name': 'Game Boy', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122234', 'name': 'Game Boy Advance', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122240', 'name': 'Microsoft Xbox', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122243', 'name': 'Microsoft Xbox 360', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '147134', 'name': 'Microsoft Xbox One', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122255', 'name': 'Nintendo 3DS', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122256', 'name': 'Nintendo 64', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122257', 'name': 'Nintendo DS', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122261', 'name': 'Nintendo GameCube', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '256862', 'name': 'Nintendo Switch', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '125422', 'name': 'Nintendo Wii U', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122264', 'name': 'Nintendo (SNES i NES)', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122267', 'name': 'Nintendo Wii', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122273', 'name': 'Sega Dreamcast', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122274', 'name': 'Sega (inne)', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122292', 'name': 'Sony PlayStation (PSX)', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122275', 'name': 'Sony PlayStation 2 (PS2)', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122284', 'name': 'Sony PlayStation 3 (PS3)', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '146562', 'name': 'Sony PlayStation 4 (PS4)', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122300', 'name': 'Sony PS Vita', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122301', 'name': 'Sony PSP', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122317', 'name': "'Gierki' elektroniczne", 'parent': {'id': '122233'}, 'leaf': True, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122318', 'name': 'Pegasus', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122319', 'name': 'Automaty do gier', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '253040', 'name': 'Usługi', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}, {'id': '122329', 'name': 'Pozostałe', 'parent': {'id': '122233'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}}]} 107 | {'id': '122233', 'name': 'Konsole i automaty', 'parent': {'id': '42540aec-367a-4e5e-b411-17c09b08e41f'}, 'leaf': False, 'options': {'advertisement': False, 'advertisementPriceOptional': False}} 108 | {'id': '122285', 'name': 'Konsole', 'parent': {'id': '122284'}, 'leaf': True, 'options': {'advertisement': False, 'advertisementPriceOptional': False}} 109 | {'parameters': [{'id': '11323', 'name': 'Stan', 'type': 'dictionary', 'required': True, 'unit': None, 'dictionary': [{'id': '11323_1', 'value': 'Nowy'}, {'id': '11323_2', 'value': 'Używany'}], 'restrictions': {'multipleChoices': False}}, {'id': '17448', 'name': 'Waga (z opakowaniem)', 'type': 'float', 'required': False, 'unit': 'kg', 'restrictions': {'min': 0, 'max': 2147483647, 'range': False, 'precision': 2}}, {'id': '3306', 'name': 'Wersja Playstation 3', 'type': 'dictionary', 'required': False, 'unit': None, 'dictionary': [{'id': '3306_10', 'value': 'Classic'}, {'id': '3306_20', 'value': 'Slim'}, {'id': '3306_21', 'value': 'Super Slim'}], 'restrictions': {'multipleChoices': False}}, {'id': '5178', 'name': 'Dodatkowe informacje', 'type': 'dictionary', 'required': False, 'unit': None, 'dictionary': [{'id': '5178_8', 'value': 'Dwa pady w zestawie'}, {'id': '5178_32', 'value': 'Gry w zestawie'}], 'restrictions': {'multipleChoices': True}}, {'id': '5179', 'name': 'Pojemność dysku', 'type': 'dictionary', 'required': False, 'unit': None, 'dictionary': [{'id': '5179_51', 'value': '500 GB'}, {'id': '5179_53', 'value': '320 GB'}, {'id': '5179_10', 'value': '250 GB'}, {'id': '5179_20', 'value': '120 GB'}, {'id': '5179_30', 'value': '80 GB'}, {'id': '5179_40', 'value': '60 GB'}, {'id': '5179_52', 'value': '12 GB'}, {'id': '5179_50', 'value': 'Inna'}], 'restrictions': {'multipleChoices': False}}]} 110 | ``` 111 | -------------------------------------------------------------------------------- /allegroapi/allegroclient.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """ 3 | Allegro REST API client 4 | 5 | Documentation: https://developer.allegro.pl/about/#rest-api 6 | """ 7 | from __future__ import unicode_literals 8 | 9 | import requests 10 | import logging 11 | from .oauth import AllegroAuthHandler 12 | from .error import AllegroError 13 | 14 | _logger = logging.getLogger(__name__) 15 | 16 | API_URL = 'https://api.allegro.pl' 17 | OAUTH_URL = 'https://allegro.pl/auth/oauth' 18 | 19 | SANDBOX_API_URL = 'https://api.allegro.pl.allegrosandbox.pl' 20 | SANDBOX_OAUTH_URL = 'https://allegro.pl.allegrosandbox.pl/auth/oauth' 21 | 22 | 23 | class AllegroClient(object): 24 | """ 25 | Core class used to connect and handle basic communication with Allegro.pl REST API 26 | """ 27 | def __init__(self, client_id, client_secret, redirect_uri, sandbox=True, api_key=None, 28 | timeout=None, request_headers=None, request_hooks=None, 29 | access_token=None, refresh_token=None, on_token_refresh_hook=None): 30 | """ 31 | Initialize the class with your client_id, client_secret, api_key, redirect_uri 32 | 33 | If you have `access_token` or / and `refresh_token` provide them as optional arguments 34 | If you want acquire them for the first time, use `sign_in()` method 35 | 36 | If `sandbox` is True, these client connects to Allegro.pl Sandbox REST API. 37 | For more info: https://developer.allegro.pl/about/#Sandbox 38 | 39 | :param client_id: Client ID 40 | :type client_id: :py:class:`str` 41 | :param client_secret: Client Secret 42 | :type client_secret: :py:class:`str` 43 | :param api_key: API Key (deprecated - not used) 44 | :type api_key: :py:class:`str` 45 | :param redirect_uri: Redirect URI 46 | :type redirect_uri: :py:class:`str` 47 | :param sandbox: Boolean that controls whether we connect to sandbox or production. Default is True (connect to sandbox) 48 | :type sandbox: :py:class:`bool` 49 | :param timeout: How many seconds to wait for the server to send data before giving up 50 | :type timeout: float or tuple 51 | :param request_headers: Dictionary of HTTP Headers to send with the Request (override defaults) 52 | :type request_headers: :py:class:`dict` 53 | :param request_hooks: Request Event-handling hooks 54 | :type request_hooks: :py:class:`dict` 55 | :param access_token: Access Token (only valid with correct credentials) 56 | :type access_token: :py:class:`str` 57 | :param refresh_token: Refresh Token (provide if you wish to use automatic refresh function) 58 | :type refresh_token: :py:class:`str` 59 | :param on_token_refresh_hook: Pass here function with 2 parameters (access_token, refresh_token) 60 | to be executed every time token has been refreshed 61 | :type on_token_refresh_hook: func(access_token, refresh_token) 62 | """ 63 | 64 | # Use super() even for parent class (that inherits from object) 65 | # more info: http://amyboyle.ninja/Python-Inheritance 66 | super(AllegroClient, self).__init__() 67 | 68 | # Set URI's for API Client (sandbox or production) 69 | if sandbox: 70 | self.base_url = SANDBOX_API_URL 71 | self.auth_url = SANDBOX_OAUTH_URL 72 | else: 73 | self.base_url = API_URL 74 | self.auth_url = OAUTH_URL 75 | 76 | self.auth_handler = AllegroAuthHandler(client_id=client_id, 77 | client_secret=client_secret, 78 | api_auth_url=self.auth_url, 79 | redirect_uri=redirect_uri, 80 | api_key=api_key, 81 | access_token=access_token, 82 | refresh_token=refresh_token) 83 | 84 | # Get authentication object (session headers) 85 | self.auth = self.auth_handler.apply_auth() 86 | 87 | self.timeout = timeout 88 | self.request_hooks = request_hooks 89 | 90 | self.request_headers = self._default_headers 91 | 92 | if isinstance(request_headers, dict): 93 | self.request_headers.update(request_headers) 94 | 95 | self._on_token_refresh_hook = on_token_refresh_hook 96 | 97 | def sign_in(self): 98 | """ 99 | Handle OAuth2 client authentication flow (register application) 100 | Using provided credentials and redirect_uri 101 | Authorizes this instance of `AllegroClient` 102 | 103 | :return: The JSON response from API 104 | :rtype: :py:class:`dict` 105 | """ 106 | auth_response = self.auth_handler.fetch_access_token() 107 | self.auth = self.auth_handler.apply_auth() 108 | 109 | return auth_response 110 | 111 | @property 112 | def _default_headers(self): 113 | headers = dict() 114 | headers['charset'] = 'utf-8' 115 | headers['Accept-Language'] = 'pl-PL' 116 | headers['Content-type'] = 'application/json' 117 | headers['Accept'] = 'application/vnd.allegro.public.v1+json' 118 | 119 | return headers 120 | 121 | def _make_request(self, **kwargs): 122 | _logger.info(u'{method} Request: {url}'.format(**kwargs)) 123 | 124 | # Register how many times tried to resend same request 125 | _tries = kwargs.pop('tries', 0) + 1 126 | 127 | if kwargs.get('json'): 128 | _logger.info('PAYLOAD: {json}'.format(**kwargs)) 129 | 130 | if kwargs.get('headers'): 131 | _logger.info('PAYLOAD: {headers}'.format(**kwargs)) 132 | 133 | try: 134 | response = requests.request(**kwargs) 135 | 136 | _logger.info(u'{method} Response: {status} {text}' 137 | .format(method=kwargs['method'], status=response.status_code, text=response.text)) 138 | 139 | except requests.exceptions.RequestException as e: 140 | raise e 141 | else: 142 | if response.status_code == 401: 143 | # First check if max tries limit exceeded 144 | if _tries > 10: 145 | raise AllegroError("Could not refresh token! Please check credentials or connection!") 146 | 147 | # Refresh access token... 148 | _logger.info(u'Response 401: Refreshing token....') 149 | self.auth_handler.refresh_access_token() 150 | self.auth = self.auth_handler.apply_auth() 151 | 152 | # On refresh callback 153 | if self._on_token_refresh_hook: 154 | self._on_token_refresh_hook(self.auth_handler.access_token, 155 | self.auth_handler.refresh_token) 156 | 157 | # ...and resend last request with new auth 158 | kwargs['auth'] = self.auth 159 | return self._make_request(tries=_tries, **kwargs) 160 | 161 | if response.status_code >= 400: 162 | if response.status_code == 404: 163 | raise AllegroError("404 Not Found") 164 | else: 165 | raise AllegroError(response.json()) 166 | 167 | return response 168 | 169 | def _post(self, url, json=None, headers=None, data=None, files=None): 170 | """ 171 | Handle authenticated POST requests 172 | 173 | :param url: The url for the endpoint 174 | :type url: :py:class:`str` 175 | :param json: The request body to be converted to json 176 | :type json: :py:data:`none` or :py:class:`dict` 177 | :param data: The request body data 178 | :type data: :py:data:`none` or :py:class:`dict` 179 | :param headers: Update headers with provided in this parameter (for this request only) 180 | :type headers: :py:data:`none` or :py:class:`dict` 181 | :return: The JSON response from API or error or None (if 204) 182 | :rtype: :py:class:`dict` or :py:data:`none` 183 | """ 184 | url = requests.compat.urljoin(self.base_url, url) 185 | 186 | # Update headers if necessary 187 | if isinstance(headers, dict): 188 | _headers = self.request_headers.copy() 189 | _headers.update(headers) 190 | else: 191 | _headers = self.request_headers 192 | 193 | r = self._make_request(**dict( 194 | method='POST', 195 | url=url, 196 | json=json, 197 | data=data, 198 | auth=self.auth, 199 | timeout=self.timeout, 200 | hooks=self.request_hooks, 201 | headers=_headers, 202 | files=files 203 | )) 204 | 205 | if r.status_code == 204: 206 | return None 207 | 208 | return r.json() 209 | 210 | def _get(self, url, params=None, headers=None): 211 | """ 212 | Handle authenticated GET requests 213 | 214 | :param url: The url for the endpoint 215 | :type url: :py:class:`str` 216 | :param params: The query string parameters 217 | :type params: :py:class:`dict` 218 | :param headers: Update headers with provided in this parameter (for this request only) 219 | :type headers: :py:data:`none` or :py:class:`dict` 220 | :return: The JSON response from API or error or None (if 204) 221 | :rtype: :py:class:`dict` or :py:data:`none` 222 | """ 223 | url = requests.compat.urljoin(self.base_url, url) 224 | # if len(queryparams): 225 | # url += '?' + requests.compat.urlencode(queryparams) 226 | 227 | # Update headers if necessary 228 | if isinstance(headers, dict): 229 | _headers = self.request_headers.copy() 230 | _headers.update(headers) 231 | else: 232 | _headers = self.request_headers 233 | 234 | r = self._make_request(**dict( 235 | method='GET', 236 | url=url, 237 | params=params, 238 | auth=self.auth, 239 | timeout=self.timeout, 240 | hooks=self.request_hooks, 241 | headers=_headers 242 | )) 243 | 244 | if r.status_code == 204: 245 | return None 246 | 247 | return r.json() 248 | 249 | def _delete(self, url, headers=None): 250 | """ 251 | Handle authenticated DELETE requests 252 | 253 | :param url: The url for the endpoint 254 | :type url: :py:class:`str` 255 | :param headers: Update headers with provided in this parameter (for this request only) 256 | :type headers: :py:data:`none` or :py:class:`dict` 257 | :return: The JSON response from API or error or None (if 204) 258 | :rtype: :py:class:`dict` or :py:data:`none` 259 | """ 260 | url = requests.compat.urljoin(self.base_url, url) 261 | 262 | # Update headers if necessary 263 | if isinstance(headers, dict): 264 | _headers = self.request_headers.copy() 265 | _headers.update(headers) 266 | else: 267 | _headers = self.request_headers 268 | 269 | r = self._make_request(**dict( 270 | method='DELETE', 271 | url=url, 272 | auth=self.auth, 273 | timeout=self.timeout, 274 | hooks=self.request_hooks, 275 | headers=_headers 276 | )) 277 | 278 | if r.status_code == 204: 279 | return None 280 | 281 | return r.json() 282 | 283 | def _patch(self, url, json=None, headers=None, data=None): 284 | """ 285 | Handle authenticated PATCH requests 286 | 287 | :param url: The url for the endpoint 288 | :type url: :py:class:`str` 289 | :param json: The request body to be converted to json 290 | :type json: :py:data:`none` or :py:class:`dict` 291 | :param data: The request body data 292 | :type data: :py:data:`none` or :py:class:`dict` 293 | :param headers: Update headers with provided in this parameter (for this request only) 294 | :type headers: :py:data:`none` or :py:class:`dict` 295 | :return: The JSON response from API or error or None (if 204) 296 | :rtype: :py:class:`dict` or :py:data:`none` 297 | """ 298 | url = requests.compat.urljoin(self.base_url, url) 299 | 300 | # Update headers if necessary 301 | if isinstance(headers, dict): 302 | _headers = self.request_headers.copy() 303 | _headers.update(headers) 304 | else: 305 | _headers = self.request_headers 306 | 307 | r = self._make_request(**dict( 308 | method='PATCH', 309 | url=url, 310 | json=json, 311 | data=data, 312 | auth=self.auth, 313 | timeout=self.timeout, 314 | hooks=self.request_hooks, 315 | headers=_headers 316 | )) 317 | 318 | if r.status_code == 204: 319 | return None 320 | 321 | return r.json() 322 | 323 | def _put(self, url, json=None, headers=None, data=None): 324 | """ 325 | Handle authenticated PUT requests 326 | 327 | :param url: The url for the endpoint 328 | :type url: :py:class:`str` 329 | :param json: The request body to be converted to json 330 | :type json: :py:data:`none` or :py:class:`dict` 331 | :param data: The request body data 332 | :type data: :py:data:`none` or :py:class:`dict` 333 | :param headers: Update headers with provided in this parameter (for this request only) 334 | :type headers: :py:data:`none` or :py:class:`dict` 335 | :return: The JSON response from API or error or None (if 204) 336 | :rtype: :py:class:`dict` or :py:data:`none` 337 | """ 338 | url = requests.compat.urljoin(self.base_url, url) 339 | 340 | # Update headers if necessary 341 | if isinstance(headers, dict): 342 | _headers = self.request_headers.copy() 343 | _headers.update(headers) 344 | else: 345 | _headers = self.request_headers 346 | 347 | r = self._make_request(**dict( 348 | method='PUT', 349 | url=url, 350 | json=json, 351 | data=data, 352 | auth=self.auth, 353 | timeout=self.timeout, 354 | hooks=self.request_hooks, 355 | headers=_headers 356 | )) 357 | 358 | if r.status_code == 204: 359 | return None 360 | 361 | return r.json() 362 | --------------------------------------------------------------------------------