├── .gitignore ├── LICENSE.txt ├── README.md ├── example.py ├── requirements.txt ├── setup.cfg ├── setup.py └── shopify ├── __init__.py ├── base.py ├── fulfillments ├── __init__.py ├── api_wrapper.py └── objects │ ├── __init__.py │ └── fulfillment.py ├── orders ├── __init__.py ├── api_wrapper.py ├── objects │ ├── __init__.py │ ├── address.py │ ├── client_detail.py │ ├── customer.py │ ├── discount_code.py │ ├── line_item.py │ ├── note_attribute.py │ ├── order.py │ ├── payment_detail.py │ ├── refund.py │ ├── shipping_line.py │ ├── tax_line.py │ └── transaction.py └── tests │ ├── __init__.py │ ├── address.json │ ├── client_detail.json │ ├── customer.json │ ├── line_item.json │ ├── location.json │ ├── order.json │ ├── payment_detail.json │ ├── response.json │ ├── shipping_line.json │ ├── tax_line.json │ ├── test_address.py │ ├── test_all.py │ ├── test_clientDetail.py │ ├── test_customer.py │ ├── test_lineItem.py │ ├── test_order.py │ ├── test_paymentDetail.py │ ├── test_shippingLine.py │ └── test_taxLine.py └── products ├── __init__.py ├── api_wrapper.py ├── objects ├── __init__.py ├── image.py ├── option.py ├── product.py └── variant.py └── tests ├── __init__.py ├── image.json ├── option.json ├── product.json ├── response.json ├── test_all.py ├── test_image.py ├── test_option.py ├── test_product.py ├── test_variant.py └── variant.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask instance folder 59 | instance/ 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | ### JetBrains template 92 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 93 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 94 | 95 | # User-specific stuff: 96 | .idea/workspace.xml 97 | .idea/tasks.xml 98 | .idea/dictionaries 99 | .idea/vcs.xml 100 | .idea/jsLibraryMappings.xml 101 | 102 | # Sensitive or high-churn files: 103 | .idea/dataSources.ids 104 | .idea/dataSources.xml 105 | .idea/dataSources.local.xml 106 | .idea/sqlDataSources.xml 107 | .idea/dynamic.xml 108 | .idea/uiDesigner.xml 109 | 110 | # Gradle: 111 | .idea/gradle.xml 112 | .idea/libraries 113 | 114 | # Mongo Explorer plugin: 115 | .idea/mongoSettings.xml 116 | 117 | ## File-based project format: 118 | *.iws 119 | 120 | ## Plugin-specific files: 121 | 122 | # IntelliJ 123 | /out/ 124 | 125 | # mpeltonen/sbt-idea plugin 126 | .idea_modules/ 127 | 128 | # JIRA plugin 129 | atlassian-ide-plugin.xml 130 | 131 | # Crashlytics plugin (for Android Studio and IntelliJ) 132 | com_crashlytics_export_strings.xml 133 | crashlytics.properties 134 | crashlytics-build.properties 135 | fabric.properties 136 | 137 | .idea 138 | 139 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Contains wrappers and parsers for Shopify's Products API. 4 | 5 | # Usage 6 | 7 | ```python 8 | from shopify.products import ProductsApiWrapper, Product, Image 9 | 10 | api_key = '' 11 | password = '' 12 | store_name = '' 13 | 14 | paw = ProductsApiWrapper(api_key, password, store_name) 15 | for product in paw.list(): 16 | print product.title 17 | 18 | product = Product(title='My test product') 19 | image = Image() 20 | image.src = 'https://www.imageurl.com/myimage.jpg' 21 | product.image = image 22 | product.add_tags('testing', 'product') 23 | 24 | product = paw.create(product) 25 | 26 | product.title = 'My New Title For My Test Product' 27 | 28 | paw.update(product) 29 | ``` 30 | 31 | # Installation 32 | 33 | `pip install git+https://github.com/ziplokk1/python-shopify-api.git` -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """ 3 | This example shows how to create new products via API 4 | reading from a list of dictionaries containing products and 5 | variants. 6 | __author__ = Bruno Rocha - brunorocha.org 7 | """ 8 | 9 | # REQUIREMENTS 10 | # pip install slugify 11 | # pip install python-shopify 12 | 13 | import os 14 | from slugify import slugify 15 | from shopify.products import ( 16 | ProductsApiWrapper, Product, Image, Variant, Option 17 | ) 18 | 19 | 20 | # FILL THE DATA below 21 | api_key = '' 22 | password = '' 23 | store_name = '' 24 | 25 | paw = ProductsApiWrapper(api_key, password, store_name) 26 | 27 | # Get a list of existing products, limited to 250 :( 28 | existing = [item.title for item in paw.list(limit=250)] 29 | 30 | 31 | def create_product(items): 32 | """Items is a list of dictionaries representing each product variant 33 | of the same product with the same ID and other data 34 | keys: ['description', 'price', 'name', 'link', 'size', 'stock'] 35 | items = [ 36 | # first variant holds full data and is default 37 | {'name': 'Awesome t-shirt', 38 | 'code': '123456', 39 | 'description': '', 40 | 'size': 'P', 41 | 'price': '22.5', 42 | 'stock': 2}, 43 | # Other variants 44 | {'size': 'M', 45 | 'price': '25.5', 46 | 'stock': 2}, 47 | {'size': 'G', 48 | 'price': '29.5', 49 | 'stock': 0}, 50 | ] 51 | """ 52 | 53 | # The first item should be the complete item holding all the fields 54 | # other items can have only the variants 55 | data = items[0] 56 | 57 | # Use data['name'].decode('utf-8') if you are reading csv files 58 | name = data['name'] 59 | 60 | 61 | if name in existing or paw.list(title=name): 62 | # skip existing 63 | print "Already registered, skipping..." 64 | return 65 | 66 | product = Product( 67 | title=data['name'], 68 | body_html=data['description'], 69 | ) 70 | 71 | # There should be a 123456.jpg file in the same folder 72 | # alternatively you can use a URL provided in data 73 | image_filename = "{0}.jpg".format(data['code']) 74 | if os.path.exists(image_filename): 75 | image = Image() 76 | image.attach(image_filename) 77 | product.add_image(image) 78 | elif data.get('image_url'): 79 | product.add_image(Image(src=data['image_url'])) 80 | 81 | # using the first word in title as tag 82 | # Product "T-shirt Blue 09" got tag "t-shirt" 83 | tag = data['name'].split()[0] 84 | tag = u''.join(i for i in tag if not i.isdigit()) 85 | 86 | product.add_tags(tag.strip().lower()) 87 | 88 | # You can add only 3 options 89 | # at positions 1, 2 and 3 90 | # you should add options before adding its variants 91 | product.add_option( 92 | Option( 93 | name="Size", 94 | position=1, 95 | ) 96 | ) 97 | 98 | for item in items: 99 | product.add_variant( 100 | Variant( 101 | option1=item['size'], 102 | # option2=data['size'], 103 | # option3=data['size'], 104 | title="Size", 105 | price=item['price'], 106 | # SKU should be different for each variant 107 | sku=data["code"] + slugify(item['size']), 108 | position=1, 109 | inventory_policy="continue", 110 | fulfillment_service="manual", 111 | inventory_management="shopify", 112 | inventory_quantity=int(item['stock']), 113 | taxable=False, 114 | weight=300, 115 | weight_unit="g", # g, kg 116 | requires_shipping=True 117 | ) 118 | ) 119 | 120 | try: 121 | product = paw.create(product) 122 | except Exception as e: 123 | # do a proper logging here please!!! 124 | print e 125 | print product 126 | print items 127 | 128 | return product -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil==2.5.3 2 | requests==2.10.0 3 | six==1.10.0 4 | wheel==0.24.0 5 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | version = '0.1.3' 4 | 5 | REQUIREMENTS = [ 6 | 'python-dateutil', 7 | 'requests' 8 | ] 9 | 10 | setup( 11 | name='python-shopify', 12 | version=version, 13 | packages=find_packages(), 14 | url='https://github.com/ziplokk1/python-shopify-api', 15 | license='LICENSE.txt', 16 | author='Mark Sanders', 17 | author_email='sdscdeveloper@gmail.com', 18 | install_requires=REQUIREMENTS, 19 | description='Wrapper and parser modules for Shopify\'s API.', 20 | include_package_data=True 21 | ) 22 | -------------------------------------------------------------------------------- /shopify/__init__.py: -------------------------------------------------------------------------------- 1 | from .orders import OrdersApiWrapper 2 | from .products import ProductsApiWrapper 3 | -------------------------------------------------------------------------------- /shopify/base.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | import urlparse 4 | 5 | import requests 6 | from dateutil import parser as date_parser 7 | 8 | 9 | def datetime_to_string(dt): 10 | """ 11 | Convert a datetime object to the preferred format for the shopify api. (2016-01-01T11:00:00-5:00) 12 | 13 | :param dt: Datetime object to convert to timestamp. 14 | :return: Timestamp string for the datetime object. 15 | """ 16 | if not dt: 17 | return None 18 | if not isinstance(dt, datetime.datetime): 19 | raise ValueError('Must supply an instance of `datetime`.') 20 | # Calculate the utc offset of the current timezone 21 | # 1 is added to the total seconds to account for the time which it takes the operation to calculate 22 | # utcnow and local now. 23 | offset = int(divmod((datetime.datetime.utcnow() - datetime.datetime.now()).total_seconds() + 1, 60)[0] / 60) 24 | offset_str = '-%d:00' % offset 25 | dt_str = dt.strftime('%Y-%m-%dT%H:%M:%S') 26 | return dt_str + offset_str 27 | 28 | 29 | def string_to_datetime(dt_str): 30 | """ 31 | Convert a formatted timestamp to a datetime object. 32 | 33 | :param dt_str: Timestamp string (2016-01-01T11:00:00-5:00) 34 | :return: datetime object from timestamp. 35 | """ 36 | if dt_str: 37 | return date_parser.parse(dt_str) 38 | return None 39 | 40 | 41 | def string_to_float(float_str): 42 | """ 43 | Convert a float in string format to a float. 44 | 45 | :param float_str: Float in string format ("25.01") 46 | :return: float from string 47 | """ 48 | if float_str: 49 | return float(float_str) 50 | return None 51 | 52 | 53 | class BaseParser(object): 54 | 55 | def __init__(self, d=None, **kwargs): 56 | self._dict = d or kwargs or {} # Prevent exceptions when getting data from d when d is None 57 | 58 | @property 59 | def id(self): 60 | return self._dict.get('id') 61 | 62 | @property 63 | def __dict__(self): 64 | return self._dict 65 | 66 | def __str__(self): 67 | return str(self._dict) 68 | 69 | def __nonzero__(self): 70 | return bool(self._dict) 71 | 72 | 73 | class ShopifyApiError(Exception): 74 | """ 75 | Used to check if a response is an error and parse out the error. 76 | """ 77 | 78 | def __init__(self, response): 79 | self.response = response 80 | content = self.response.content 81 | if content: 82 | self.content = json.loads(self.response.content) 83 | else: 84 | self.content = {} 85 | Exception.__init__(self, self.message) 86 | 87 | def has_error(self): 88 | return bool(self.content.get('errors')) 89 | 90 | @property 91 | def message(self): 92 | errors = self.content.get('errors', {}) 93 | msg = '' 94 | if isinstance(errors, dict): 95 | for category, message in errors.items(): 96 | # Error Category, Message 97 | msg += '({} - {})'.format(category, message) 98 | else: 99 | msg += errors 100 | return msg 101 | 102 | 103 | class ShopifyApiWrapper(object): 104 | 105 | def __init__(self, api_key, password, store_name): 106 | self.api_key = api_key 107 | self.password = password 108 | self.store_name = store_name 109 | 110 | @staticmethod 111 | def remove_empty(d): 112 | """ 113 | Return a new dictionary object from the old one without any items with falsey values. 114 | :param d: 115 | :return: 116 | """ 117 | return {k: v for k, v in d.items() if v} 118 | 119 | def url_host(self): 120 | """ 121 | Format the URL host with the api_key, password, and store name for access to shopify's api. 122 | :return: Formatted URL (https://api_key:password@store_name.myshopify.com) 123 | """ 124 | return 'https://{}:{}@{}.myshopify.com'.format(self.api_key, self.password, self.store_name) 125 | -------------------------------------------------------------------------------- /shopify/fulfillments/__init__.py: -------------------------------------------------------------------------------- 1 | from .objects import * 2 | from api_wrapper import FulfillmentsApiWrapper 3 | -------------------------------------------------------------------------------- /shopify/fulfillments/api_wrapper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | from ..base import ShopifyApiWrapper, datetime_to_string, ShopifyApiError 5 | 6 | 7 | class FulfillmentsApiWrapper(ShopifyApiWrapper): 8 | 9 | max_results_limit = 250 10 | 11 | list_endpoint = '/admin/orders/{}/fulfillments.json' 12 | 13 | def list(self, id_, limit=50, page=1, since_id=None, created_at_min=None, created_at_max=None, updated_at_min=None, updated_at_max=None, fields=()): 14 | """ 15 | Return a list of fulfillments. 16 | 17 | :param id_: The ID element's value from an order. 18 | :param limit: Amount of results. (default 50). 19 | :param page: Page to show. (default 1). 20 | :param since_id: Restrict results to after the specified ID. 21 | :param created_at_min: Show fulfillments created after date. 22 | :param created_at_max: Show fulfillments created before date. 23 | :param updated_at_min: Show fulfillments last updated after date. 24 | :param updated_at_max: Show fulfillments last updated before date. 25 | :param fields: List of fields to include in the response. 26 | :return: 27 | """ 28 | if limit > self.max_results_limit: 29 | raise ValueError('`limit` cannot exceed {}'.format(self.max_results_limit)) 30 | params = dict( 31 | limit=limit, 32 | page=page, 33 | since_id=since_id, 34 | created_at_min=datetime_to_string(created_at_min), 35 | created_at_max=datetime_to_string(created_at_max), 36 | updated_at_min=datetime_to_string(updated_at_min), 37 | updated_at_max=datetime_to_string(updated_at_max), 38 | fields=','.join(fields) 39 | ) 40 | params = self.remove_empty(params) 41 | 42 | endpoint = self.list_endpoint.format(id_) 43 | url = self.url_host() + endpoint 44 | response = requests.get(url, params=params) 45 | 46 | err = ShopifyApiError(response) 47 | if err.has_error(): 48 | raise err 49 | 50 | with open('fulfillments-list.json', 'wb') as f: 51 | f.write(response.content) 52 | 53 | response.raise_for_status() 54 | 55 | data = json.loads(response.content) 56 | 57 | return data 58 | -------------------------------------------------------------------------------- /shopify/fulfillments/objects/__init__.py: -------------------------------------------------------------------------------- 1 | from .fulfillment import Fulfillment 2 | -------------------------------------------------------------------------------- /shopify/fulfillments/objects/fulfillment.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser, string_to_datetime 2 | from ...orders.objects.line_item import LineItem 3 | 4 | 5 | class Receipt(BaseParser): 6 | """ 7 | Text field that provides information about the receipt. 8 | """ 9 | 10 | @property 11 | def testcase(self): 12 | """ 13 | States whether or not the fulfillment was a testcase. 14 | 15 | Valid values are True or False. 16 | :return: 17 | """ 18 | return self._dict.get('testcase') 19 | 20 | @property 21 | def authorization(self): 22 | """ 23 | The authorization code. 24 | :return: 25 | """ 26 | return self._dict.get('authorization') 27 | 28 | 29 | class Fulfillment(BaseParser): 30 | 31 | @property 32 | def order_id(self): 33 | """ 34 | The unique numeric identifier for the order. 35 | :return: 36 | """ 37 | return self._dict.get('order_id') 38 | 39 | @property 40 | def status(self): 41 | """ 42 | The status of the fulfillment. 43 | 44 | Valid values are: 45 | - pending (The fulfillment is pending). 46 | - open (The fulfillment has been acknowledged by the service and is in processing). 47 | - success (The fulfillment was successful). 48 | - cancelled (The fulfillment was cancelled). 49 | - error (There was an error with the fulfillment request). 50 | - failure (The fulfillment request failed). 51 | :return: 52 | """ 53 | return self._dict.get('status') 54 | 55 | @property 56 | def created_at(self): 57 | """ 58 | The date and time when the fulfillment was created. 59 | :return: 60 | """ 61 | return string_to_datetime(self._dict.get('created_at')) 62 | 63 | @property 64 | def service(self): 65 | # ToDo: Find documentation 66 | return self._dict.get('service') 67 | 68 | @property 69 | def updated_at(self): 70 | """ 71 | The date and time when the fulfillment was last modified. 72 | :return: 73 | """ 74 | return string_to_datetime(self._dict.get('updated_at')) 75 | 76 | @property 77 | def tracking_company(self): 78 | """ 79 | The name of the shipping company. 80 | :return: 81 | """ 82 | return self._dict.get('tracking_company') 83 | 84 | @property 85 | def shipment_status(self): 86 | # ToDo: Find documentation 87 | return self._dict.get('shipment_status') 88 | 89 | @property 90 | def tracking_number(self): 91 | # ToDo: Find documentation 92 | return self._dict.get('tracking_number') 93 | 94 | @property 95 | def tracking_numbers(self): 96 | """ 97 | A list of shipping numbers, provided by the shipping company. 98 | :return: 99 | """ 100 | return self._dict.get('tracking_numbers', []) 101 | 102 | @property 103 | def tracking_url(self): 104 | # ToDo: Find documentation 105 | return self._dict.get('tracking_url') 106 | 107 | @property 108 | def tracking_urls(self): 109 | """ 110 | The URLs to track the fulfillment. 111 | :return: 112 | """ 113 | return self._dict.get('tracking_urls', []) 114 | 115 | @property 116 | def receipt(self): 117 | """ 118 | Text field that provides information about the receipt. 119 | :return: 120 | """ 121 | return Receipt(self._dict.get('receipt')) 122 | 123 | @property 124 | def line_items(self): 125 | """ 126 | A historical record of each item in the fulfillment. 127 | :return: 128 | """ 129 | return [LineItem(x) for x in self._dict.get('line_items', [])] 130 | -------------------------------------------------------------------------------- /shopify/orders/__init__.py: -------------------------------------------------------------------------------- 1 | from .objects import * 2 | from .api_wrapper import OrdersApiWrapper 3 | -------------------------------------------------------------------------------- /shopify/orders/api_wrapper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | from ..base import ShopifyApiWrapper, datetime_to_string, ShopifyApiError 5 | from .objects import Order 6 | 7 | 8 | class OrdersApiWrapper(ShopifyApiWrapper): 9 | 10 | max_results_limit = 250 11 | 12 | default_endpoint = '/admin/orders.json' 13 | operational_endpoint = '/admin/orders/{}.json' 14 | close_endpoint = '/admin/orders/{}/close.json' 15 | open_endpoint = '/admin/orders/{}/open.json' 16 | cancel_endpoint = '/admin/orders/{}/cancel.json' 17 | 18 | valid_status_values = [ 19 | 'open', 20 | 'closed', 21 | 'cancelled', 22 | 'any' 23 | ] 24 | 25 | valid_financial_status_values = [ 26 | 'authorized', 27 | 'pending', 28 | 'paid', 29 | 'partially_paid', 30 | 'refunded', 31 | 'voided', 32 | 'partially_refunded', 33 | 'any', 34 | 'unpaid' 35 | ] 36 | 37 | valid_fulfillment_status_values = [ 38 | 'shipped', 39 | 'partial', 40 | 'unshipped', 41 | 'any' 42 | ] 43 | 44 | def list(self, ids=(), limit=50, page=1, since_id=None, created_at_min=None, created_at_max=None, 45 | updated_at_min=None, updated_at_max=None, processed_at_min=None, processed_at_max=None, status='open', 46 | financial_status='any', fulfillment_status='any', fields=()): 47 | if limit > self.max_results_limit: 48 | raise ValueError('`limit` cannot exceed {}'.format(self.max_results_limit)) 49 | if status not in self.valid_status_values: 50 | raise ValueError('`status` must be one of {}'.format(self.valid_status_values)) 51 | if financial_status not in self.valid_financial_status_values: 52 | raise ValueError('`financial_status` must be one of {}'.format(self.valid_financial_status_values)) 53 | if fulfillment_status not in self.valid_fulfillment_status_values: 54 | raise ValueError('`fulfillment_status` must be one of {}'.format(self.valid_fulfillment_status_values)) 55 | params = dict( 56 | ids=','.join(ids), 57 | limit=limit, 58 | page=page, 59 | since_id=since_id, 60 | created_at_min=datetime_to_string(created_at_min), 61 | created_at_max=datetime_to_string(created_at_max), 62 | updated_at_min=datetime_to_string(updated_at_min), 63 | updated_at_max=datetime_to_string(updated_at_max), 64 | processed_at_min=datetime_to_string(processed_at_min), 65 | processed_at_max=datetime_to_string(processed_at_max), 66 | status=status, 67 | financial_status=financial_status, 68 | fulfillment_status=fulfillment_status, 69 | fields=','.join(fields) 70 | ) 71 | params = self.remove_empty(params) 72 | url = self.url_host() + self.default_endpoint 73 | response = requests.get(url, params=params) 74 | 75 | with open('orders-list.json', 'wb') as f: 76 | f.write(response.content) 77 | 78 | # Check if the response was an error and raise if necessary. 79 | err = ShopifyApiError(response) 80 | if err.has_error(): 81 | raise err 82 | 83 | data = json.loads(response.content) 84 | return [Order(x) for x in data.get('orders', [])] 85 | 86 | def _close(self, order_id): 87 | endpoint = self.close_endpoint.format(order_id) 88 | url = self.url_host() + endpoint 89 | 90 | r = requests.post(url, data='{}', headers={"Content-Type": 'application/json'}) 91 | 92 | with open('order-close.json', 'wb') as f: 93 | f.write(r.content) 94 | 95 | err = ShopifyApiError(r) 96 | if err.has_error(): 97 | raise err 98 | 99 | return json.loads(r.content) 100 | 101 | def close(self, order): 102 | if not order.id: 103 | raise ValueError('`order` object must have id.') 104 | return Order(self._close(order.id).get('order')) 105 | 106 | def close_by_id(self, id_): 107 | return Order(self._close(id_).get('order')) 108 | -------------------------------------------------------------------------------- /shopify/orders/objects/__init__.py: -------------------------------------------------------------------------------- 1 | from .order import Order 2 | from .address import Address 3 | from .client_detail import ClientDetail 4 | from .customer import Customer 5 | from .line_item import LineItem 6 | from .payment_detail import PaymentDetail 7 | from .shipping_line import ShippingLine 8 | from .tax_line import TaxLine 9 | -------------------------------------------------------------------------------- /shopify/orders/objects/address.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser, string_to_float 2 | 3 | 4 | class Location(BaseParser): 5 | """ 6 | Parser for basic location information such as city, state, and zip. 7 | """ 8 | 9 | @property 10 | def country_code(self): 11 | """ 12 | The two-letter code (ISO 3166-1 alpha-2 two-letter country code) for the country of the address. 13 | :return: 14 | """ 15 | return self._dict.get('country_code') 16 | 17 | @property 18 | def province_code(self): 19 | """ 20 | The two-letter abbreviation of the state or province of the address. 21 | :return: 22 | """ 23 | return self._dict.get('province_code') 24 | 25 | @property 26 | def name(self): 27 | """ 28 | The full name of the person associated with the address. 29 | :return: 30 | """ 31 | return self._dict.get('name') 32 | 33 | @property 34 | def address_1(self): 35 | """ 36 | Street address. 37 | :return: 38 | """ 39 | return self._dict.get('address1') 40 | 41 | @property 42 | def address_2(self): 43 | """ 44 | Optional field for the street address. 45 | :return: 46 | """ 47 | return self._dict.get('address2') 48 | 49 | @property 50 | def city(self): 51 | """ 52 | The city of the address. 53 | :return: 54 | """ 55 | return self._dict.get('city') 56 | 57 | @property 58 | def zip(self): 59 | """ 60 | The zip/postal-code of the address. 61 | :return: 62 | """ 63 | return self._dict.get('zip') 64 | 65 | 66 | class Address(Location): 67 | """ 68 | Parser which contains more descriptive information about a location/address such as name and phone number. 69 | """ 70 | 71 | # Address has no id field 72 | 73 | @property 74 | def first_name(self): 75 | """ 76 | The first name of the person associated with the address. 77 | :return: 78 | """ 79 | return self._dict.get('first_name') 80 | 81 | @property 82 | def phone(self): 83 | """ 84 | The phone number of the address. 85 | :return: 86 | """ 87 | return self._dict.get('phone') 88 | 89 | @property 90 | def province(self): 91 | """ 92 | The name of the state or province of the address. 93 | :return: 94 | """ 95 | return self._dict.get('province') 96 | 97 | @property 98 | def country(self): 99 | """ 100 | The name of the country of the address. 101 | :return: 102 | """ 103 | return self._dict.get('country') 104 | 105 | @property 106 | def last_name(self): 107 | """ 108 | The last name of the person associated with the address. 109 | :return: 110 | """ 111 | return self._dict.get('last_name') 112 | 113 | @property 114 | def company(self): 115 | """ 116 | The company of the person associated with the address. 117 | :return: 118 | """ 119 | return self._dict.get('company') 120 | 121 | @property 122 | def longitude(self): 123 | """ 124 | The longitude of the address. 125 | :return: 126 | """ 127 | return self._dict.get('longitude') 128 | 129 | @property 130 | def latitude(self): 131 | """ 132 | The latitude of the address. 133 | :return: 134 | """ 135 | return self._dict.get('latitude') 136 | 137 | @property 138 | def default(self): 139 | """ 140 | Boolean value whether or not the address is to be used as the default address. 141 | :return: 142 | """ 143 | return self._dict.get('default') 144 | -------------------------------------------------------------------------------- /shopify/orders/objects/client_detail.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser 2 | 3 | 4 | class ClientDetail(BaseParser): 5 | """ 6 | An object containing information about the client machine such as the browser IP and user agent. 7 | """ 8 | 9 | @property 10 | def browser_ip(self): 11 | """ 12 | The browser IP address. 13 | :return: 14 | """ 15 | return self._dict.get('browser_ip') 16 | 17 | @property 18 | def accept_language(self): 19 | """ 20 | Accept-Language header value. 21 | :return: 22 | """ 23 | return self._dict.get('accept_language') 24 | 25 | @property 26 | def user_agent(self): 27 | """ 28 | User agent string from the User-Agent header. 29 | :return: 30 | """ 31 | return self._dict.get('user_agent') 32 | 33 | @property 34 | def session_hash(self): 35 | """ 36 | A hash of the session. 37 | :return: 38 | """ 39 | return self._dict.get('session_hash') 40 | 41 | @property 42 | def browser_width(self): 43 | """ 44 | The browser screen width in pixels, if available. 45 | :return: 46 | """ 47 | return self._dict.get('browser_width') 48 | 49 | @property 50 | def browser_height(self): 51 | """ 52 | The browser screen height in pixels, if available. 53 | :return: 54 | """ 55 | return self._dict.get('browser_height') 56 | -------------------------------------------------------------------------------- /shopify/orders/objects/customer.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser, string_to_datetime, string_to_float 2 | from .address import Address 3 | 4 | 5 | class Customer(BaseParser): 6 | """ 7 | Parser which contains descriptive information about the customer such as name and email. 8 | """ 9 | 10 | @property 11 | def email(self): 12 | """ 13 | The customer's email address. 14 | :return: 15 | """ 16 | return self._dict.get('email') 17 | 18 | @property 19 | def accepts_marketing(self): 20 | """ 21 | Indicates whether or not the customer would like to receive email updates from the shop. 22 | 23 | Valid values are True and False. 24 | :return: 25 | """ 26 | return self._dict.get('accepts_marketing') 27 | 28 | @property 29 | def created_at(self): 30 | """ 31 | The date and time when the customer record was created. 32 | :return: 33 | """ 34 | return string_to_datetime(self._dict.get('created_at')) 35 | 36 | @property 37 | def updated_at(self): 38 | """ 39 | The date and time when the customer record was last updated. 40 | :return: 41 | """ 42 | return string_to_datetime(self._dict.get('updated_at')) 43 | 44 | @property 45 | def first_name(self): 46 | """ 47 | The customer's first name. 48 | :return: 49 | """ 50 | return self._dict.get('first_name') 51 | 52 | @property 53 | def last_name(self): 54 | """ 55 | The customer's last name. 56 | :return: 57 | """ 58 | return self._dict.get('last_name') 59 | 60 | @property 61 | def orders_count(self): 62 | """ 63 | The number of orders placed by this customer to a shop. 64 | 65 | Pulls the current information. 66 | :return: 67 | """ 68 | return self._dict.get('orders_count') 69 | 70 | @property 71 | def state(self): 72 | """ 73 | No documentation available. 74 | 75 | :return: 76 | """ 77 | # ToDo: find documentation 78 | return self._dict.get('state') 79 | 80 | @property 81 | def total_spent(self): 82 | """ 83 | No documentation available. 84 | :return: 85 | """ 86 | # ToDo: find documentation 87 | return string_to_float(self._dict.get('total_spent')) 88 | 89 | @property 90 | def last_order_id(self): 91 | """ 92 | No documentation available. 93 | :return: 94 | """ 95 | # ToDo: find documentation 96 | return self._dict.get('last_order_id') 97 | 98 | @property 99 | def note(self): 100 | """ 101 | Extra information about the customer. 102 | :return: 103 | """ 104 | return self._dict.get('note') 105 | 106 | @property 107 | def verified_email(self): 108 | """ 109 | No documentation available. 110 | :return: 111 | """ 112 | # ToDo: find documentation 113 | return self._dict.get('verified_email') 114 | 115 | @property 116 | def multipass_identifier(self): 117 | """ 118 | No documentation available. 119 | :return: 120 | """ 121 | # ToDo: find documentation 122 | return self._dict.get('multipass_identifier') 123 | 124 | @property 125 | def tax_exempt(self): 126 | """ 127 | No documentation available. 128 | :return: 129 | """ 130 | # ToDo: find documentation 131 | return self._dict.get('tax_exempt') 132 | 133 | @property 134 | def tags(self): 135 | """ 136 | Tags are additional short descriptors, commonly used for filtering and searching. 137 | :return: 138 | """ 139 | return tuple([x.strip() for x in self._dict.get('tags', '').split(',')]) 140 | 141 | @property 142 | def last_order_name(self): 143 | """ 144 | No documentation available. 145 | :return: 146 | """ 147 | # ToDo: find documentation 148 | return self._dict.get('last_order_name') 149 | 150 | @property 151 | def default_address(self): 152 | """ 153 | No documentation available. 154 | :return: 155 | """ 156 | # ToDo: find documentation 157 | return Address(self._dict.get('default_address')) 158 | -------------------------------------------------------------------------------- /shopify/orders/objects/discount_code.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser, string_to_float 2 | 3 | 4 | class DiscountCode(BaseParser): 5 | """ 6 | Parser for any applicable discounts to an order. 7 | """ 8 | 9 | @property 10 | def code(self): 11 | """ 12 | The discount code. 13 | :return: 14 | """ 15 | return self._dict.get('code') 16 | 17 | @property 18 | def amount(self): 19 | """ 20 | The amount of the discount. 21 | :return: 22 | """ 23 | return string_to_float(self._dict.get('amount')) 24 | 25 | @property 26 | def type(self): 27 | """ 28 | The type of discount. 29 | 30 | Can be one of: 31 | - percentage 32 | - shipping 33 | - fixed_amount (default) 34 | :return: 35 | """ 36 | return self._dict.get('type') 37 | -------------------------------------------------------------------------------- /shopify/orders/objects/line_item.py: -------------------------------------------------------------------------------- 1 | from .address import Location 2 | from .tax_line import TaxLine 3 | from .note_attribute import NoteAttribute 4 | from ...base import BaseParser, string_to_float 5 | 6 | 7 | class LineItem(BaseParser): 8 | """ 9 | Parser for an item in an order. 10 | """ 11 | 12 | @property 13 | def variant_id(self): 14 | """ 15 | The id of the product variant being fulfilled. 16 | :return: 17 | """ 18 | return self._dict.get('variant_id') 19 | 20 | @property 21 | def title(self): 22 | """ 23 | The title of the product. 24 | :return: 25 | """ 26 | return self._dict.get('title') 27 | 28 | @property 29 | def quantity(self): 30 | """ 31 | The number of items in the fulfillment. 32 | :return: 33 | """ 34 | return self._dict.get('quantity') 35 | 36 | @property 37 | def price(self): 38 | """ 39 | The price of the item. 40 | :return: 41 | """ 42 | return string_to_float(self._dict.get('price')) 43 | 44 | @property 45 | def grams(self): 46 | """ 47 | The weight of the item in grams. 48 | :return: 49 | """ 50 | return self._dict.get('grams') 51 | 52 | @property 53 | def sku(self): 54 | """ 55 | A unique identifier of the item in the fulfillment. 56 | :return: 57 | """ 58 | return self._dict.get('sku') 59 | 60 | @property 61 | def variant_title(self): 62 | """ 63 | The title of the product variant being fulfilled. 64 | :return: 65 | """ 66 | return self._dict.get('variant_title') 67 | 68 | @property 69 | def vendor(self): 70 | """ 71 | The name of the supplier of the item. 72 | :return: 73 | """ 74 | return self._dict.get('vendor') 75 | 76 | @property 77 | def fulfillment_service(self): 78 | """ 79 | Service provider who is doing the fulfillment. 80 | :return: 81 | """ 82 | return self._dict.get('fulfillment_service') 83 | 84 | @property 85 | def product_id(self): 86 | """ 87 | The unique numeric identifier for the product in the fulfillment. 88 | :return: 89 | """ 90 | return self._dict.get('product_id') 91 | 92 | @property 93 | def requires_shipping(self): 94 | """ 95 | Specifies whether or not a customer needs to provide a shipping address when placing an order for this product variant. 96 | 97 | Valid values are: True or False 98 | :return: 99 | """ 100 | return self._dict.get('requires_shipping') 101 | 102 | @property 103 | def taxable(self): 104 | # ToDo: Find documentation 105 | return self._dict.get('taxable') 106 | 107 | @property 108 | def gift_card(self): 109 | # ToDo: Find documentation 110 | return self._dict.get('gift_card') 111 | 112 | @property 113 | def name(self): 114 | # ToDo: Find documentation 115 | return self._dict.get('name') 116 | 117 | @property 118 | def variant_inventory_management(self): 119 | """ 120 | Returns the name of the inventory management system. 121 | :return: 122 | """ 123 | return self._dict.get('variant_inventory_management') 124 | 125 | @property 126 | def properties(self): 127 | """ 128 | Returns additional properties associated with the line item. 129 | :return: 130 | """ 131 | # NoteAttribute instance is used since it contains the same data as 132 | # the properties element. 133 | return [NoteAttribute(x) for x in self._dict.get('properties', [])] 134 | 135 | @property 136 | def product_exists(self): 137 | """ 138 | States whether or not the product exists. Valid values are True or False. 139 | :return: 140 | """ 141 | return self._dict.get('product_exists') 142 | 143 | @property 144 | def fulfillable_quantity(self): 145 | """ 146 | The amount available to fulfill. 147 | 148 | This is the quantity - max(refunded_quantity, fulfilled_quantity) - pending_fulfilled_quantity - open_fulfilled_quantity. 149 | :return: 150 | """ 151 | return self._dict.get('fulfillable_quantity') 152 | 153 | @property 154 | def total_discount(self): 155 | # ToDo: Find documentation 156 | return string_to_float(self._dict.get('total_discount')) 157 | 158 | @property 159 | def fulfillment_status(self): 160 | """ 161 | Status of an order in terms of the line_items being fulfilled. 162 | 163 | Values: 164 | - fulfilled 165 | - None 166 | - partial 167 | :return: 168 | """ 169 | return self._dict.get('fulfillment_status') 170 | 171 | @property 172 | def tax_lines(self): 173 | # ToDo: Find documentation 174 | return [TaxLine(x) for x in self._dict.get('tax_lines', [])] 175 | 176 | @property 177 | def origin_location(self): 178 | # ToDo: Find documentation 179 | return Location(self._dict.get('origin_location')) 180 | 181 | @property 182 | def destination_location(self): 183 | # ToDo: Find documentation 184 | return Location(self._dict.get('destination_location')) 185 | -------------------------------------------------------------------------------- /shopify/orders/objects/note_attribute.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser 2 | 3 | 4 | class NoteAttribute(BaseParser): 5 | 6 | @property 7 | def name(self): 8 | return self._dict.get('name') 9 | 10 | @property 11 | def value(self): 12 | return self._dict.get('value') 13 | -------------------------------------------------------------------------------- /shopify/orders/objects/order.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser, string_to_datetime, string_to_float 2 | from .address import Address 3 | from .line_item import LineItem 4 | from .tax_line import TaxLine 5 | from .shipping_line import ShippingLine 6 | from .client_detail import ClientDetail 7 | from .payment_detail import PaymentDetail 8 | from .customer import Customer 9 | from .discount_code import DiscountCode 10 | from .note_attribute import NoteAttribute 11 | from ...fulfillments import Fulfillment 12 | from .refund import Refund 13 | 14 | 15 | class Order(BaseParser): 16 | 17 | @property 18 | def email(self): 19 | """ 20 | The customer's email address. 21 | 22 | Is required when a billing address is present. 23 | :return: 24 | """ 25 | return self._dict.get('email') 26 | 27 | @property 28 | def closed_at(self): 29 | """ 30 | The date and time when the order was closed. 31 | 32 | If the order was not closed, this value is None. 33 | :return: 34 | """ 35 | return string_to_datetime(self._dict.get('closed_at')) 36 | 37 | @property 38 | def created_at(self): 39 | """ 40 | By default, this auto-generated property is the date and time when the order was created in Shopify. 41 | 42 | If you are importing orders to the Shopify platform from another system, 43 | the writable processed_at property will override the created_at date. 44 | :return: 45 | """ 46 | return string_to_datetime(self._dict.get('created_at')) 47 | 48 | @property 49 | def updated_at(self): 50 | return string_to_datetime(self._dict.get('updated_at')) 51 | 52 | @property 53 | def number(self): 54 | return self._dict.get('number') 55 | 56 | @property 57 | def note(self): 58 | return self._dict.get('note') 59 | 60 | @property 61 | def token(self): 62 | return self._dict.get('token') 63 | 64 | @property 65 | def gateway(self): 66 | return self._dict.get('gateway') 67 | 68 | @property 69 | def test(self): 70 | return self._dict.get('test') 71 | 72 | @property 73 | def total_price(self): 74 | return string_to_float(self._dict.get('total_price')) 75 | 76 | @property 77 | def subtotal_price(self): 78 | return string_to_float(self._dict.get('subtotal_price')) 79 | 80 | @property 81 | def total_weight(self): 82 | return self._dict.get('total_weight') 83 | 84 | @property 85 | def total_tax(self): 86 | return string_to_float(self._dict.get('total_tax')) 87 | 88 | @property 89 | def taxes_included(self): 90 | return self._dict.get('taxes_included') 91 | 92 | @property 93 | def currency(self): 94 | """ 95 | The three letter code (ISO 4217) for the currency used for the payment. 96 | :return: 97 | """ 98 | return self._dict.get('currency') 99 | 100 | @property 101 | def financial_status(self): 102 | """ 103 | The financial processing status for the order. 104 | 105 | Values can be one of: 106 | - pending 107 | - authorized 108 | - partially_paid 109 | - paid (default) 110 | - partially_refunded 111 | - refunded 112 | - voided 113 | :return: 114 | """ 115 | return self._dict.get('financial_status') 116 | 117 | @property 118 | def confirmed(self): 119 | return self._dict.get('confirmed') 120 | 121 | @property 122 | def total_discounts(self): 123 | return string_to_float(self._dict.get('total_discounts')) 124 | 125 | @property 126 | def total_line_items_price(self): 127 | return string_to_float(self._dict.get('total_line_items_price')) 128 | 129 | @property 130 | def cart_token(self): 131 | """ 132 | Unique identifier for a particular cart that is attached to a particular order. 133 | :return: 134 | """ 135 | return self._dict.get('cart_token') 136 | 137 | @property 138 | def buyer_accepts_marketing(self): 139 | """ 140 | Indicates whether or not the person who placed the order would like to receive email updates from the shop. 141 | 142 | This is set when checking the "I want to receive occasional emails about new products, 143 | promotions and other news" checkbox during checkout. 144 | Valid values are True and False. 145 | :return: 146 | """ 147 | return self._dict.get('buyer_accepts_marketing') 148 | 149 | @property 150 | def name(self): 151 | return self._dict.get('name') 152 | 153 | @property 154 | def referring_site(self): 155 | return self._dict.get('referring_site') 156 | 157 | @property 158 | def landing_site(self): 159 | return self._dict.get('landing_site') 160 | 161 | @property 162 | def cancelled_at(self): 163 | """ 164 | The date and time when the order was cancelled. 165 | 166 | If the order was not cancelled, this value is None. 167 | :return: 168 | """ 169 | return string_to_datetime(self._dict.get('cancelled_at')) 170 | 171 | @property 172 | def cancel_reason(self): 173 | """ 174 | The reason why the order was cancelled. 175 | 176 | If the order was not cancelled, this value is None. 177 | If the order was cancelled, the value will be one of the following: 178 | - customer: The customer changed or cancelled the order. 179 | - fraud: The order was fraudulent. 180 | - inventory: Items in the order were not in inventory. 181 | - other: The order was cancelled for a reason not in the list above. 182 | :return: 183 | """ 184 | return self._dict.get('cancel_reason') 185 | 186 | @property 187 | def total_price_usd(self): 188 | return string_to_float(self._dict.get('total_price_usd')) 189 | 190 | @property 191 | def checkout_token(self): 192 | return self._dict.get('checkout_token') 193 | 194 | @property 195 | def reference(self): 196 | return self._dict.get('reference') 197 | 198 | @property 199 | def user_id(self): 200 | return self._dict.get('user_id') 201 | 202 | @property 203 | def location_id(self): 204 | return self._dict.get('location_id') 205 | 206 | @property 207 | def source_identifier(self): 208 | return self._dict.get('source_identifier') 209 | 210 | @property 211 | def source_url(self): 212 | return self._dict.get('source_url') 213 | 214 | @property 215 | def processed_at(self): 216 | return string_to_datetime(self._dict.get('processed_at')) 217 | 218 | @property 219 | def device_id(self): 220 | return self._dict.get('device_id') 221 | 222 | @property 223 | def browser_ip(self): 224 | """ 225 | The IP address of the browser used by the customer when placing the order. 226 | :return: 227 | """ 228 | return self._dict.get('browser_ip') 229 | 230 | @property 231 | def landing_site_ref(self): 232 | return self._dict.get('landing_site_ref') 233 | 234 | @property 235 | def order_number(self): 236 | return self._dict.get('order_number') 237 | 238 | @property 239 | def discount_codes(self): 240 | """ 241 | Applicable discount codes that can be applied to the order. 242 | 243 | If no codes exist the value will default to an empty list. 244 | :return: 245 | """ 246 | return [DiscountCode(x) for x in self._dict.get('discount_codes', [])] 247 | 248 | @property 249 | def note_attributes(self): 250 | return [NoteAttribute(x) for x in self._dict.get('note_attributes', [])] 251 | 252 | @property 253 | def payment_gateway_names(self): 254 | return self._dict.get('payment_gateway_names', []) 255 | 256 | @property 257 | def processing_method(self): 258 | return self._dict.get('processing_method') 259 | 260 | @property 261 | def checkout_id(self): 262 | return self._dict.get('checkout_id') 263 | 264 | @property 265 | def source_name(self): 266 | return self._dict.get('source_name') 267 | 268 | @property 269 | def fulfillment_status(self): 270 | """ 271 | Show fulfillment status. 272 | 273 | Values: 274 | - fulfilled (Every line item in the order has been fulfilled) 275 | - null (None of the line items in the order have been fulfilled) 276 | - partial (At least one line item in the order has been fulfilled) 277 | :return: 278 | """ 279 | return self._dict.get('fulfillment_status') 280 | 281 | @fulfillment_status.setter 282 | def fulfillment_status(self, val): 283 | self._dict['fulfillment_status'] = val 284 | 285 | @property 286 | def tax_lines(self): 287 | return [TaxLine(x) for x in self._dict.get('tax_lines', [])] 288 | 289 | @property 290 | def tags(self): 291 | """ 292 | Tags are additional short descriptors, commonly used for filtering and searching. 293 | :return: 294 | """ 295 | return tuple([x.strip() for x in self._dict.get('tags').split(',')]) 296 | 297 | @property 298 | def contact_email(self): 299 | return self._dict.get('contact_email') 300 | 301 | @property 302 | def order_status_url(self): 303 | return self._dict.get('order_status_url') 304 | 305 | @property 306 | def line_items(self): 307 | return [LineItem(x) for x in self._dict.get('line_items', [])] 308 | 309 | @property 310 | def shipping_lines(self): 311 | return [ShippingLine(x) for x in self._dict.get('shipping_lines', [])] 312 | 313 | @property 314 | def billing_address(self): 315 | return Address(self._dict.get('billing_address')) 316 | 317 | @property 318 | def shipping_address(self): 319 | """ 320 | The mailing address associated with the payment method. 321 | 322 | This address is an optional field that will not be available on orders that do not require one. 323 | :return: 324 | """ 325 | return Address(self._dict.get('shipping_address')) 326 | 327 | @property 328 | def fulfillments(self): 329 | """ 330 | Return a list of Fulfillment objects parsed from the fulfillments element of the order. 331 | :return: 332 | """ 333 | return [Fulfillment(x) for x in self._dict.get('fulfillments', [])] 334 | 335 | @property 336 | def client_details(self): 337 | """ 338 | An object containing information about the client. 339 | return: 340 | """ 341 | return ClientDetail(self._dict.get('client_details')) 342 | 343 | @property 344 | def refunds(self): 345 | return [Refund(x) for x in self._dict.get('refunds', [])] 346 | 347 | @property 348 | def payment_details(self): 349 | return PaymentDetail(self._dict.get('payment_details')) 350 | 351 | @property 352 | def customer(self): 353 | """ 354 | An object containing information about the customer. 355 | 356 | It is important to note that the order may not have a customer and consumers 357 | should not depend on the existence of a customer object. 358 | This value may be falsey if the order was created through Shopify POS. 359 | :return: 360 | """ 361 | return Customer(self._dict.get('customer')) 362 | -------------------------------------------------------------------------------- /shopify/orders/objects/payment_detail.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser 2 | 3 | 4 | class PaymentDetail(BaseParser): 5 | 6 | @property 7 | def credit_card_bin(self): 8 | return self._dict.get('credit_card_bin') 9 | 10 | @property 11 | def avs_result_code(self): 12 | return self._dict.get('avs_result_code') 13 | 14 | @property 15 | def cvv_result_code(self): 16 | return self._dict.get('cvv_result_code') 17 | 18 | @property 19 | def credit_card_number(self): 20 | return self._dict.get('credit_card_number') 21 | 22 | @property 23 | def credit_card_company(self): 24 | return self._dict.get('credit_card_company') 25 | -------------------------------------------------------------------------------- /shopify/orders/objects/refund.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser, string_to_datetime 2 | from .transaction import Transaction 3 | from .line_item import LineItem 4 | 5 | 6 | class Refund(BaseParser): 7 | 8 | @property 9 | def order_id(self): 10 | return self._dict.get('order_id') 11 | 12 | @property 13 | def created_at(self): 14 | return string_to_datetime(self._dict.get('created_at')) 15 | 16 | @property 17 | def note(self): 18 | return self._dict.get('note') 19 | 20 | @property 21 | def restock(self): 22 | return self._dict.get('restock') 23 | 24 | @property 25 | def user_id(self): 26 | return self._dict.get('user_id') 27 | 28 | @property 29 | def refund_line_items(self): 30 | return [LineItem(x) for x in self._dict.get('refund_line_items', [])] 31 | 32 | @property 33 | def transactions(self): 34 | return [Transaction(x) for x in self._dict.get('transactions', [])] 35 | 36 | @property 37 | def order_adjustments(self): 38 | # ToDo: Figure out what order_adjustments returns 39 | return [x for x in self._dict.get('order_adjustments', [])] 40 | -------------------------------------------------------------------------------- /shopify/orders/objects/shipping_line.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser, string_to_float 2 | from .tax_line import TaxLine 3 | 4 | 5 | class ShippingLine(BaseParser): 6 | 7 | @property 8 | def title(self): 9 | return self._dict.get('title') 10 | 11 | @property 12 | def price(self): 13 | return string_to_float(self._dict.get('price')) 14 | 15 | @property 16 | def code(self): 17 | return self._dict.get('code') 18 | 19 | @property 20 | def source(self): 21 | return self._dict.get('source') 22 | 23 | @property 24 | def phone(self): 25 | return self._dict.get('phone') 26 | 27 | @property 28 | def requested_fulfillment_service_id(self): 29 | return self._dict.get('requested_fulfillment_service_id') 30 | 31 | @property 32 | def delivery_category(self): 33 | return self._dict.get('delivery_category') 34 | 35 | @property 36 | def carrier_identifier(self): 37 | return self._dict.get('carrier_identifier') 38 | 39 | @property 40 | def tax_lines(self): 41 | return [TaxLine(x) for x in self._dict.get('tax_lines', [])] 42 | -------------------------------------------------------------------------------- /shopify/orders/objects/tax_line.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser, string_to_float 2 | 3 | 4 | class TaxLine(BaseParser): 5 | """ 6 | Parser for objects in the tax_lines list object. 7 | """ 8 | 9 | @property 10 | def title(self): 11 | return self._dict.get('title') 12 | 13 | @title.setter 14 | def title(self, val): 15 | self._dict['title'] = val 16 | 17 | @property 18 | def price(self): 19 | return string_to_float(self._dict.get('price')) 20 | 21 | @price.setter 22 | def price(self, val): 23 | self._dict['price'] = str(val) 24 | 25 | @property 26 | def rate(self): 27 | return self._dict.get('rate') 28 | 29 | @rate.setter 30 | def rate(self, val): 31 | self._dict['rate'] = float(val) 32 | -------------------------------------------------------------------------------- /shopify/orders/objects/transaction.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser, string_to_datetime, string_to_float 2 | 3 | 4 | class Transaction(BaseParser): 5 | 6 | @property 7 | def order_id(self): 8 | return self._dict.get('order_id') 9 | 10 | @property 11 | def amount(self): 12 | return string_to_float(self._dict.get('amount')) 13 | 14 | @property 15 | def kind(self): 16 | return self._dict.get('kind') 17 | 18 | @property 19 | def gateway(self): 20 | return self._dict.get('gateway') 21 | 22 | @property 23 | def status(self): 24 | return self._dict.get('status') 25 | 26 | @property 27 | def message(self): 28 | return self._dict.get('message') 29 | 30 | @property 31 | def created_at(self): 32 | return string_to_datetime(self._dict.get('created_at')) 33 | 34 | @property 35 | def test(self): 36 | return self._dict.get('test') 37 | 38 | @property 39 | def authorization(self): 40 | return self._dict.get('authorization') 41 | 42 | @property 43 | def currency(self): 44 | return self._dict.get('currency') 45 | 46 | @property 47 | def location_id(self): 48 | return self._dict.get('location_id') 49 | 50 | @property 51 | def user_id(self): 52 | return self._dict.get('user_id') 53 | 54 | @property 55 | def parent_id(self): 56 | return self._dict.get('parent_id') 57 | 58 | @property 59 | def device_id(self): 60 | return self._dict.get('device_id') 61 | 62 | @property 63 | def receipt(self): 64 | # ToDo: Figure out what elements are in receipt 65 | return self._dict.get('receipt') 66 | 67 | @property 68 | def error_code(self): 69 | return self._dict.get('error_code') 70 | 71 | @property 72 | def source_name(self): 73 | return self._dict.get('source_name') 74 | -------------------------------------------------------------------------------- /shopify/orders/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ziplokk1/python-shopify-api/fe7481e98d2197265ab44f13682d54bbd6679b92/shopify/orders/tests/__init__.py -------------------------------------------------------------------------------- /shopify/orders/tests/address.json: -------------------------------------------------------------------------------- 1 | { 2 | "first_name": "Test", 3 | "address1": "123 Test Ln", 4 | "phone": null, 5 | "city": "City", 6 | "zip": "12345", 7 | "province": "State", 8 | "country": "United States", 9 | "last_name": "Customer", 10 | "address2": "", 11 | "company": null, 12 | "latitude": 0.0, 13 | "longitude": 0.0, 14 | "name": "Test Customer", 15 | "country_code": "US", 16 | "province_code": "ST" 17 | } -------------------------------------------------------------------------------- /shopify/orders/tests/client_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "browser_ip": "192.168.1.1", 3 | "accept_language": "en-US,en;q=0.5", 4 | "user_agent": "Mozilla\/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko\/20100101 Firefox\/47.0", 5 | "session_hash": "1f29043960fc3c472ccda7d7f22c6fbe", 6 | "browser_width": 1855, 7 | "browser_height": 985 8 | } -------------------------------------------------------------------------------- /shopify/orders/tests/customer.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3778180163, 3 | "email": "test@email.com", 4 | "accepts_marketing": false, 5 | "created_at": "2016-07-26T09:52:25-05:00", 6 | "updated_at": "2016-07-26T09:52:57-05:00", 7 | "first_name": "Test", 8 | "last_name": "Customer", 9 | "orders_count": 0, 10 | "state": "disabled", 11 | "total_spent": "0.00", 12 | "last_order_id": null, 13 | "note": null, 14 | "verified_email": true, 15 | "multipass_identifier": null, 16 | "tax_exempt": false, 17 | "tags": "", 18 | "last_order_name": null, 19 | "default_address": {} 20 | } -------------------------------------------------------------------------------- /shopify/orders/tests/line_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 6207715523, 3 | "variant_id": 23950183043, 4 | "title": "Nature Made, Vitamin D3 2,000 I.U. Liquid Softgels, 250-Count", 5 | "quantity": 3, 6 | "price": "15.99", 7 | "grams": 454, 8 | "sku": "test-sku", 9 | "variant_title": "", 10 | "vendor": "Nature Made", 11 | "fulfillment_service": "manual", 12 | "product_id": 7583017923, 13 | "requires_shipping": true, 14 | "taxable": true, 15 | "gift_card": false, 16 | "name": "Nature Made, Vitamin D3 2,000 I.U. Liquid Softgels, 250-Count", 17 | "variant_inventory_management": "shopify", 18 | "properties": [], 19 | "product_exists": true, 20 | "fulfillable_quantity": 3, 21 | "total_discount": "0.00", 22 | "fulfillment_status": null, 23 | "tax_lines": [], 24 | "origin_location": {}, 25 | "destination_location": {} 26 | } -------------------------------------------------------------------------------- /shopify/orders/tests/location.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1573630275, 3 | "country_code": "US", 4 | "province_code": "ST", 5 | "name": "Test Store", 6 | "address1": "456 Test Dr", 7 | "address2": "", 8 | "city": "City", 9 | "zip": "67890" 10 | } -------------------------------------------------------------------------------- /shopify/orders/tests/order.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 3389059459, 3 | "email": "test@email.com", 4 | "closed_at": null, 5 | "created_at": "2016-07-26T09:52:57-05:00", 6 | "updated_at": "2016-07-26T09:52:57-05:00", 7 | "number": 1, 8 | "note": null, 9 | "token": "31a03d57375c347a7e1baf64a6147841", 10 | "gateway": "bogus", 11 | "test": true, 12 | "total_price": "60.03", 13 | "subtotal_price": "47.97", 14 | "total_weight": 1361, 15 | "total_tax": "4.06", 16 | "taxes_included": false, 17 | "currency": "USD", 18 | "financial_status": "paid", 19 | "confirmed": true, 20 | "total_discounts": "0.00", 21 | "total_line_items_price": "47.97", 22 | "cart_token": "4b921d873bf3bba40a619926fd390e15", 23 | "buyer_accepts_marketing": false, 24 | "name": "#1001", 25 | "referring_site": "", 26 | "landing_site": "\/", 27 | "cancelled_at": null, 28 | "cancel_reason": null, 29 | "total_price_usd": "60.03", 30 | "checkout_token": "78fedd56e5750664cfc2701cc26cbe5e", 31 | "reference": null, 32 | "user_id": null, 33 | "location_id": null, 34 | "source_identifier": null, 35 | "source_url": null, 36 | "processed_at": "2016-07-26T09:52:57-05:00", 37 | "device_id": null, 38 | "browser_ip": "192.168.1.1", 39 | "landing_site_ref": null, 40 | "order_number": 1001, 41 | "discount_codes": [], 42 | "note_attributes": [], 43 | "payment_gateway_names": [ 44 | "bogus" 45 | ], 46 | "processing_method": "direct", 47 | "checkout_id": 8240611139, 48 | "source_name": "web", 49 | "fulfillment_status": null, 50 | "tax_lines": [], 51 | "tags": "", 52 | "contact_email": "test@email.com", 53 | "order_status_url": "https:\/\/checkout.shopify.com\/", 54 | "line_items": [], 55 | "shipping_lines": [], 56 | "billing_address": {}, 57 | "shipping_address": {}, 58 | "fulfillments": [], 59 | "client_details": {}, 60 | "refunds": [], 61 | "payment_details": {}, 62 | "customer": {} 63 | } -------------------------------------------------------------------------------- /shopify/orders/tests/payment_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "credit_card_bin": "1", 3 | "avs_result_code": null, 4 | "cvv_result_code": null, 5 | "credit_card_number": "•••• •••• •••• 1", 6 | "credit_card_company": "Bogus" 7 | } -------------------------------------------------------------------------------- /shopify/orders/tests/response.json: -------------------------------------------------------------------------------- 1 | { 2 | "orders": [ 3 | { 4 | "id": 3389059459, 5 | "email": "test@email.com", 6 | "closed_at": null, 7 | "created_at": "2016-07-26T09:52:57-05:00", 8 | "updated_at": "2016-07-26T09:52:57-05:00", 9 | "number": 1, 10 | "note": null, 11 | "token": "31a03d57375c347a7e1baf64a6147841", 12 | "gateway": "bogus", 13 | "test": true, 14 | "total_price": "60.03", 15 | "subtotal_price": "47.97", 16 | "total_weight": 1361, 17 | "total_tax": "4.06", 18 | "taxes_included": false, 19 | "currency": "USD", 20 | "financial_status": "paid", 21 | "confirmed": true, 22 | "total_discounts": "0.00", 23 | "total_line_items_price": "47.97", 24 | "cart_token": "4b921d873bf3bba40a619926fd390e15", 25 | "buyer_accepts_marketing": false, 26 | "name": "#1001", 27 | "referring_site": "", 28 | "landing_site": "\/", 29 | "cancelled_at": null, 30 | "cancel_reason": null, 31 | "total_price_usd": "60.03", 32 | "checkout_token": "78fedd56e5750664cfc2701cc26cbe5e", 33 | "reference": null, 34 | "user_id": null, 35 | "location_id": null, 36 | "source_identifier": null, 37 | "source_url": null, 38 | "processed_at": "2016-07-26T09:52:57-05:00", 39 | "device_id": null, 40 | "browser_ip": "192.168.1.1", 41 | "landing_site_ref": null, 42 | "order_number": 1001, 43 | "discount_codes": [], 44 | "note_attributes": [], 45 | "payment_gateway_names": [ 46 | "bogus" 47 | ], 48 | "processing_method": "direct", 49 | "checkout_id": 8240611139, 50 | "source_name": "web", 51 | "fulfillment_status": null, 52 | "tax_lines": [ 53 | { 54 | "title": "State Tax", 55 | "price": "3.50", 56 | "rate": 0.0625 57 | }, 58 | { 59 | "title": "Municipal Tax", 60 | "price": "0.56", 61 | "rate": 0.01 62 | } 63 | ], 64 | "tags": "", 65 | "contact_email": "test@email.com", 66 | "order_status_url": "https:\/\/checkout.shopify.com\/", 67 | "line_items": [ 68 | { 69 | "id": 6207715523, 70 | "variant_id": 23950183043, 71 | "title": "Nature Made, Vitamin D3 2,000 I.U. Liquid Softgels, 250-Count", 72 | "quantity": 3, 73 | "price": "15.99", 74 | "grams": 454, 75 | "sku": "test-sku", 76 | "variant_title": "", 77 | "vendor": "Nature Made", 78 | "fulfillment_service": "manual", 79 | "product_id": 7583017923, 80 | "requires_shipping": true, 81 | "taxable": true, 82 | "gift_card": false, 83 | "name": "Nature Made, Vitamin D3 2,000 I.U. Liquid Softgels, 250-Count", 84 | "variant_inventory_management": "shopify", 85 | "properties": [], 86 | "product_exists": true, 87 | "fulfillable_quantity": 3, 88 | "total_discount": "0.00", 89 | "fulfillment_status": null, 90 | "tax_lines": [ 91 | { 92 | "title": "State Tax", 93 | "price": "3.00", 94 | "rate": 0.0625 95 | }, 96 | { 97 | "title": "Municipal Tax", 98 | "price": "0.48", 99 | "rate": 0.01 100 | } 101 | ], 102 | "origin_location": { 103 | "id": 1573630275, 104 | "country_code": "US", 105 | "province_code": "ST", 106 | "name": "Test Store", 107 | "address1": "456 Test Dr", 108 | "address2": "", 109 | "city": "City", 110 | "zip": "67890" 111 | }, 112 | "destination_location": { 113 | "id": 1616145027, 114 | "country_code": "US", 115 | "province_code": "ST", 116 | "name": "Test Customer", 117 | "address1": "123 Test Ln", 118 | "address2": "", 119 | "city": "City", 120 | "zip": "12345" 121 | } 122 | } 123 | ], 124 | "shipping_lines": [ 125 | { 126 | "id": 2772048707, 127 | "title": "Standard Shipping", 128 | "price": "8.00", 129 | "code": "Standard Shipping", 130 | "source": "shopify", 131 | "phone": null, 132 | "requested_fulfillment_service_id": null, 133 | "delivery_category": null, 134 | "carrier_identifier": null, 135 | "tax_lines": [ 136 | { 137 | "title": "State Tax", 138 | "price": "0.50", 139 | "rate": 0.0625 140 | }, 141 | { 142 | "title": "Municipal Tax", 143 | "price": "0.08", 144 | "rate": 0.01 145 | } 146 | ] 147 | } 148 | ], 149 | "billing_address": { 150 | "first_name": "Test", 151 | "address1": "123 Test Ln", 152 | "phone": null, 153 | "city": "City", 154 | "zip": "12345", 155 | "province": "State", 156 | "country": "United States", 157 | "last_name": "Customer", 158 | "address2": "", 159 | "company": null, 160 | "latitude": 0.0, 161 | "longitude": 0.0, 162 | "name": "Test Customer", 163 | "country_code": "US", 164 | "province_code": "ST" 165 | }, 166 | "shipping_address": { 167 | "first_name": "Test", 168 | "address1": "123 Test Ln", 169 | "phone": null, 170 | "city": "City", 171 | "zip": "12345", 172 | "province": "State", 173 | "country": "United States", 174 | "last_name": "Customer", 175 | "address2": "", 176 | "company": null, 177 | "latitude": 0.0, 178 | "longitude": 0.0, 179 | "name": "Test Customer", 180 | "country_code": "US", 181 | "province_code": "ST" 182 | }, 183 | "fulfillments": [], 184 | "client_details": { 185 | "browser_ip": "192.168.1.1", 186 | "accept_language": "en-US,en;q=0.5", 187 | "user_agent": "Mozilla\/5.0 (X11; Ubuntu; Linux x86_64; rv:47.0) Gecko\/20100101 Firefox\/47.0", 188 | "session_hash": "1f29043960fc3c472ccda7d7f22c6fbe", 189 | "browser_width": 1855, 190 | "browser_height": 985 191 | }, 192 | "refunds": [], 193 | "payment_details": { 194 | "credit_card_bin": "1", 195 | "avs_result_code": null, 196 | "cvv_result_code": null, 197 | "credit_card_number": "•••• •••• •••• 1", 198 | "credit_card_company": "Bogus" 199 | }, 200 | "customer": { 201 | "id": 3778180163, 202 | "email": "test@email.com", 203 | "accepts_marketing": false, 204 | "created_at": "2016-07-26T09:52:25-05:00", 205 | "updated_at": "2016-07-26T09:52:57-05:00", 206 | "first_name": "Test", 207 | "last_name": "Customer", 208 | "orders_count": 0, 209 | "state": "disabled", 210 | "total_spent": "0.00", 211 | "last_order_id": null, 212 | "note": null, 213 | "verified_email": true, 214 | "multipass_identifier": null, 215 | "tax_exempt": false, 216 | "tags": "", 217 | "last_order_name": null, 218 | "default_address": { 219 | "id": 3988356099, 220 | "first_name": "Test", 221 | "last_name": "Customer", 222 | "company": null, 223 | "address1": "123 Test Ln", 224 | "address2": "", 225 | "city": "City", 226 | "province": "State", 227 | "country": "United States", 228 | "zip": "12345", 229 | "phone": null, 230 | "name": "Test Customer", 231 | "province_code": "ST", 232 | "country_code": "US", 233 | "country_name": "United States", 234 | "default": true 235 | } 236 | } 237 | } 238 | ] 239 | } -------------------------------------------------------------------------------- /shopify/orders/tests/shipping_line.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2772048707, 3 | "title": "Standard Shipping", 4 | "price": "8.00", 5 | "code": "Standard Shipping", 6 | "source": "shopify", 7 | "phone": null, 8 | "requested_fulfillment_service_id": null, 9 | "delivery_category": null, 10 | "carrier_identifier": null, 11 | "tax_lines": [] 12 | } -------------------------------------------------------------------------------- /shopify/orders/tests/tax_line.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "State Tax", 3 | "price": "3.50", 4 | "rate": 0.0625 5 | } -------------------------------------------------------------------------------- /shopify/orders/tests/test_address.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | from unittest import TestCase 6 | 7 | # Prevent relative import errors 8 | file_ = os.path.abspath(__file__) 9 | tests_ = os.path.dirname(file_) 10 | products_ = os.path.dirname(tests_) 11 | shopify_ = os.path.dirname(products_) 12 | root = os.path.dirname(shopify_) 13 | sys.path.append(root) 14 | 15 | from shopify.orders import Address 16 | 17 | 18 | class TestAddress(TestCase): 19 | 20 | def setUp(self): 21 | with open(os.path.join(tests_, 'address.json'), 'rb') as f: 22 | self.data = json.load(f) 23 | self.address = Address(self.data) 24 | 25 | def test_first_name(self): 26 | self.failUnlessEqual(self.data['first_name'], self.address.first_name) 27 | 28 | def test_address_1(self): 29 | self.failUnlessEqual(self.data['address1'], self.address.address_1) 30 | 31 | def test_phone(self): 32 | self.failUnlessEqual(self.data['phone'], self.address.phone) 33 | 34 | def test_city(self): 35 | self.failUnlessEqual(self.data['city'], self.address.city) 36 | 37 | def test_zip(self): 38 | self.failUnlessEqual(self.data['zip'], self.address.zip) 39 | 40 | def test_province(self): 41 | self.failUnlessEqual(self.data['province'], self.address.province) 42 | 43 | def test_country(self): 44 | self.failUnlessEqual(self.data['country'], self.address.country) 45 | 46 | def test_last_name(self): 47 | self.failUnlessEqual(self.data['last_name'], self.address.last_name) 48 | 49 | def test_address_2(self): 50 | self.failUnlessEqual(self.data['address2'], self.address.address_2) 51 | 52 | def test_company(self): 53 | self.failUnlessEqual(self.data['company'], self.address.company) 54 | 55 | def test_longitude(self): 56 | self.failUnlessEqual(self.data['latitude'], self.address.latitude) 57 | 58 | def test_latitude(self): 59 | self.failUnlessEqual(self.data['longitude'], self.address.longitude) 60 | 61 | def test_country_code(self): 62 | self.failUnlessEqual(self.data['country_code'], self.address.country_code) 63 | 64 | def test_province_code(self): 65 | self.failUnlessEqual(self.data['province_code'], self.address.province_code) 66 | 67 | 68 | if __name__ == '__main__': 69 | import unittest 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /shopify/orders/tests/test_all.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from test_address import TestAddress 4 | from test_clientDetail import TestClientDetail 5 | from test_customer import TestCustomer 6 | # from test_lineItem import TestLineItem 7 | # #from test_order import TestOrder 8 | from test_paymentDetail import TestPaymentDetail 9 | from test_shippingLine import TestShippingLine 10 | from test_taxLine import TestTaxLine 11 | 12 | 13 | if __name__ == '__main__': 14 | unittest.main() 15 | -------------------------------------------------------------------------------- /shopify/orders/tests/test_clientDetail.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | import datetime 5 | 6 | from unittest import TestCase 7 | 8 | # Prevent relative import errors 9 | file_ = os.path.abspath(__file__) 10 | tests_ = os.path.dirname(file_) 11 | products_ = os.path.dirname(tests_) 12 | shopify_ = os.path.dirname(products_) 13 | root = os.path.dirname(shopify_) 14 | sys.path.append(root) 15 | 16 | from shopify.orders import ClientDetail 17 | 18 | 19 | class TestClientDetail(TestCase): 20 | 21 | def setUp(self): 22 | with open(os.path.join(tests_, 'client_detail.json'), 'rb') as f: 23 | self.data = json.load(f) 24 | self.client_detail = ClientDetail(self.data) 25 | 26 | def test_browser_ip(self): 27 | self.failUnlessEqual(self.data['browser_ip'], self.client_detail.browser_ip) 28 | 29 | def test_accept_language(self): 30 | self.failUnlessEqual(self.data['accept_language'], self.client_detail.accept_language) 31 | 32 | def test_user_agent(self): 33 | self.failUnlessEqual(self.data['user_agent'], self.client_detail.user_agent) 34 | 35 | def test_session_hash(self): 36 | self.failUnlessEqual(self.data['session_hash'], self.client_detail.session_hash) 37 | 38 | def test_browser_width(self): 39 | self.failUnlessEqual(self.data['browser_width'], self.client_detail.browser_width) 40 | 41 | def test_browser_height(self): 42 | self.failUnlessEqual(self.data['browser_height'], self.client_detail.browser_height) 43 | 44 | 45 | if __name__ == '__main__': 46 | import unittest 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /shopify/orders/tests/test_customer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | import datetime 5 | 6 | from unittest import TestCase 7 | 8 | # Prevent relative import errors 9 | file_ = os.path.abspath(__file__) 10 | tests_ = os.path.dirname(file_) 11 | products_ = os.path.dirname(tests_) 12 | shopify_ = os.path.dirname(products_) 13 | root = os.path.dirname(shopify_) 14 | sys.path.append(root) 15 | 16 | from shopify.orders import Customer 17 | 18 | 19 | class TestCustomer(TestCase): 20 | 21 | def setUp(self): 22 | with open(os.path.join(tests_, 'customer.json'), 'rb') as f: 23 | self.data = json.load(f) 24 | self.customer = Customer(self.data) 25 | 26 | def _compare_dt(self, og_dt_key, dt2): 27 | og = self.data[og_dt_key].rpartition('-')[0] # Remove the utc offset from the end of the timestamp 28 | og_dt = datetime.datetime.strptime(og, '%Y-%m-%dT%H:%M:%S') 29 | psr_dt = dt2.replace(tzinfo=None) 30 | self.failUnlessEqual(og_dt, psr_dt) 31 | 32 | def test_id(self): 33 | self.failUnlessEqual(self.data['id'], self.customer.id) 34 | 35 | def test_email(self): 36 | self.failUnlessEqual(self.data['email'], self.customer.email) 37 | 38 | def test_accepts_marketing(self): 39 | self.failUnlessEqual(self.data['accepts_marketing'], self.customer.accepts_marketing) 40 | 41 | def test_created_at(self): 42 | self._compare_dt('created_at', self.customer.created_at) 43 | 44 | def test_updated_at(self): 45 | self._compare_dt('updated_at', self.customer.updated_at) 46 | 47 | def test_first_name(self): 48 | self.failUnlessEqual(self.data['first_name'], self.customer.first_name) 49 | 50 | def test_last_name(self): 51 | self.failUnlessEqual(self.data['last_name'], self.customer.last_name) 52 | 53 | def test_orders_count(self): 54 | self.failUnlessEqual(self.data['orders_count'], self.customer.orders_count) 55 | 56 | def test_state(self): 57 | self.failUnlessEqual(self.data['state'], self.customer.state) 58 | 59 | def test_total_spent(self): 60 | self.failUnlessEqual(float(self.data['total_spent']), self.customer.total_spent) 61 | 62 | def test_last_order_id(self): 63 | self.failUnlessEqual(self.data['last_order_id'], self.customer.last_order_id) 64 | 65 | def test_note(self): 66 | self.failUnlessEqual(self.data['note'], self.customer.note) 67 | 68 | def test_verified_email(self): 69 | self.failUnlessEqual(self.data['verified_email'], self.customer.verified_email) 70 | 71 | def test_multipass_identifier(self): 72 | self.failUnlessEqual(self.data['multipass_identifier'], self.customer.multipass_identifier) 73 | 74 | def test_tax_exempt(self): 75 | self.failUnlessEqual(self.data['tax_exempt'], self.customer.tax_exempt) 76 | 77 | def test_tags(self): 78 | self.failUnlessEqual(self.data['tags'], self.customer.tags) 79 | 80 | def test_last_order_name(self): 81 | self.failUnlessEqual(self.data['last_order_name'], self.customer.last_order_name) 82 | 83 | 84 | if __name__ == '__main__': 85 | import unittest 86 | unittest.main() 87 | -------------------------------------------------------------------------------- /shopify/orders/tests/test_lineItem.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | 4 | class TestLineItem(TestCase): 5 | def test_variant_id(self): 6 | self.fail() 7 | 8 | def test_title(self): 9 | self.fail() 10 | 11 | def test_quantity(self): 12 | self.fail() 13 | 14 | def test_price(self): 15 | self.fail() 16 | 17 | def test_grams(self): 18 | self.fail() 19 | 20 | def test_sku(self): 21 | self.fail() 22 | 23 | def test_variant_title(self): 24 | self.fail() 25 | 26 | def test_vendor(self): 27 | self.fail() 28 | 29 | def test_fulfillment_service(self): 30 | self.fail() 31 | 32 | def test_product_id(self): 33 | self.fail() 34 | 35 | def test_requires_shipping(self): 36 | self.fail() 37 | 38 | def test_taxable(self): 39 | self.fail() 40 | 41 | def test_gift_card(self): 42 | self.fail() 43 | 44 | def test_name(self): 45 | self.fail() 46 | 47 | def test_variant_inventory_management(self): 48 | self.fail() 49 | 50 | def test_properties(self): 51 | self.fail() 52 | 53 | def test_product_exists(self): 54 | self.fail() 55 | 56 | def test_fulfillable_quantity(self): 57 | self.fail() 58 | 59 | def test_total_discount(self): 60 | self.fail() 61 | 62 | def test_fulfillment_status(self): 63 | self.fail() 64 | 65 | def test_tax_lines(self): 66 | self.fail() 67 | 68 | def test_origin_location(self): 69 | self.fail() 70 | 71 | def test_destination_location(self): 72 | self.fail() 73 | -------------------------------------------------------------------------------- /shopify/orders/tests/test_order.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | 4 | class TestOrder(TestCase): 5 | def test_email(self): 6 | self.fail() 7 | 8 | def test_closed_at(self): 9 | self.fail() 10 | 11 | def test_created_at(self): 12 | self.fail() 13 | 14 | def test_updated_at(self): 15 | self.fail() 16 | 17 | def test_number(self): 18 | self.fail() 19 | 20 | def test_note(self): 21 | self.fail() 22 | 23 | def test_token(self): 24 | self.fail() 25 | 26 | def test_gateway(self): 27 | self.fail() 28 | 29 | def test_test(self): 30 | self.fail() 31 | 32 | def test_total_price(self): 33 | self.fail() 34 | 35 | def test_subtotal_price(self): 36 | self.fail() 37 | 38 | def test_total_weight(self): 39 | self.fail() 40 | 41 | def test_total_tax(self): 42 | self.fail() 43 | 44 | def test_taxes_included(self): 45 | self.fail() 46 | 47 | def test_currency(self): 48 | self.fail() 49 | 50 | def test_financial_status(self): 51 | self.fail() 52 | 53 | def test_confirmed(self): 54 | self.fail() 55 | 56 | def test_total_discounts(self): 57 | self.fail() 58 | 59 | def test_total_line_items_price(self): 60 | self.fail() 61 | 62 | def test_cart_token(self): 63 | self.fail() 64 | 65 | def test_buyer_accepts_marketing(self): 66 | self.fail() 67 | 68 | def test_name(self): 69 | self.fail() 70 | 71 | def test_referring_site(self): 72 | self.fail() 73 | 74 | def test_landing_site(self): 75 | self.fail() 76 | 77 | def test_cancelled_at(self): 78 | self.fail() 79 | 80 | def test_cancel_reason(self): 81 | self.fail() 82 | 83 | def test_total_price_usd(self): 84 | self.fail() 85 | 86 | def test_checkout_token(self): 87 | self.fail() 88 | 89 | def test_reference(self): 90 | self.fail() 91 | 92 | def test_user_id(self): 93 | self.fail() 94 | 95 | def test_location_id(self): 96 | self.fail() 97 | 98 | def test_source_identifier(self): 99 | self.fail() 100 | 101 | def test_source_url(self): 102 | self.fail() 103 | 104 | def test_processed_at(self): 105 | self.fail() 106 | 107 | def test_device_id(self): 108 | self.fail() 109 | 110 | def test_browser_ip(self): 111 | self.fail() 112 | 113 | def test_landing_site_ref(self): 114 | self.fail() 115 | 116 | def test_order_number(self): 117 | self.fail() 118 | 119 | def test_discount_codes(self): 120 | self.fail() 121 | 122 | def test_note_attributes(self): 123 | self.fail() 124 | 125 | def test_payment_gateway_names(self): 126 | self.fail() 127 | 128 | def test_processing_method(self): 129 | self.fail() 130 | 131 | def test_checkout_id(self): 132 | self.fail() 133 | 134 | def test_source_name(self): 135 | self.fail() 136 | 137 | def test_fulfillment_status(self): 138 | self.fail() 139 | 140 | def test_tax_lines(self): 141 | self.fail() 142 | 143 | def test_tags(self): 144 | self.fail() 145 | -------------------------------------------------------------------------------- /shopify/orders/tests/test_paymentDetail.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | from unittest import TestCase 6 | 7 | # Prevent relative import errors 8 | file_ = os.path.abspath(__file__) 9 | tests_ = os.path.dirname(file_) 10 | products_ = os.path.dirname(tests_) 11 | shopify_ = os.path.dirname(products_) 12 | root = os.path.dirname(shopify_) 13 | sys.path.append(root) 14 | 15 | from shopify.orders import PaymentDetail 16 | 17 | 18 | class TestPaymentDetail(TestCase): 19 | 20 | def setUp(self): 21 | with open(os.path.join(tests_, 'payment_detail.json'), 'rb') as f: 22 | self.data = json.load(f) 23 | self.payment_detail = PaymentDetail(self.data) 24 | 25 | def test_credit_card_bin(self): 26 | self.failUnlessEqual(self.data['credit_card_bin'], self.payment_detail.credit_card_bin) 27 | 28 | def test_avs_result_code(self): 29 | self.failUnlessEqual(self.data['avs_result_code'], self.payment_detail.avs_result_code) 30 | 31 | def test_cvv_result_code(self): 32 | self.failUnlessEqual(self.data['cvv_result_code'], self.payment_detail.cvv_result_code) 33 | 34 | def test_credit_card_number(self): 35 | self.failUnlessEqual(self.data['credit_card_number'], self.payment_detail.credit_card_number) 36 | 37 | def test_credit_card_company(self): 38 | self.failUnlessEqual(self.data['credit_card_company'], self.payment_detail.credit_card_company) 39 | 40 | 41 | if __name__ == '__main__': 42 | import unittest 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /shopify/orders/tests/test_shippingLine.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | from unittest import TestCase 6 | 7 | # Prevent relative import errors 8 | file_ = os.path.abspath(__file__) 9 | tests_ = os.path.dirname(file_) 10 | products_ = os.path.dirname(tests_) 11 | shopify_ = os.path.dirname(products_) 12 | root = os.path.dirname(shopify_) 13 | sys.path.append(root) 14 | 15 | from shopify.orders import ShippingLine 16 | 17 | 18 | class TestShippingLine(TestCase): 19 | 20 | def setUp(self): 21 | with open(os.path.join(tests_, 'shipping_line.json'), 'rb') as f: 22 | self.data = json.load(f) 23 | self.shipping_line = ShippingLine(self.data) 24 | 25 | def test_title(self): 26 | self.failUnlessEqual(self.data['title'], self.shipping_line.title) 27 | 28 | def test_price(self): 29 | self.failUnlessEqual(float(self.data['price']), self.shipping_line.price) 30 | 31 | def test_code(self): 32 | self.failUnlessEqual(self.data['code'], self.shipping_line.code) 33 | 34 | def test_source(self): 35 | self.failUnlessEqual(self.data['source'], self.shipping_line.source) 36 | 37 | def test_phone(self): 38 | self.failUnlessEqual(self.data['phone'], self.shipping_line.phone) 39 | 40 | def test_requested_fulfillment_service_id(self): 41 | self.failUnlessEqual(self.data['requested_fulfillment_service_id'], self.shipping_line.requested_fulfillment_service_id) 42 | 43 | def test_delivery_category(self): 44 | self.failUnlessEqual(self.data['delivery_category'], self.shipping_line.delivery_category) 45 | 46 | def test_carrier_identifier(self): 47 | self.failUnlessEqual(self.data['carrier_identifier'], self.shipping_line.carrier_identifier) 48 | 49 | 50 | if __name__ == '__main__': 51 | import unittest 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /shopify/orders/tests/test_taxLine.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import sys 4 | 5 | from unittest import TestCase 6 | 7 | # Prevent relative import errors 8 | file_ = os.path.abspath(__file__) 9 | tests_ = os.path.dirname(file_) 10 | products_ = os.path.dirname(tests_) 11 | shopify_ = os.path.dirname(products_) 12 | root = os.path.dirname(shopify_) 13 | sys.path.append(root) 14 | 15 | from shopify.orders import TaxLine 16 | 17 | 18 | class TestTaxLine(TestCase): 19 | 20 | def setUp(self): 21 | with open(os.path.join(tests_, 'tax_line.json'), 'rb') as f: 22 | self.data = json.load(f) 23 | self.tax_line = TaxLine(self.data) 24 | 25 | def test_title(self): 26 | self.failUnlessEqual(self.data['title'], self.tax_line.title) 27 | 28 | def test_price(self): 29 | self.failUnlessEqual(float(self.data['price']), self.tax_line.price) 30 | 31 | def test_rate(self): 32 | self.failUnlessEqual(self.data['rate'], self.tax_line.rate) 33 | 34 | 35 | if __name__ == '__main__': 36 | import unittest 37 | unittest.main() 38 | -------------------------------------------------------------------------------- /shopify/products/__init__.py: -------------------------------------------------------------------------------- 1 | from objects import * 2 | from api_wrapper import ProductsApiWrapper 3 | 4 | 5 | def count(api_key, api_password, store_name): 6 | import json 7 | import requests 8 | 9 | url = 'https://{}:{}@{}.myshopify.com/admin/products/count.json'.format(api_key, api_password, store_name) 10 | response = requests.get(url) 11 | data = json.loads(response.content) 12 | return int(data['count']) 13 | -------------------------------------------------------------------------------- /shopify/products/api_wrapper.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | from .objects import Product 6 | from ..base import ShopifyApiWrapper, ShopifyApiError, datetime_to_string 7 | 8 | 9 | class ProductsApiWrapper(ShopifyApiWrapper): 10 | 11 | valid_published_status_values = [ 12 | 'published', 13 | 'unpublished', 14 | 'any' 15 | ] 16 | 17 | max_results_limit = 250 18 | 19 | default_endpoint = '/admin/products.json' 20 | operational_endpoint = '/admin/products/{}.json' 21 | 22 | def update(self, product): 23 | if not product.id: 24 | raise ValueError('product must have an ID to use `update`') 25 | endpoint = self.operational_endpoint.format(product.id) 26 | url = self.url_host() + endpoint 27 | d = json.dumps(product.__dict__) 28 | 29 | response = requests.put(url, data=d, headers={'Content-Type': 'application/json'}) 30 | 31 | err_check = ShopifyApiError(response) 32 | if err_check.has_error(): 33 | raise err_check 34 | 35 | def create(self, product): 36 | """ 37 | Create a product in inventory. 38 | 39 | :param product: A Product object instance. 40 | :return: 41 | """ 42 | url = self.url_host() + self.default_endpoint 43 | d = json.dumps(product.__dict__) 44 | 45 | # with open('shopify-create-product-body.json', 'wb') as f: 46 | # f.write(d) 47 | 48 | response = requests.post( 49 | url, 50 | data=d, 51 | headers={'Content-Type': 'application/json'} 52 | ) 53 | 54 | # with open('shopfiy-create-product-response.json', 'wb') as f: 55 | # f.write(response.content) 56 | 57 | err_check = ShopifyApiError(response) 58 | if err_check.has_error(): 59 | raise err_check 60 | 61 | return Product(json.loads(response.content).get('product')) 62 | 63 | def _delete(self, id_): 64 | """ 65 | Delete product from inventory using the products ID. 66 | 67 | :param id_: The products ID to delete. 68 | :return: 69 | """ 70 | endpoint = self.operational_endpoint.format(id_) 71 | url = self.url_host() + endpoint 72 | response = requests.delete(url) 73 | 74 | err_check = ShopifyApiError(response) 75 | if err_check.has_error(): 76 | raise err_check 77 | 78 | def delete(self, product): 79 | """ 80 | Delete a product from inventory. 81 | 82 | Use when you have a product instance returned from list(). 83 | 84 | :param product: A Product object. 85 | :return: 86 | """ 87 | if not product.id: 88 | raise ValueError('product must have an ID to use `delete`') 89 | self._delete(product.id) 90 | 91 | def delete_by_id(self, id_): 92 | """ 93 | Delete a product from inventory using the product's ID. 94 | 95 | :param id_: The numerical id for a product. 96 | :return: 97 | """ 98 | self._delete(id_) 99 | 100 | def list(self, ids=(), limit=50, page=1, since_id=None, title=None, 101 | vendor=None, handle=None, product_type=None, collection_id=None, 102 | created_at_min=None, created_at_max=None, updated_at_min=None, 103 | updated_at_max=None, published_at_min=None, published_at_max=None, 104 | published_status='any', fields=()): 105 | """ 106 | Return a list of products which are associated with your shopify account. 107 | 108 | :param ids: product ids to request. 109 | :param limit: Amount of results. (maximum: 250). 110 | :param page: Page to show. 111 | :param since_id: Restrict results to after the specified ID. 112 | :param title: Filter by product title. 113 | :param vendor: Filter by product vendor. 114 | :param handle: Filter by product handle. 115 | :param product_type: Filter by product type. 116 | :param collection_id: Filter by collection ID. 117 | :param created_at_min: Show products created after date. 118 | :param created_at_max: Show products create before date. 119 | :param updated_at_min: Show products last update after date. 120 | :param updated_at_max: Show products last updated before date. 121 | :param published_at_min: Show products published after date. 122 | :param published_at_max: Show products published before date. 123 | :param published_status: Filter by published status. 124 | Valid Values: 125 | published - Show only published products. 126 | unpublished - Show only unpublished products. 127 | any - Show all products. 128 | :param fields: List of fields to include in the response. 129 | :return: 130 | """ 131 | url = self.url_host() + self.default_endpoint 132 | if published_status not in self.valid_published_status_values: 133 | raise ValueError('`published_status` must be one of {}'.format(self.valid_published_status_values)) 134 | if limit > self.max_results_limit: 135 | raise ValueError('`limit` cannot exceed {}'.format(self.max_results_limit)) 136 | params = dict( 137 | ids=','.join(ids), 138 | limit=limit, 139 | page=page, 140 | since_id=since_id, 141 | title=title, 142 | vendor=vendor, 143 | handle=handle, 144 | product_type=product_type, 145 | collection_id=collection_id, 146 | created_at_min=datetime_to_string(created_at_min), 147 | created_at_max=datetime_to_string(created_at_max), 148 | updated_at_min=datetime_to_string(updated_at_min), 149 | updated_at_max=datetime_to_string(updated_at_max), 150 | published_at_min=datetime_to_string(published_at_min), 151 | published_at_max=datetime_to_string(published_at_max), 152 | published_status=published_status, 153 | fields=','.join(fields) 154 | ) 155 | params = self.remove_empty(params) 156 | response = requests.get(url, params=params) 157 | # with open('products-list.json', 'wb') as f: 158 | # f.write(response.content) 159 | data = json.loads(response.content) 160 | return [Product(x) for x in data.get('products', [])] 161 | -------------------------------------------------------------------------------- /shopify/products/objects/__init__.py: -------------------------------------------------------------------------------- 1 | from .product import Product 2 | from .image import Image 3 | from .variant import Variant 4 | from .option import Option 5 | -------------------------------------------------------------------------------- /shopify/products/objects/image.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | from ...base import BaseParser, datetime_to_string, string_to_datetime 4 | 5 | 6 | class Image(BaseParser): 7 | 8 | def __init__(self, d=None, **kwargs): 9 | BaseParser.__init__(self, d, **kwargs) 10 | self.image_location = None 11 | 12 | @property 13 | def product_id(self): 14 | return self._dict.get('product_id') 15 | 16 | # No product_id setter since that value shouldn't be modified. 17 | 18 | @property 19 | def position(self): 20 | return self._dict.get('position') 21 | 22 | @position.setter 23 | def position(self, val): 24 | self._dict['position'] = int(val) 25 | 26 | @property 27 | def created_at(self): 28 | return string_to_datetime(self._dict.get('created_at')) 29 | 30 | @created_at.setter 31 | def created_at(self, val): 32 | self._dict['created_at'] = datetime_to_string(val) 33 | 34 | @property 35 | def updated_at(self): 36 | return string_to_datetime(self._dict.get('updated_at')) 37 | 38 | @updated_at.setter 39 | def updated_at(self, val): 40 | self._dict['updated_at'] = datetime_to_string(val) 41 | 42 | @property 43 | def src(self): 44 | return self._dict.get('src') 45 | 46 | @src.setter 47 | def src(self, val): 48 | self._dict['src'] = val 49 | 50 | def attach(self, f): 51 | """ 52 | Attach an image file instead of using a url. 53 | :param f: Path to image file. 54 | :return: 55 | """ 56 | with open(f, 'rb') as f: 57 | encoded = base64.b64encode(f.read()) 58 | self._dict['attachment'] = encoded 59 | self.image_location = f 60 | 61 | def __repr__(self): 62 | return ''.format(self.id, self.src, self.image_location) 63 | -------------------------------------------------------------------------------- /shopify/products/objects/option.py: -------------------------------------------------------------------------------- 1 | from ...base import BaseParser 2 | 3 | 4 | class Option(BaseParser): 5 | 6 | @property 7 | def product_id(self): 8 | return self._dict.get('product_id') 9 | 10 | # No product_id setter since that value shouldn't be modified. 11 | 12 | @property 13 | def name(self): 14 | return self._dict.get('name') 15 | 16 | @name.setter 17 | def name(self, val): 18 | self._dict['name'] = val 19 | 20 | @property 21 | def position(self): 22 | return self._dict.get('position') 23 | 24 | @position.setter 25 | def position(self, val): 26 | self._dict['position'] = int(val) 27 | 28 | @property 29 | def values(self): 30 | return self._dict.get('values') 31 | 32 | @values.setter 33 | def values(self, val): 34 | self._dict['values'] = val 35 | 36 | def __repr__(self): 37 | return '